Men的博客

欢迎光临!

0%

内存管理

https://www.jishudog.com/8744/html
Tagged Pointer
所以我们可以将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。
所有类都继承自NSObject,因此每个对象都有一个isa指针指向它所属的类。在《ARM64 and You》文章中指出:在32位环境下,对象的引用计数都保存在一个外部的表中,
而对引用计数的增减操作都要先锁定这个表,操作完成后才解锁。这个效率是非常慢的。
而在64位环境下,isa也是64位,实际作为指针部分只用到的其中33位,剩余的部分会运用到Tagged Pointer的概念,其中19位将保存对象的引用计数,这样对引用计数的操作只需要原子的修改这个指针即可,
如果引用计数超出19位,才会将引用计数保存到外部表,而这种情况往往是很少的,因此效率将会大大提高。

1.Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate
2.Tagged Pointer指针的值不再是地址了,而是真正的值。
实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。
它的内存并不存储在堆中,也不需要malloc和free。
3.在内存读取上有着3倍的效率,创建时比以前快106倍。
4.它不单单是一个指针,还包括了其值+类型

dispatch_queue_t queue = dispatch_queue_create(“queue”, DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 1000; i ++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@”abcdefghijklmn”];
});
}
运行结果:崩溃(坏内存访问)
因为setter方法中,对strong修饰的属性会有一个retain和release的操作。在并发多线程的赋值操作中,都是对_name指针进行的操作,可能在_name刚刚被release后进行赋值操作,这个时候_name指向的内存地址是已经被释放了,所以造成了坏内存访问崩溃
解决办法:
1.异步改同步
2.将属性改成原子性
3.加锁

dispatch_queue_t queue = dispatch_queue_create(“queue”, DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 1000; i ++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@”a”];
});
}
为什么不崩溃了?因为没有用到引用计数的内存管理方法,使用的是TaggedPointer
从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象存储

NONPOINTER_ISA
 NONPOINTER_ISA在64位机上,对象的isa区域不再只是一个指向另一块存储空间的指针。还包含了更多信息,比如引用计数,析构状态,被其他weak 变量引用情况等。如果引用计数超过了当前指针所能表示的范围,Runtime 会使用一张散列表来管理用计数。
(union),利用联合体可以用相同的存储空间存储不同型别的数据类型,从而节省内存空间
ARC:由LLVM和Runtime共同协作来进行自动引用计数的。
  uintptr_t nonpointer : 1;

        uintptr_t has_assoc : 1; //

        uintptr_t has_cxx_dtor : 1;

        uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000

        uintptr_t magic : 6;

        uintptr_t weakly_referenced : 1;

        uintptr_t deallocating : 1;

        uintptr_t has_sidetable_rc : 1;

        uintptr_t extra_rc : 19;

SideTables包括了多个SideTable,在不同系统架构中SideTable的个数是不同的;SideTables是哈希表,可以通过一个对象的指针来找到具体的引用计数表或弱引用表在哪一个具体的SideTable中。

 为什么用多个SideTable? 如果只有一个table,意味着内存中分配的所有对象都要在一个表中操作,因为多个线程可能同时操作这个表,所以就要对这个表加锁,如果并发操作这个表的线程有成千上万个,就会产生效率问题。所以系统引入了分离锁这样一个技术方案,把大表拆成多个小表来进行操作,分别对小表加锁,从而提升效率。

 自旋锁:
                      自旋锁:是“忙等”的锁。由于自旋时不释放CPU,因而持有自旋锁的线程应该尽快释放自旋锁,否则其他等待该自旋锁的线程会一直自旋,从而浪费CPU时间。

                        自旋锁适用于那些仅需要阻塞很短时间的场景。

dealloc -> weak_clear_no_lock(从弱引用表中取出所有该对象的weak_entry_t对象,weak_entry_t里存放了weak_referrer_t数组,就是所有的弱引用,全部设为nil,然后再移除weak_entry_t对象)

Autoreleasepool的实现原理:

以栈为结点,由双向链表的形式合成的数据结构。
与线程一一对应。

Main函数自动添加了@autoreleasepool{}; 很多次循环的内部最好自己添加@autoreleasepool{},以及时释放其内部的临时对象。
 在当次runloop将要结束的时候调用AutoreleasePoolPage::pop()。
autoreleasePoolPage 数据结构
可在源码中查看
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;

                        多层嵌套就是多次插入哨兵对象。

                        在for循环中alloc图片数据等内存消耗较大的场景手动插入autoreleasePool。
AutoreleasePool 为何可以嵌套使用

多层嵌套就是多次插入哨兵对象
,每次创建一个AutoreleasePool,@AutoreleasePool,其实系统就是为我们创建了一个哨兵对象,其实就是创建page,若果当前page没有满,其实就是创建一个哨兵,所以可以嵌套使用
中间用nil作为分割

子线程默认不会开启 Runloop,那出现 Autorelease 对象如何处理?不手动处理会内存泄漏吗?
在子线程你创建了 Pool 的话,产生的 Autorelease 对象就会交给 pool 去管理。如果你没有创建 Pool ,但是产生了 Autorelease 对象,就会调用 autoreleaseNoPage 方法。在这个方法中,会自动帮你创建一个 hotpage(hotPage 可以理解为当前正在使用的 AutoreleasePoolPage,如果你还是不理解,可以先看看 Autoreleasepool 的源代码,再来看这个问题 ),并调用 page->add(obj)将对象添加到 AutoreleasePoolPage 的栈中,也就是说你不进行手动的内存管理,也不会内存泄漏啦!

将对象放入自动释放池不会引起引用计数+1

在for循环大量使用imageNamed:之类的方法生成UIImage对象可能是个更要命的事情,内存随时可能因为占用过多被系统杀掉。
这种情况下利用Autoreleasepool可以大幅度降低程序的内存占用。

AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应结构中的parent指针和child指针)
AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)
AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址
上面的id *next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置
一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入

1.根据传入的哨兵对象地址找到哨兵对象所处的page
2.在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置
补充2:从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page

黑魔法之Thread Local Storage

Thread Local Storage(TLS)线程局部存储,目的很简单,将一块内存作为某个线程专有的存储,以key-value的形式进行读写,比如在非arm架构下,使用pthread提供的方法实现:

一 、dealloc 调用流程

1.首先调用 _objc_rootDealloc()

2.接下来调用 rootDealloc() 

3. isTaggedPointer   是否是标记指针 是直接 return ;

  接下来会判断是否可以被直接快速释放,判断的依据主要有 5 个,判断是否有以下五种情况

nonpointer              是否优化过isa指针

 weakly_reference  是否存在弱引用指向

 has_assoc              是否设置过关联对象

 has_cxx_dtor         是否有cpp的析构函数(.cxx_destruct)

 has_sidetable_rc   引用计数器是否过大无法存储在isa中

二、object_dispose() 调用流程。

1.直接调用 objc_destructInstance()。

2.之后调用C的 free() 函数。

3.objc_destructInstance() 调用流程

1>.先判断 hasCxxDtor,是否有析构函数(析构器),要调用 object_cxxDestruct() ,释放(清除成员变量)。

2>.再判断hasAssocitatedObjects,如果有的话,要调用object_remove_associations(), 移除当前对象的关联对象。

3>.然后调用 clearDeallocating()。 

4>.执行完毕。

4.clearDeallocating() 调用流程
0>. 判断isa是否优化过,从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,所以结果一般是优化过了。

判断是否有弱引用或者引用计数

1>.执行 clearDeallocating_slow()。

2>.再执行 weak_clear_no_lock,在这一步骤中,会将指向该对象的弱引用指针置为 nil。

3>.接下来执行 table.refcnts.eraser(),从引用计数表中擦除该对象的引用计数。

4>.至此为止,Dealloc 的执行流程结束。

如何破除循环引用

方式1:--打断引用链条 方式2:--使用__weak
NSTimer破除循环引用
weak指针:
既然是强引用导致循环引用,那么用__weak修饰self就好了,想法是对的,但是做法是无效的。
中间类,block
及时销毁
创建一个继承NSProxy的子类WeakProxy,并实现消息转发的相关方法
多次调用对象的autorelease方法会导致什么问题?
答:多次将地址存到自动释放池中,导致野指针异常
图片加载占用内存对比

使用 imageName: 加载图片:
加载到内存当中后,占据内存空间较大
相同的图片,图片不会重复加载
加载内存当中之后,会一直停留在内存当中,不会随着对象销毁而销毁
加载进去图片之后,占用的内存归系统管理,我们无法管理
使用 imageWithContentsOfFile: 加载图片
加载到内存当中后,占据内存空间较小
相同的图片会被重复加载内存当中
对象销毁的时候,加载到内存中图片会随着一起销毁
结论:
图片较小,并且使用频繁,使用 imageName: 来加载(按钮图标/主页里面图片)
图片较大,并且使用较少,使用 imageWithContentsOfFile: 来加载(版本新特性/相册)