全方位剖析iOS高级技术问题(四)之内存管理相关问题

本文主要内容

一.内存布局
二.内存管理方案
三.数据结构
四.MRC&ARC
五.引用计数管理
六.弱引用管理
七.自动释放池
八.循环引用

全方位剖析iOS高级技术问题(四)之内存管理相关问题

一.内存布局

  • stack: 栈区,方法调用
  • heap: 堆区,通过alloc等分配的对象
  • bss: 未初始化的全局变量等
  • data: 已初始化的全局变量等
  • text: 程序代码

全方位剖析iOS高级技术问题(四)之内存管理相关问题

二.内存管理方案

1、内存管理方案

  • TaggedPointer: NSNumber等小对象
  • NONPOINTER_ISA: 64位架构下应用程序,非指针型ISA
  • 散列表: 复杂的数据结构,其中包含引用计数表、弱引用表

关于内存管理的讨论全部基于objc-runtime-680版本讲解

2、NONPOINTER_ISA管理方案

arm64架构

  • indexed: [0]1位,0代表纯isa,只存储类对象地址,1代表isa,类地址和内存管理相关数据
  • has_assoc: [1]1位, 当前对象是否有关联对象,0代表没有,1代表有
  • has_cxx_dtor: [2]1位,当前对象是否使用C++相关内容
  • shiftcls: [3,35]33位,当前对象类对象的指针地址
  • magic: [36,41]6位
  • weakly_referenced: [42]1位,标识对象是否有弱引用指针
  • deallocating: [43]1位,是否在进行dealloc操作
  • has_sidetable_rc: [44]1位,当前isa指针所存储的引用计数达到上限,需要外挂数据结构存储引用计数内容(散列表)
  • extra_rc: [45,63]19位,额外的引用计数

全方位剖析iOS高级技术问题(四)之内存管理相关问题

全方位剖析iOS高级技术问题(四)之内存管理相关问题

3、散列表方式

  • SideTables()结构-Hash结构

全方位剖析iOS高级技术问题(四)之内存管理相关问题

  • SideTable结构

全方位剖析iOS高级技术问题(四)之内存管理相关问题

问题1:为什么不是一个SideTable?

  • 假如只有一张SideTable表,相当于在内存中分配的所有对象的引用计数或弱引用存储都放在一张表中,如果要操作某个对象的引用计数值,进行加1或减1的修改,由于所有对象可能是在不同的线程中分配创建,包括调用release/retain方法也可能在不同线程操作,此时对SideTable表的操作需要加锁处理才能保证线程安全,就会存在效率问题。 全方位剖析iOS高级技术问题(四)之内存管理相关问题

处理方案:

  • 分离锁,解决效率问题。把内存对象所对应的引用计数表分拆成8个。

全方位剖析iOS高级技术问题(四)之内存管理相关问题

问题2:如何实现快速分流?
也就是说通过一个对象的指针如何快速定位到它属于哪张SideTable表。

  • SideTables的本质是一张Hash表
  • 一个对象的指针作为Key通过Hash函数运算,计算出一个value值来决定出对象对应的SideTable是哪张或在数组哪个位置、索引是什么 全方位剖析iOS高级技术问题(四)之内存管理相关问题
  • Hash查找
    给定值是对象内存地址,目标值是数组下标索引。 把对象内存地址作为函数参数,经过Hash函数的运算得出数组下标索引值。实际是通过对象内存地址和SideTables数组个数取余计算,计算出在哪张表中。Hash查找是为了提高查找效率,存储是通过Hash函数存储,查找也通过Hash查找,不需要遍历。

全方位剖析iOS高级技术问题(四)之内存管理相关问题

三.数据结构(散列表方式实现的内存管理方案)

  • Spinlock_t:自旋锁
  • RefcountMap: 引用计数表
  • weak_table_t:弱引用表

1、Spinlock_t自旋锁

  • Spinlock_t是“忙等”的锁
  • 如果当前锁已被其他线程获取,当前线程就会不断的探测这个锁是否被释放,如果释放第一时间获取当前锁。
  • 正常的信号链,当获取不到锁时,会将所处线程阻塞休眠,等到其他线程释放该锁时唤醒当前线程。
  • 适用于轻量访问

2、RefcountMap引用计数表

  • Hash表
  • 使用Hash查找,用于提高查找效率
  • 为什么能提高查找效率?因为在存储对象的引用计数时就是通过Hash函数计算存储位置的,而获取对象的引用计数值时也是通过这个Hash函数来获取索引位置,避免循环遍历的操作全方位剖析iOS高级技术问题(四)之内存管理相关问题
  • size_t,64位
    weakly_reference:第1位,是否有弱引用;
    deallocating: 第2位,是否在进行dealloc操作; RC:引用计数值 全方位剖析iOS高级技术问题(四)之内存管理相关问题

3、弱引用表weak_table_t

  • Hash表

全方位剖析iOS高级技术问题(四)之内存管理相关问题

四.MRC & ARC

1、什么是MRC?

  • 通过手动引用计数来对对象进行内存管理
  • alloc:分配内存空间
    retain:对象引用计数+1
    release:对象引用计数-1
    retainCount:获取对象引用计数值
    autorelease:调用autoreleasePool结束时对象引用计数-1
    dealloc:释放父类成员变量

2、什么是ARC?

  • 通过自动引用计数来管理内存
  • 是LLVM编译器和Runtime协作的结果
  • 禁止手动调用retain/release/retainCount/dealloc
  • 新增weakstrong属性关键字

问题1:MRC和ARC的区别

  • MRC是手动管理内存,ARC是由编译器和Runtime协作自动引用计数的内存管理;
  • MRC中可以调用引用计数相关的方法,ARC中无法调用。

五.引用计数管理

实现原理分析

  • alloc
  • retain
  • release
  • retainCount
  • dealloc

1、alloc实现

经过一系列调用,最终调用了C函数calloc。此时并没有设置引用计数为1。

2、retain实现

  • 经过2次哈希查找。
  • 通过当前对象的指针到SideTables()去获取它所属的SideTable(第1次Hash查找),再在SideTable中获取引用计数值(第2次Hash查找),再对这个引用计数值进行加1操作,由于存储引用计数的Size_t64个bit位中从第三位开始才存储引用计数,前两位没有存储引用计数,所以此时加的是偏移量(4)。 全方位剖析iOS高级技术问题(四)之内存管理相关问题

3、release实现

  • 经过2次哈希查找。
  • 通过当前对象通过Hash算法在SideTables()中找到它所属的SideTable,然后根据当前对象指针访问table的引用计数表查找引用计数值,然后对引用计数值进行减1操作,与retain实现同理,此时减的也是一个偏移量。

全方位剖析iOS高级技术问题(四)之内存管理相关问题

4、retainCount实现

  • 通过当前对象通过Hash算法在SideTables()中找到它所属的SideTable,声明size_t变量为1,根据当前对象指针访问table的引用计数表查找引用计数值,把查找的结果向右偏移操作。如果刚alloc的对象,在引用计数表中没有该对象相关联的key/value映射,此时it->second为0,由于局部变量refcnt_result为1,所以通过alloc调用产生的对象调用retainCount值为1

全方位剖析iOS高级技术问题(四)之内存管理相关问题

5、dealloc实现

  • 首先会调用_obic_rootDealloc()函数,接着调用rootDealloc()函数
  • rootDealloc()函数内部会进行是否可以释放的判断? 判断当前对象是否使用了非指针型的isa; 判断当前对象是否有弱引用 判断当前对象是否有关联对象 判断当前对象内部实现是否有C++内容以及是否以ARC管理内存 判断当前对象的引用计数是否用sideTable引用计数表来维护 ()
  • 如果以上条件都不满足,即没有非指针型isa、没有弱引用、没有关联对象、内部没有C++内容并且不是以ARC管理内存、没有sideTable表维护引用计数,则调用C函数free()
  • 否则调用object_dispose()

全方位剖析iOS高级技术问题(四)之内存管理相关问题

5.1 object dispose()实现

  • 调用_objc_destructinstance()函数,销毁实例
  • 再调用C函数free() 全方位剖析iOS高级技术问题(四)之内存管理相关问题

5.2 obj destructinstance()实现

  • 先判断当前对象内部是否有C++相关内容或是否采用ARC;
  • 如果有,调用object _cxxDestruct()函数;
  • 如果没有,会判断当前对象是否有关联对象,如果当前对象有关联对象,会调用_obiect_remove_assocations()来移除对象相关的关联对象;
  • 如果当前对象没有关联对象,会调用clearDeallocating()全方位剖析iOS高级技术问题(四)之内存管理相关问题

问题:通过关联对象技术为类添加了实例变量,在对象的dealloc方法中是否有必要对关联对象进行移除操作?

  • 在系统的dealloc内部实现中,会自动判断是否有关联对象,有的话,系统内部就会把相关的关联对象移除。

5.3 clearDeallocating()实现

  • 首先调用sidetable clearDeallocating()函数;
  • 然后调用weak_clear_no_lock()函数,将指向该对象的弱引用指针置为nil;
  • 再调用table.refcnts.erase()函数,从引用计数表中擦除该对象引用计数。

全方位剖析iOS高级技术问题(四)之内存管理相关问题

问题:如果一个对象有weak指针指向它,当该对象dealloc或废弃后,它的weak指针为何被置为nil?

  • dealloc内部实现中做客将指向该对象的弱引用指针置为nil的操作

六.弱引用管理

问题:weak变量怎样被添加到弱引用表中的

  • 一个被声明为__weak的对象指针,经过编译器编译会调用相应的objc_initWeak()函数,经过一系列的函数调用栈,最终在weak_register_no_lock()函数当中做弱引用变量的添加,添加位置由Hash算法查找,如果查找的位置已经有了当前对象对应的弱引用数组,就把新的弱引用变量添加到数组中,如果没有,重新创建一个弱引用数组,第0个位置添加上最新的weak指针,其他位置都初始化为0或nil。

全方位剖析iOS高级技术问题(四)之内存管理相关问题

问题:当一个对象被释放或废弃后,weak变量如何处理

  • 清除weak变量,同时设置指向为nil
  • 为何会被自动置为nil?当一个对象被dealloc()之后,在dealloc()内部实现中,会调用弱引用清除的相关函数,在这个函数内部会根据当前对象指针查找弱引用表,把当前对象相对应的弱引用(数组)都拿出来,遍历数组中的弱引用指针分被置为nil。 全方位剖析iOS高级技术问题(四)之内存管理相关问题

七.自动释放池

  • 编译器会将@autoreleasepool{}改写为:
void *ctx = objc_autoreleasePoolPush();
{ objc_autoreleasePoolPop(ctx)}
  • objc_autoreleasePoolPush()函数
void *objc_autoreleasePoolPush(void)
// 内部调用如                
void *AutoreleasePoolPage::push(void)
  • objc_autoreleasePoolPop(ctx)函数 一次pop实际上相当于一次批量的pop操作
void objc_autoreleasePoolPop(void *ctx)
//内部调用
AutoreleasePoolPage::pop(void *ctx)

1、回顾知识点

双向链表

首先头节点的ParentPtr(父指针)指向空NULL,后续的各个节点ParentPtr指向前一个节点,ChildPtr指向它的后一个节点,最后一个节点的ChildPt r指向空NULL。

双向链表的结构: 全方位剖析iOS高级技术问题(四)之内存管理相关问题

栈是向下增长的,下面是高地址,上面是低地址。对栈的操作有出栈、入栈,特点是后入先出

栈的结构: 全方位剖析iOS高级技术问题(四)之内存管理相关问题

2、AutoreleasePoolPage(C++类)

  • id* next:指向栈中下一个可填充的位置
  • AutoreleasePoolPage* const parent; :父指针
  • AutoreleasePoolPage* child; :子指针
  • pthread_t const thread; :线程

AutoreleasePoolPage的结构:
向下增长。最下面为AutoreleasePoolPage自身占用内存,向上可以存储{}中填充的的Auto release对象。

全方位剖析iOS高级技术问题(四)之内存管理相关问题

AutoreleasePoolPage::push的内部实现
发生push操作后,会把当前next位置置为nil,称为哨兵对象,将next指针指向下一个可入栈的位置。实际上每次进行AutoreleasePool代码块的创建,相当于不断向栈中插入哨兵对象

全方位剖析iOS高级技术问题(四)之内存管理相关问题

3、[obj autorelease]实现过程

当一个对象调用autorelease,首先要判断当前next是否指向栈顶,如果没有指向栈顶,就将此对象直接添加到当前栈的next指向的位置。如果指向栈顶,需要增加一个栈节点到链表上,然后在新的栈上增加对象。

全方位剖析iOS高级技术问题(四)之内存管理相关问题

全方位剖析iOS高级技术问题(四)之内存管理相关问题

AutoreleasePoolPage::pop的内部实现

  • 根据传入的哨兵对象找到对应位置
  • 给上次push操作之后添加的对象依次发送release消息

全方位剖析iOS高级技术问题(四)之内存管理相关问题

全方位剖析iOS高级技术问题(四)之内存管理相关问题

  • 问题1:array的内存是在什么时候释放的?
- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSMutableArray *array = [NSMutableArray array];
    NSLog(@"%@",array);
}
1:在当次runloop将要结束的时候调用`AutoreleasePoolPage::pop()
  • 问题2:AutoreleasePool为何可以嵌套使用?
4:多层嵌套就是多次插入哨兵对象。每次进行`AutoreleasePool`代码块的创建,相当于不断向栈中插入`哨兵对象`
  • 问题3:什么是自动释放池/自动释放池实现结构?
    是以`栈`为结点通过双向链表的形式组合而成;
    是和`线程`一一对应的。
    
  • 问题3:什么情况下需要手动创建AutoreleasePool
    for循环中alloc图片数据等内存消耗较大的场景需要手动插入`AutoreleasePool`
    
  • 问题4:AutoreleasePool的实现原理是怎样的?
`AutoreleasePool`的实现原理就是以栈为节点、通过双向链表形式组合而成的数据结构。

八.循环引用

三种循环引用

  • 自循环引用
  • 相互循环引用
  • 多循环引用

1、循环引用类型

自循环引用

假如有一个对象,对象中有一个成员变量obj,对象强持有成员变量obj,如果将成员变量赋值为原对象,则会造成自循环引用。

全方位剖析iOS高级技术问题(四)之内存管理相关问题

相互循环引用

有一个对象A,其中有一个id类型成员变量obj,有另一个对象B,其中有一个id类型成员变量obj,如果对象A中的obj指向对象B,同时对象B中的obj指向对象A,则会造成相互循环引用

全方位剖析iOS高级技术问题(四)之内存管理相关问题

多循环引用

存在多个对象,每一个对象的obj都指向下一个对象,则会造成多循环引用。

全方位剖析iOS高级技术问题(四)之内存管理相关问题

2.循环引用的重点(考点)

  • 代理
  • Block
  • NSTimer
  • 大环引用
  • 问题1:如何破除循环引用?
    (1)避免产生循环引用
    (2)在合适的时机手动断环
    
  • 问题2:解决循环引用的方案有哪些?

    (1)__weak

    全方位剖析iOS高级技术问题(四)之内存管理相关问题

    (2)__block

    • MRC下,__block修饰对象不会增加其引用计数,避免了循环引用
    • ARC下,__block修饰对象会被强引用,无法避免循环引用,需要手动解环

    (3)__unsafe_unretained

    • 修饰对象不会增加其引用计数,避免了循环引用
    • 如果被修饰对象在某一时机被释放,会产生悬垂指针!(因此不建议使用)

3.循环引用示例

NSTimer循环引用问题

全方位剖析iOS高级技术问题(四)之内存管理相关问题

问题:在日常开发中是否遇到过循环引用的问题?如何解决循环引用的?

遇到NSTimer的循环引用问题。解决方案是:创建一个中间对象,令中间对象分别持有两个弱引用对象NSTimer、原对象,在NSTimer中直接分派的回调在中间对象实现,在中间对象实现NSTimer回调的方法中,对它所持有的Target进行值得判断,如果值存在,将NSTimer的回调给原对象,如果当前对象已经被释放,设置NSTimer为无效状态,就解除了当前线程Runloop对NSTimer的强引用、以及NSTimer对当前对象的强引用。

本文总结

问题1:什么是ARC?

ARC是由LLVM编译器和Runtime共同协作为实现自动引用计数管理。

问题2:为什么weak指针指向的对象在废弃之后会被自动置为nil?

当对象被废弃之后,dealloc方法的内部实现中会调用对弱引用清除的方法,在清除弱引用方法中,会通过哈希算法查找被废弃对象在弱引用表当中的位置来提取它所对应的弱引用指针的列表数组,然后进行for循环遍历将所有weak指针置为nil。

问题3:苹果是如何实现AutoreleasePool的?

AutoreleasePool是以为节点、通过双向链表形式来合成的数据结构。

问题4:什么是循环引用?你遇到过哪些循环引用,是怎么解决的?

八.循环引用