iOS之深入解析内存管理的引用计数retainCount的底层原理

Posted Forever_wj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS之深入解析内存管理的引用计数retainCount的底层原理相关的知识,希望对你有一定的参考价值。

一、简介

① 引用计数概念
  • OC 在创建对象时,不会直接返回该对象,而是返回一个指向对象的指针。
  • OC 在内存管理上采用了引用计数,它是一个简单而有效管理对象生命周期的方式。
  • 在对象内部保存一个用来表示被引用计数的数字,init、new 和 copy 都会让引用计数 +1,调用 release 让引用计数 -1。当计数等于 0 的时候,系统调用 dealloc 方法来销毁对象。
  • 引用计数分为自动引用计数(ARC : Automatic Reference Counting)和手动引用计数(MRC : Manual Reference Counting)。
② 引用计数原理

在这里插入图片描述

③ 引用计数计算
  • 指针赋值时,retainCount 不会自动增加,如下所示:
	A *a = [[A alloc] init]; // retain count = 1
	A *b = a;                // 指针赋值时,retainCount 不会自动增加
	[b retain];              // retain count = 2
  • 创建对象并引用,其引用计数为 1,如下:
	OBJC_EXTERN int _objc_rootRetainCount(id);
    
    NSObject *obj = [[NSObject alloc] init];
    // 创建对象并引用,引用计数为 1
    NSLog(@"obj retainCount:%lu", (unsigned long)_objc_rootRetainCount(obj));
    
    NSObject *obj1 = [[NSObject alloc] init];
    // 创建对象并引用,引用计数为 1
    NSLog(@"obj1 retainCount:%lu", (unsigned long)_objc_rootRetainCount(obj1));
  • 对象重新指向,引用计数变化:
	// obj指向了obj1所指的对象B,失去了对原来对象A的引用,所以对象A的引用计数-1,变为0,即A被销毁
    // 对于B,obj引用了它,所以引用计数+1,变为2
    obj = obj1;
    // self.obj又引用了A,所以引用计数+1,变为3
    self.obj = obj;
    NSLog(@"strong obj1 retainCount:%lu",(unsigned long)_objc_rootRetainCount(obj1));
    NSLog(@"strong obj retainCount:%lu",(unsigned long)_objc_rootRetainCount(obj));
  • 引用计数的计算分析:
    • 现有如下代码:
	NSObject *obj1 = [NSObject new];
    NSLog(@"引用计数: %lu", (unsigned long)[obj1 retainCount]);
    NSObject *obj2 = [obj1 retain];
    NSObject *obj3 = [obj1 retain];
    NSLog(@"引用计数: %lu", (unsigned long)[obj1 retainCount]);
    [obj1 release];
    NSLog(@"引用计数: %lu %@", (unsigned long)[obj1 retainCount], obj1);
    [obj1 release];
    NSLog(@"引用计数: %lu %@", (unsigned long)[obj1 retainCount], obj1);
    [obj1 release];
    NSLog(@"引用计数: %lu %@", (unsigned long)[obj1 retainCount], obj1);
    • 运行程序,执行结果如下:
	引用计数:1
	引用计数:3
	引用计数:2 <NSObject:0x60400001ecd0>
	引用计数:1 <NSObject:0x60400001ecd0>
	*** -[NSObject retainCount]: message sent to deallocated instance 0x60400001ecd0
    • 根据 Debug 输出可以看到:obj1 可以调用多次 release 方法。
    • 从两次打印 obj1 的地址相同可以猜测,在 [obj1 release] 执行之后对象的引用计数 -1,不再强引用对象,但 obj1 仍然指向对象所在的那片内存空间,在第三次执行 release 后,对象的引用计数为 0,对象所在的内存空间被销毁,但是 obj1 指针仍然存在,此时调用 retainCount 会报野指针错误,可以通过置 obj1 = nil 解决这个问题。
    • 对 Linux 文件系统比较了解的可能发现,引用计数的这种管理方式类似于文件系统里面的硬链接。在 Linux 文件系统中,我们用 ln 命令可以创建一个硬链接(相当于 retain),当删除一个文件时(相当于 release),系统调用会检查文件的 link count 值,如果大于 1,则不会回收文件所占用的磁盘区域,直到最后一次删除前,系统发现 link count 值为 1,则系统才会执行直正的删除操作,把文件所占用的磁盘区域标记成未用。
④ 为什么需要引用计数?
  • 引用计数真正派上用场的场景是在面向对象的程序设计架构中,用于对象之间传递和共享数据。
  • 如下所示,对象 A 生成了一个对象 O,需要调用对象 B 的某个方法,并将对象 O 作为参数传递过去:
	[objB doSomething:o];
  • 在没有引用计数的情况下,一般内存管理的原则是“谁申请谁释放”。那么对象 A 就需要在对象 B 不再需要 O 的时候,将 O 销毁,但对象 B 可能临时用一下 O,也可能将它设置为自己的一个成员变量,在这种情况下,什么时候销毁就成了一个难题了。

在这里插入图片描述

  • 对于以上情况有两种做法:
    • 对象 A 在调用完对象 B 的某个方法之后,马上销毁参数 O;然后对象 B 需要将对象 O 复制一份,生成另一个对象 O2,同时自己来管理对象 O2 的生命周期。这种做法带来更多的内存申请、复制、释放的工作,本来可以复用的对象,因为不方便管理它的生命周期,就简单地把它销毁,又重新构造一份一样的,实在太影响性能。
    • 对象 A 只负责生成 O,之后就由对象 B 负责完成 O 的销毁工作。如果对象 B 只是临时用一下 O,就可以用完后马上销毁;如果对象 B 需要长时间使用 O,就不销毁它。这种做法看似解决了对象复制的问题,但是它强烈依赖于 A 和 B 两个对象的配合,代码维护者需要明确地记住这种编程约定。并且,由于 O 的生成和释放在不同对象中,使得它的内存管理代码分散在不同对象中,管理起来也很费劲,如果这个时候情况更加复杂一些,例如对象 B 需要再向对象 C 传递参数 O,那么这个对象在对象 C 中又不能让对象 C 管理,所以这种方法带来的复杂度更高,更加不可取。

在这里插入图片描述

  • 引用计数的出现很好地解决这个问题,在参数 O 的传递过程中,哪些对象需要长时间使用它,就把它的引用计数 +1,使用完就-1,所有对象遵守这个规则,对象的生命周期管理就可以完全交给引用计数了。

二、retainCount 底层分析

  • 进入 retainCount -> _objc_rootRetainCount -> rootRetainCount 源码,其实现如下:
	- (NSUInteger)retainCount {
    	return _objc_rootRetainCount(self);
	}
	
	uintptr_t_objc_rootRetainCount(id obj){
	    ASSERT(obj);
	    return obj->rootRetainCount();
	}
  • 继续 rootRetainCount 源码实现,如下:
	inline uintptr_t 
	objc_object::rootRetainCount()
	{
	    if (isTaggedPointer()) return (uintptr_t)this;
	
	    sidetable_lock();
	    isa_t bits = LoadExclusive(&isa.bits);
	    ClearExclusive(&isa.bits);
	    // 如果是nonpointer isa,才有引用计数的下层处理
	    // isa经过优化
	    if (bits.nonpointer) {
	        // alloc创建的对象引用计数为0,包括sideTable,所以对于alloc来说,是 0+1=1,这也是为什么通过retaincount获取的引用计数为1的原因
	        uintptr_t rc = 1 + bits.extra_rc;
	        // 获取散列表中的引用计数
	        if (bits.has_sidetable_rc) {
	            rc += sidetable_getExtraRC_nolock();
	        }
	        sidetable_unlock();
	        return rc;
	    }
	    // 如果不是,则正常返回
	    sidetable_unlock();
		// isa未经过优化,直接获取散列表中的引用计数
	    return sidetable_retainCount();
	}
  • 当对象的 isa 经过优化,首先获取 isa 位域 extra_rc 中的引用计数,默认会 +1,uintptr_t rc = 1 + bits.extra_rc;然后获取散列表的引用计数表中的引用计数,两者相加得到对象的最终的引用计数,rc += sidetable_getExtraRC_nolock();
  • 当对象的 isa 没有经过优化,直接获取散列表的引用计数表中的引用计数返回;
  • 当 alloc 一个对象时,然后调用 retainCount 函数,得到对象的引用计数为 1,是因为在底层 rootRetainCount 方法中,引用计数默认 +1 了,这里只有对引用计数的读取操作,是没有写入操作的,简单来说就是:为了防止 alloc 创建的对象被释放(引用计数为0会被释放),所以在编译阶段,程序底层默认进行了 +1 操作;
  • 实际上在 extra_rc 中的引用计数仍然为0。
  • 综上所述,retainCount 的底层流程如下:
    在这里插入图片描述

三、ARC 下的内存管理问题

① 产生原因
  • 过度使用 block 之后,无法解决循环引用问题。
  • 遇到底层 Core Foundation 对象,需要手工管理它们的引用计数时,处理麻烦。
② 循环引用
  • 引用计数这种管理内存的方式虽然很简单,但是有一个比较大的瑕疵,即它不能很好的解决循环引用问题。
  • 如下图所示:对象 A 和对象 B,相互引用了对方作为自己的成员变量,只有当自己销毁时,才会将成员变量的引用计数减 1。因为对象 A 的销毁依赖于对象 B 销毁,而对象 B 的销毁又依赖于对象 A 的销毁,这样就造成了循环引用 Reference Cycle 的问题,这两个对象即使在外界已经没有任何指针能够访问到它们了,它们也无法被释放。

在这里插入图片描述

  • 不止两对象存在循环引用问题,多个对象依次持有对方,形式一个环状,也可以造成循环引用问题,而且在真实编程环境中,环越大就越难被发现。比如,下图是 4 个对象形成的循环引用问题:

在这里插入图片描述

③ 主动断开循环引用
  • 解决循环引用问题主要有两个办法。第一个办法:明确知道这里会存在循环引用,在合理的位置主动断开环中的一个引用,使得对象得以回收。如下图所示:

在这里插入图片描述

  • 主动断开循环引用这种方式常见于各种与 block 相关的代码逻辑中。
  • 主动断开循环引用这种操作依赖于程序员自己手工显式地控制,相当于回到了以前 “谁申请谁释放” 的内存管理年代,它依赖于程序员自己有能力发现循环引用并且知道在什么时机断开循环引用回收内存,所以这种解决方法并不常用,更常见的办法是使用弱引用的办法。
④ 使用弱引用
  • 弱引用虽然持有对象,但是并不增加引用计数,这样就避免了循环引用的产生。
  • ios 开发中,弱引用通常在 delegate 模式中使用。如下所示:

在这里插入图片描述

⑤ 弱引用的实现原理
  • 弱引用的实现原理是这样的,系统对于每一个有弱引用的对象,都维护一个表来记录它所有的弱引用的指针地址。这样,当一个对象的引用计数为 0 时,系统就通过这张表,找到所有的弱引用指针,继而把它们都置成 nil。
  • 从这个原理中,可以看出,弱引用的使用是有额外的开销的。虽然这个开销很小,但是如果一个地方我们肯定它不需要弱引用的特性,就不应该盲目使用弱引用。
  • 举个例子,有人喜欢在手写界面的时候,将所有界面元素都设置成 weak 的,这某种程度上与Xcode 通过 Storyboard 拖拽生成的新变量是一致的。但是我个人认为这样做并不太合适:
    • 在创建这个对象时,需要注意临时使用一个强引用持有它,否则因为 weak 变量并不持有对象,就会造成一个对象刚被创建就销毁掉。
    • 大部分 ViewController 的视图对象的生命周期与 ViewController 本身是一致的,没有必要额外做这个事情。
    • 早先苹果这么设计,是有历史原因的。在早年,当时系统收到 Memory Warning 的时候,ViewController 的 View 会被 unLoad 掉,这个时候,使用 weak 的视图变量是有用的,可以保持这些内存被回收。但是这个设计已经被废弃了,替代方案是将相关视图的 CALayer 对应的 CABackingStore 类型的内存区会被标记成 volatile 类型。详见 iOS 唐巧大神的博客:再见,viewDidUnload方法
⑥ 检测循环引用
  • 可以使用 Xcode 的 leaks 工具检测循环引用的出现。

以上是关于iOS之深入解析内存管理的引用计数retainCount的底层原理的主要内容,如果未能解决你的问题,请参考以下文章

iOS之深入解析内存管理Tagged Pointer的底层原理

iOS之深入解析内存管理retain与release的底层原理

iOS之深入解析如何检测“循环引用”

iOS之深入解析内存管理散列表SideTables和弱引用表weak_table的底层原理

Swift之深入解析内存管理的底层原理

iOS之深入解析内存管理NSTimer的强引用问题