这一部分是写给非iOS工程师的,便于你们了解引用计数、循环引用、弱引用的概念。若是已经了解相关概念能够直接跳过第一部分。html
你们都知道想要占用一块内存很容易,我们 new 一个对象就完事儿了。可是何时回收?不回收天然是不成的,内存再大也不能彻底不回收利用。回收早了的话,真正用到的时候会出现 野指针 问题。回收晚了又浪费宝贵的内存资源。我们得拿出一套管理内存的方法才成。本文只讨论iOS管理对象内存的 引用计数 法。数组
内存中每个对象都有一个属于本身的引用计数器。当某个对象A被另外一个家伙引用时,A的引用计数器就+1,若是再有一个家伙引用到A,那么A的引用计数就再+1。当其中某个家伙再也不引用A了,A的引用计数会-1。直到A的引用计数减到了0,那么就没有人再须要它了,就是时候把它释放掉了。数据结构
在引用计数中,每个对象负责维护对象全部引用的计数值。当一个新的引用指向对象时,引用计数器就递增,当去掉一个引用时,引用计数就递减。当引用计数到零时,该对象就将释放占有的资源。并发
采用上述机制看似就能够知道对象在内存中应该什么时候释放了,可是还有一个 循环引用 的问题须要咱们解决。app
如今内存中有两个对象,A和B。ide
A.x = B; B.y = A;
这样两个对象互相循环引用着对方谁都不会被释放就形成了内存泄露。为了解决这个问题咱们来引入 弱引用 的概念。性能
弱引用指向要引用的对象,可是不会增长那个对象的引用计数。就像下面这个图这样。 虚线为弱引用 (艾玛我画图画的真丑)this
A.x = B; __weak B.y = A;
这里咱们让B的y是一个弱引用,它还能够指向A可是不增长A的引用计数。url
循环引用的问题解决了。咱们不妨思考一下,这套方案还会不会有其它的问题?操作系统
思考中...
还有一个 野指针 的问题等待咱们解决。
所以咱们还须要一个机制,可让A释放以后,我再访问全部指向A的指针( 好比B.y )的时候均可以友好的得知A已经不存在了,从而避免出错。
咱们这里假设用一个数组,把全部指向A的弱引用都存起来,而后当A被释放的时候把数组内全部的若引用都设置成nil( 至关于其余语言中的NULL )。这样当B再访问B.y的时候就会返回nil。经过判空的方式就能够避免野指针错误了。固然提及来简单,下面咱们来看看苹果是如何实现的。
前面絮絮不休说了一大堆,其实真正如今才抛出本次讨论的问题。
我们先来讨论最顶层的 SideTables
为了管理全部对象的引用计数和weak指针,苹果建立了一个全局的SideTables,虽然名字后面有个"s"不过他实际上是一个全局的 Hash 表,里面的内容装的都是 SideTable 结构体而已。它使用对象的 内存地址当它的key 。管理引用计数和weak指针就靠它了。
由于对象引用计数相关操做应该是 原子性 的。否则若是多个线程同时去写一个对象的引用计数,那就会形成数据错乱,失去了内存管理的意义。同时又由于内存中对象的数量是 很是很是庞大 的须要很是频繁的操做SideTables,因此 不 能对整个Hash表加锁。苹果采用了 分离锁 技术。
分离锁和分拆锁的区别
下降锁竞争的另外一种方法是下降线程请求锁的频率。分拆锁 (lock splitting) 和分离锁 (lock striping) 是达到此目的两种方式。相互独立的状态变量,应该使用独立的锁进行保护。有时开发人员会错误地使用一个锁保护全部的状态变量。这些技术减少了锁的粒度,实现了更好的可伸缩性。可是,这些锁须要仔细地分配,以下降发生死锁的危险。
若是一个锁守护多个相互独立的状态变量,你可能可以经过分拆锁,使每个锁守护不一样的变量,从而改进可伸缩性。经过这样的改变,使每个锁被请求的频率都变小了。分拆锁对于中等竞争强度的锁,可以有效地把它们大部分转化为非竞争的锁,使性能和可伸缩性都获得提升。
分拆锁有时候能够被扩展,分红若干加锁块的集合,而且它们归属于相互独立的对象,这样的状况就是分离锁。
由于是使用对象的内存地址当key因此Hash的分部也很平均。假设Hash表有n个元素,则能够将Hash的冲突减小到n分之一,支持n路的并发写操做。
当咱们经过SideTables[key]来获得SideTable的时候,SideTable的结构以下:
自旋锁 比较适用于锁使用者保持锁时间比较短的状况。正是因为自旋锁使用者通常保持锁时间很是短,所以选择自旋而不是睡眠是很是必要的,自旋锁的效率远高于互斥锁。信号量和读写信号量适合于保持时间较长的状况,它们会致使调用者睡眠,所以只能在进程上下文使用,而自旋锁适合于保持时间很是短的状况,它能够在任何上下文使用。
它的做用是在操做引用技术的时候对SideTable加锁,避免数据错误。
苹果在对锁的选择上能够说是精益求精。苹果知道对于引用计数的操做实际上是很是快的。因此选择了虽然不是那么高级可是确实效率高的自旋锁,我在这里只能说"双击666,老铁们! 没毛病!"
对象具体的引用计数数量是记录在这里的。
这里注意RefcountMap实际上是个C++的 Map 。为何Hash之后还须要个Map?其实苹果采用的是分块化的方法。
举个例子
假设如今内存中有16个对象。
0x0000、0x000一、0x00十、0x00十一、0x0100......
我们建立一个SideTables[8]来存放这16个对象,那么查找的时候发生Hash冲突的几率就是八分之一。
假设SideTables[0x0000]和SideTables[0x1111]冲突,映射到相同的结果。
SideTables[0x0000] == SideTables[0x1111] ==> 都指向同一个SideTable
苹果把两个对象的内存管理都放到里同一个SideTable中。你在这个SideTable中须要再次调用 table.refcnts.find(0x0000 )或者 table.refcnts.find(0x1111) 来找到他们真正的引用计数。
这里是一个分流。内存中对象的数量实在是太庞大了咱们经过第一个Hash表只是过滤了第一次,而后咱们还须要再经过这个Map才能精确的定位到咱们要找的对象的引用计数器。
引用计数器的存储结构以下
引用计数器的数据类型是:
typedef __darwin_size_t size_t;
再进一步看它的定义实际上是 unsigned long ,在32位和64位操做系统中,它分别占用32和64个bit。
苹果常用 bit mask 技术。这里也不例外。拿32位系统为例的话,能够理解成有32个盒子排成一排横着放在你面前。盒子里能够装0或者1两个数字。咱们规定最后边的盒子是低位,左边的盒子是高位。
下面来分析引用计数器( 图中右侧 )的结构,从低位到高位。
(1UL<<0) WEAKLY_REFERENCED
表示是否有弱引用指向这个对象,若是有的话(值为1)在对象释放的时候须要把全部指向它的弱引用都变成nil( 至关于其余语言的NULL ),避免野指针错误。
(1UL<<1) DEALLOCATING
表示对象是否正在被释放。1正在释放,0没有。
REAL COUNT
图中REAL COUNT的部分才是对象真正的引用计数存储区。因此我们说的引用计数加一或者减一,其实是对整个unsigned long加四或者减四,由于真正的计数是从2^2位开始的。
(1UL<<(WORD_BITS-1)) SIDE_TABLE_RC_PINNED
其中WORD_BITS在32位和64位系统的时候分别等于32和64。其实这一位没啥具体意义,就是随着对象的引用计数不断变大。若是这一位都变成1了,就表示引用计数已经最大了不能再增长了。
上面的RefcountMap refcnts;是一个一层结构,能够经过key直接找到对应的value。而这里是一个两层结构。
第一层结构体中包含两个元素。
第一个元素 weak_entry_t *weak_entries; 是一个数组,上面的RefcountMap是要经过find(key)来找到精确的元素的。weak_entries则是经过循环遍从来找到对应的entry。
(上面管理引用计数苹果使用的是Map,这里管理weak指针苹果使用的是数组,有兴趣的朋友能够思考一下为何苹果会分别采用这两种不一样的结构)
第二个元素num_entries是用来维护保证数组始终有一个合适的size。好比数组中元素的数量超过3/4的时候将数组的大小乘以2。
这时候其实并不操做SideTable,具体能够参考:
深刻浅出ARC(上)
Objc使用了相似散列表的结构来记录引用计数。而且在初始化的时候设为了一。
//一、经过对象内存地址,在SideTables找到对应的SideTable SideTable& table = SideTables()[this]; //二、经过对象内存地址,在refcnts中取出引用计数 size_t& refcntStorage = table.refcnts[this]; //三、判断PINNED位,不为1则+4 if (! (refcntStorage & PINNED)) { refcntStorage += (1UL<<2); }
table.lock(); 引用计数 = table.refcnts.find(this); if (引用计数 == table.refcnts.end()) { //标记对象为正在释放 table.refcnts[this] = SIDE_TABLE_DEALLOCATING; } else if (引用计数 < SIDE_TABLE_DEALLOCATING) { //这里颇有意思,当出现小余(1UL<<1) 的状况的时候 //就是前面引用计数位都是0,后面弱引用标记位WEAKLY_REFERENCED可能有弱引用1 //或者没弱引用0 //为了避免去影响WEAKLY_REFERENCED的状态 引用计数 |= SIDE_TABLE_DEALLOCATING; } else if ( SIDE_TABLE_RC_PINNED位为0) { 引用计数 -= SIDE_TABLE_RC_ONE; } table.unlock(); 若是作完上述操做后若是须要释放对象,则调用dealloc
dealloc操做也作了大量了逻辑判断和其它处理,我们这里抛开那些逻辑只讨论下面部分 sidetable_clearDeallocating()
SideTable& table = SideTables()[this]; table.lock(); 引用计数 = table.refcnts.find(this); if (引用计数 != table.refcnts.end()) { if (引用计数中SIDE_TABLE_WEAKLY_REFERENCED标志位为1) { weak_clear_no_lock(&table.weak_table, (id)this); } //从refcnts中删除引用计数 table.refcnts.erase(it); } table.unlock();
weak_clear_no_lock() 是关键,它才是在对象被销毁的时候处理全部弱引用指针的方法。
weak_clear_no_lock objc-weak.mm line:461-504 void weak_clear_no_lock(weak_table_t *weak_table, id referent_id) { //一、拿到被销毁对象的指针 objc_object *referent = (objc_object *)referent_id; //二、经过 指针 在weak_table中查找出对应的entry weak_entry_t *entry = weak_entry_for_referent(weak_table, referent); if (entry == nil) { /// XXX shouldn't happen, but does with mismatched CF/objc //printf("XXX no entry for clear deallocating %p\n", referent); return; } //三、将全部的引用设置成nil weak_referrer_t *referrers; size_t count; if (entry->out_of_line()) { //3.一、若是弱引用超过4个则将referrers数组内的弱引用都置成nil。 referrers = entry->referrers; count = TABLE_SIZE(entry); } else { //3.二、不超过4个则将inline_referrers数组内的弱引用都置成nil referrers = entry->inline_referrers; count = WEAK_INLINE_COUNT; } //循环设置全部的引用为nil for (size_t i = 0; i < count; ++i) { objc_object **referrer = referrers[i]; if (referrer) { if (*referrer == referent) { *referrer = nil; } else if (*referrer) { _objc_inform("__weak variable at %p holds %p instead of %p. " "This is probably incorrect use of " "objc_storeWeak() and objc_loadWeak(). " "Break on objc_weak_error to debug.\n", referrer, (void*)*referrer, (void*)referent); objc_weak_error(); } } } //四、从weak_table中移除entry weak_entry_remove(weak_table, entry); }
讲到这里咱们就已经把SideTables的操做流程过一遍了,但愿你们看的开心。