引用计数如何存储?
1. 对象如果支持使用 TaggedPointer,苹果会直接将其指针值作为引用计数返回;
2. 如果当前设备是 64 位环境并且使用 Objective-C 2.0,那么“一些”对象会使用其 isa 指针的一部分空间来存储它的引用计数;
3. 否则Runtime 会使用一张散列表来管理引用计数。
isa 指针(NONPOINTER_ISA)
用 64 bit 存储一个内存地址显然是种浪费,毕竟很少有那么大内存的设备。于是可以优化存储方案,用一部分额外空间存储其他内容。
isa 指针第一位为 1 即表示使用优化的 isa 指针。目前只有 arm64 架构的设备支持。
SUPPORT_NONPOINTER_ISA 用于标记是否支持优化的 isa 指针,其字面含义意思是 isa 的内容不再是类的指针了,而是包含了更多信息。比如引用计数,析构状态,被其他 weak 变量引用情况。
在 64 位环境下,优化的 isa 指针并不是就一定会存储引用计数,毕竟用 19bit (iOS 系统)保存引用计数不一定够。需要注意的是这 19 位保存的是引用计数的值减一。
ISA 指针中如果 has_sidetable_rc 的值如果为 1:那么引用计数会存储在一个叫 SideTable 的类的属性中
sidetable_retainCount() 方法的逻辑就是先从 SideTable 的静态方法获取当前实例对应的 SideTable 对象,其 refcnts 属性就是之前说的存储引用计数的散列表,在引用计数表中用迭代器查找当前实例对应的键值对,获取引用计数值,并在此基础上 +1 并将结果返回。这也就是为什么之前说引用计数表存储的值为实际引用计数减一。
第一个 bit 表示该对象是否有过 weak 对象,如果没有,在析构释放内存时可以更快;
第二个 bit 表示该对象是否正在析构。
从第三个 bit 开始才是存储引用计数数值的地方。所以这里要做向右移两位的操作。
不能够完全信任这个 _objc_rootRetainCount(id obj) 函数,对于已释放的对象以及不正确的对象地址,有时也返回 “1”。它所返回的引用计数只是某个给定时间点上的值,该方法并未考虑到系统稍后会把自动释放吃池清空,因而不会将后续的释放操作从返回值里减去。clang 会尽可能把 NSString 实现成单例对象,其引用计数会很大。
获取某个对象的引用计数
1. MRC 环境:使用 retainCount 方法,其会调用 objc_object 的 rootRetainCount() 方法:
2. ARC : 使用 Core Foundation 库的 CFGetRetainCount() 方法, 也 可以使用 Runtime 的 _objc_rootRetainCount(id obj) 方法来获取引用计数,这个函数也是调用 objc_object 的 rootRetainCount() 方法。
问题一:临时变量什么时候释放?
1. 正常情况下,超出作用域立即释放
2. 加入自动释放池,会延迟释放,在Runloop休眠或者autoreleasepool 作用域之后释放
AutoReleasePool 是OC 的内存自动回收机制,将加入到AutoReleasePool中的变量release时机延迟。将对象加入到AutoReleasePool中,这个对象即使超出作用域也不会立即释放,直到runloop休眠或者超出AutoReleasePool作用域才会释放
AutoreleasePool原理
自动释放池本质是一个AutoreleasePoolPage结构体对象,栈结构存储,每一个AutoreleasePoolPage以双向链表形式连接
自动释放的压栈和出栈本质上是调用AutoreleasePoolPage的push和pop方法
自动释放池 -- 内存结构
1. 只有第一页有哨兵对象,一页大小等于505*8=4040
2. 当一页压栈满了会开辟新页,第一页最多存储504个对象,其余最多存储505个
自动释放池 -- push压栈
1. 获取当前操作页,判断hotPage是否存在
2. 存在且未满,add方法压栈,在页中通过next指针递增压栈对象
3. 存在且满了,autoreleaseFullPage进入下一页,创建新页并设置当前页的child对象为新页
4. 不存在,autoreleaseNoPage 创建新页,压栈哨兵对象
pop 出栈
1. 执行pop出栈时,会传入push操作的返回值,即POOL_BOUNDARY的内存地址token,根据token找到哨兵对象所在,并释放之前的对象,next指针--
首先根据传入的边界对象地址找到边界对象所处的page;然后选择当前page 中最新加入的对象一直向前清理,可以向前跨越若干个page,直到边界所在的位置;清理的方式是向这些对象发送一次release消息,使其引用计数减一;
清空page对象会遵循一些原则:
如果当前的page中存放的对象少于一半,则子page全部删除;
如果当前当前的page存放的多余一半(意味着马上将要满),则保留一个子page,节省创建新page的开销;
2. 通过next指针递减出栈普通对象
3. 当前页空了,设置父节点页为hot页,销毁当前页(kill 操作),将父节点页赋值为当前页(hotpage),并将父节点页的child对象指针置为nil
注意: objc_autoreleasePoolPop 方法中传入一个对象---(push压栈后返回的哨兵对象),即ctxt,防止出栈混乱。
参考:
oc --引用计数原理
iOS内存管理-深入解析自动释放池 - 云+社区 - 腾讯云 自动释放池讲的很好
iOS 内存管理(二)AutoReleasePool -