IT资讯

iOS内存管理的那些事儿-原理及实现

作者:admin 2021-08-12 我要评论

为什么要写这篇文章 最近在做内存优化相关的问题,趁着这个机会把内存相关知识捋一捋。虽然现在语言设计的趋势之一就是,让程序员不在关心内存管理这件事。但是...

在说正事之前,我要推荐一个福利:你还在原价购买阿里云、腾讯云、华为云服务器吗?那太亏啦!来这里,新购、升级、续费都打折,能够为您省60%的钱呢!2核4G企业级云服务器低至69元/年,点击进去看看吧>>>)

为什么要写这篇文章

最近在做内存优化相关的问题,趁着这个机会把内存相关知识捋一捋。虽然现在语言设计的趋势之一就是,让程序员不在关心内存管理这件事。但是作为一名程序开发,如果因为语言这个特性,而忽略这方面的知识的话,那是很不可取的,不懂这方面知识,遇到问题会让我们知其然还不知其所以然。因为内存设计的知识比较多,因此我把他做成了系列。***部分讲下基础的知识和原理,第二部分讲下一些开源监测内存泄漏的实现。第三部分讲下如何利用开源工具做相关的APM。文章中难免有出错的地方,还请各位斧正。

iOS内存管理的那些事儿-原理及实现

为什么要进行内存管理

内存是计算机的稀缺资源,在移动设备乃至嵌入设备就显得更为稀缺。不同的操作系统对程序运行时所占用的内存要求不一样。在这里我们主要说一下移动操作系统对运行中App所占用的内存限制。Android不同Rom在默认情况下,对单个App所能申请的内存是有上限。这里的上限没有一个统一的具体值,但可以肯定的是,这个上限是存在的。iOS也同样如此。做移动开发的同学对此应该都会有所感受。内存管理是移动日常开发中非常重要的一环。因此,作为移动开发的我们,不仅要知其然,也要知其所以然。

程序内存空间布局

一个程序被加载到内存中,内存布局通常是分为如下几块。主要分为,代码段,数据段,栈,堆。不同语言的程序可能有所不同,比如C++还会具体区分为全局/静态存储区,常量区,自由存储区。这里主要关注,属于程序员可以分配和释放的部分。虽然有些语言使用了GC技术,但是我们在写代码时候依然要关注内存的分配和释放。

常见的内存管理技术

现代的内存管理技术主要集中在GC(Garbage Collection)上,现在很多语言也在使用GC技术,GC中的内存管理技术主要是有以下这些:

标记清除算法

标记清除算法是有两个部分组成,分别是标记阶段和清除阶段。标记阶段就是对对象进行遍历,将所有可达的对象进行标记。在清除阶段,会将那些没有被标记的对象进行回收,收回内存。这个算法的优缺点容易造成内存碎片

标记复制算法

标记复制算法就是把活动对象复制到新的空间,然后把旧的控件全部释放掉。这个算法不会像清除算法一样产生大量的碎片,因为他是一次把就有空间释放掉,因此吞吐量比较大。速度较快。他缺点也很明显,算法使用可能会用到AB两个空间,对的使用率较低,同时在实现的时候不可能避免的产生递归调用

标记压缩算法

相比较上面的标记清除算法,标记压缩算法会把可达的对象重新排列起来,减少可达对象之间的间隙。这样就不产生内存碎片。相比复制算法不用开辟两个空间,也节约了空间。

引用计数法

引用计数法,内部保存一个计数器,保存了被多少个程序引用。当没有被其他程序引用时候,内存会被回收。相比于其他的算法,引用技术法。有以下的优点,可以及时的回收垃圾,查找次数少。但引用计数有一个比较致命的缺点,无法解决循环引用问题。

通过边对内存管理技术介绍,作为iOS开发会对引用计数法有种熟悉的感觉。iOS也是用到了这个技术,只是实现有所不同。

iOS的内存管理技术

MRC

通过上面关于常见内存管理技术的介绍,我们知道iOS使用的是引用计数这一技术。在前几年iOS是手动管理引用计数的也就是MRC(manual retain-release),MRC,需要程序员自己管理一个对象的引用计数。随着ARC(Automatic Reference Counting)技术的发展。现在已经很少看到MRC的代码。在MRC时代,程序员要手动管理引用计数,通常要遵循一下几个原则

  • 开头为alloc,new,copy,mutableCopy的方法创建的对象,引用计数都会被+1;
  • 如果需要对对象进行引用,可以通过retain来使引用计数+1;
  • 不再使用该对象时候,通过release使应用计数-1;
  • 不要release你没有持有的对象。

ARC

在ARC时代,我们不需要手动retain,relase。由于ARC是一种编译器的技术,因此他本质上并没有变。以前MRC的知识依然是有用且是必要的。ARC引入了一些新的关键词,如strong,weak,__strong,__weak,__unsafe_reatian等等,值得关注是weak,__weak。这两个关键词会在对象释放后,会将引用置位nil,从而避免了野指针的问题。同时,我们也要注意ARC所能管理的只是OC对象,对于非OC的对象,ARC并不会管理他们的内存问题。所以在一个对象转成C的时候,我们要进行桥接。告诉这个编译器对象生命周期有程序员自己来控制;这时候程序员需要手动管理c指针的生命周期。同时C指针转化为OC对象时候,也要进行桥接,这时候桥接的含义则生命周期管理交由ARC管理。你要对它负责。因此我们可以看出来ARC相对于MRC来说,减轻了程序员的负担,不用写大量的retain,relase的代码,同时使用weak,__weak关键字可以有效的避免野指针的问题。其背后的原理则没有变。

iOS内存的代码实现

苹果的runtime源码可以在这里看runtime,如果你觉得这样看不方便的话,你可以通过wget把源码现在下来看,具体命令如下所示

  1. wget -c -r -np -k -L -p https://opensource.apple.com/source/objc4/objc4-723/ 

下面我看看苹果的源码是如何实现。 https://opensource.apple.com/source/objc4/objc4-723/runtime/NSObject.mm.auto.html

alloc

使用一个对象,首先我们得要对象分配内存,所以我们首先来看下alloc的实现吧: alloc方法很简单,里边只是调用了一个C函数 _objc_rootAlloc(Class cls);

  1. + (id)alloc { 
  2.     return _objc_rootAlloc(self); 

而_objc_rootAlloc则调用了callAlloc(Class cls, bool checkNil, bool allocWithZone=false)函数;

  1. id _objc_rootAlloc(Class cls) 
  2.     return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/); 

因此我们只需要重点关注callAlloc这个函数的逻辑,剖析这个函数的行为和功能。

  1. static ALWAYS_INLINE id callAlloc(Class cls, bool checkNil, bool allocWithZone=false
  2.     if (slowpath(checkNil && !cls)) return nil; 
  3.  
  4. #if __OBJC2__ 
  5.     if (fastpath(!cls->ISA()->hasCustomAWZ())) { 
  6.         if (fastpath(cls->canAllocFast())) { 
  7.             bool dtor = cls->hasCxxDtor(); 
  8.             id obj = (id)calloc(1, cls->bits.fastInstanceSize()); 
  9.             if (slowpath(!obj)) return callBadAllocHandler(cls); 
  10.             obj->initInstanceIsa(cls, dtor); 
  11.             return obj; 
  12.         } 
  13.         else { 
  14.             id obj = class_createInstance(cls, 0); 
  15.             if (slowpath(!obj)) return callBadAllocHandler(cls); 
  16.             return obj; 
  17.         } 
  18.     } 
  19. #endif 
  20.  
  21.     if (allocWithZone) return [cls allocWithZone:nil]; 
  22.     return [cls alloc]; 
  1. fastpath(!cls->ISA()->hasCustomAWZ()) 

fastpath 是一个编译优化的宏,他会告诉编译器刮号里边的值大概率是什么,从而编译器在代码优化过程中进行相应汇编指令的优化。这里主要是判断子类或者当前类有没有实现alloc/allocWithZone。如果有实现的话则直接进入

  1. if (allocWithZone) return [cls allocWithZone:nil];  
  2. return [cls alloc]; 

没有实现的话,那么会进入稍复杂的判断逻辑里边,通过宏定义可以看出我们是不支持fastalloc的,所以相关部分逻辑我们暂时忽略过。所以我们只需要关注class_createInstance这个函数的实现。

  1. id class_createInstance(Class cls, size_t extraBytes) 
  2.     return _class_createInstanceFromZone(cls, extraBytes, nil); 
  3.  
  4. static __attribute__((always_inline))  id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,  
  5.                               bool cxxConstruct = true,  
  6.                               size_t *outAllocatedSize = nil) 
  7.     if (!cls) return nil; 
  8.  
  9.     assert(cls->isRealized()); 
  10.  
  11.     bool hasCxxCtor = cls->hasCxxCtor(); 
  12.     bool hasCxxDtor = cls->hasCxxDtor(); 
  13.     bool fast = cls->canAllocNonpointer(); 
  14.  
  15.     size_t size = cls->instanceSize(extraBytes); 
  16.     if (outAllocatedSize) *outAllocatedSize = size
  17.  
  18.     id obj; 
  19.     if (!zone  &&  fast) { 
  20.         obj = (id)calloc(1, size); 
  21.         if (!obj) return nil; 
  22.         obj->initInstanceIsa(cls, hasCxxDtor); 
  23.     }  
  24.     else { 
  25.         if (zone) { 
  26.             obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size); 
  27.         } else { 
  28.             obj = (id)calloc(1, size); 
  29.         } 
  30.         if (!obj) return nil; 
  31.         obj->initIsa(cls); 
  32.     } 
  33.  
  34.     if (cxxConstruct && hasCxxCtor) { 
  35.         obj = _objc_constructOrFree(obj, cls); 
  36.     } 
  37.  
  38.     return obj; 

在这个_class_createInstanceFromZone方法中给对象分配了相应的内存。而初始化则调用了initInstanceIsa 和 initIsa两个方法。而 initInstanceIsa 只是在调用initIsa前进行了判断。因此我们只需要分析initIsa方法。从方法名字看,似乎是对isa进行初始化。是不是这样呢?我们进入到方法内部看看具体实现:

  1. inline void objc_object::initIsa(Class cls) 
  2.     initIsa(cls, falsefalse); 
  3.  
  4. inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)  
  5. {  
  6.     assert(!isTaggedPointer());  
  7.      
  8.     if (!nonpointer) { 
  9.         isa.cls = cls; 
  10.     } else { 
  11.         assert(!DisableNonpointerIsa); 
  12.         assert(!cls->instancesRequireRawIsa()); 
  13.         isa_t newisa(0); 
  14.  
  15. #if SUPPORT_INDEXED_ISA 
  16.         assert(cls->classArrayIndex() > 0); 
  17.         newisa.bits = ISA_INDEX_MAGIC_VALUE; 
  18.         newisa.has_cxx_dtor = hasCxxDtor; 
  19.         newisa.indexcls = (uintptr_t)cls->classArrayIndex(); 
  20. #else 
  21.         newisa.bits = ISA_MAGIC_VALUE; 
  22.         newisa.has_cxx_dtor = hasCxxDtor; 
  23.         newisa.shiftcls = (uintptr_t)cls >> 3; 
  24. #endif 
  25.  
  26.         isa = newisa; 
  27.     } 

这里代码很简单只是简单的赋值操作这里不做细讲,可以说从名字上就可以看出来这个函数要干嘛了。

retain

retain是对引用计数+1操作。分配完内存后我来看看retain是如何实现的

  1. - (id)retain { 
  2.     return ((id)self)->rootRetain(); 
  3.  
  4. ALWAYS_INLINE id objc_object::rootRetain() 
  5.     return rootRetain(falsefalse); 
  6.  
  7. ALWAYS_INLINE id objc_object::rootRetain(bool tryRetain, bool handleOverflow) 
  8.     if (isTaggedPointer()) return (id)this; 
  9.  
  10.     bool sideTableLocked = false
  11.     bool transcribeToSideTable = false
  12.  
  13.     isa_t oldisa; 
  14.     isa_t newisa; 
  15.  
  16.     do { 
  17.         transcribeToSideTable = false
  18.         oldisa = LoadExclusive(&isa.bits); 
  19.         newisa = oldisa; 
  20.         if (slowpath(!newisa.nonpointer)) { 
  21.             ClearExclusive(&isa.bits); 
  22.             if (!tryRetain && sideTableLocked) sidetable_unlock(); 
  23.             if (tryRetain) return sidetable_tryRetain() ? (id)this : nil; 
  24.             else return sidetable_retain(); 
  25.         } 
  26.       
  27.         if (slowpath(tryRetain && newisa.deallocating)) { 
  28.             ClearExclusive(&isa.bits); 
  29.             if (!tryRetain && sideTableLocked) sidetable_unlock(); 
  30.             return nil; 
  31.         } 
  32.         uintptr_t carry; 
  33.         newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++ 
  34.  
  35.         if (slowpath(carry)) { 
  36.       
  37.             if (!handleOverflow) { 
  38.                 ClearExclusive(&isa.bits); 
  39.                 return rootRetain_overflow(tryRetain); 
  40.             } 
  41.      
  42.             if (!tryRetain && !sideTableLocked) sidetable_lock(); 
  43.             sideTableLocked = true
  44.             transcribeToSideTable = true
  45.             newisa.extra_rc = RC_HALF; 
  46.             newisa.has_sidetable_rc = true
  47.         } 
  48.     } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits))); 
  49.  
  50.     if (slowpath(transcribeToSideTable)) { 
  51.         sidetable_addExtraRC_nolock(RC_HALF); 
  52.     } 
  53.  
  54.     if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock(); 
  55.     return (id)this; 

我们来主要看rootRetain的逻辑,他接受两个bool参数。如果是TaggedPointer对象的话直接返回this。因此TaggedPointer的对象调用reatin不会改变引用计数。这个函数里边有个do{}while()的循环,当isa.bits中的值被更新后则循环结束。我们一步一步看下do里边的逻辑。

  1. if (slowpath(!newisa.nonpointer)) { 
  2.     ClearExclusive(&isa.bits); 
  3.     if (!tryRetain && sideTableLocked) sidetable_unlock(); 
  4.     if (tryRetain) return sidetable_tryRetain() ? (id)this : nil; 
  5.     else return sidetable_retain(); 
  6.  } 

这段逻辑主要处理当前类没有开启进行内存优化的情况。这里主要有两个函数sidetable_tryRetain和sidetable_retain。

  1. bool objc_object::sidetable_tryRetain() 
  2. #if SUPPORT_NONPOINTER_ISA 
  3.     assert(!isa.nonpointer); 
  4. #endif 
  5.     SideTable& table = SideTables()[this]; 
  6.     bool result = true
  7.     RefcountMap::iterator it = table.refcnts.find(this); 
  8.     if (it == table.refcnts.end()) { 
  9.         table.refcnts[this] = SIDE_TABLE_RC_ONE; 
  10.     } else if (it->second & SIDE_TABLE_DEALLOCATING) { 
  11.         result = false
  12.     } else if (! (it->second & SIDE_TABLE_RC_PINNED)) { 
  13.         it->second += SIDE_TABLE_RC_ONE; 
  14.     } 
  15.      
  16.     return result; 
  17.  
  18. id objc_object::sidetable_retain() 
  19. #if SUPPORT_NONPOINTER_ISA 
  20.     assert(!isa.nonpointer); 
  21. #endif 
  22.     SideTable& table = SideTables()[this]; 
  23.      
  24.     table.lock(); 
  25.     size_t& refcntStorage = table.refcnts[this]; 
  26.     if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) { 
  27.         refcntStorage += SIDE_TABLE_RC_ONE; 
  28.     } 
  29.     table.unlock(); 
  30.  
  31.     return (id)this; 

sidetable_tryRetain函数主要做了这几件事,先从散列表中取出数值,如果这个数值找不到,就在Map添加 SIDE_TABLE_RC_ONE 值,如果这个数值所在的对象正在析构,那么将result置位false。***检查下这个数字是否溢出,如果没有溢出则将引用计数+1;而sidetable_retain函数加了个自旋锁,同时逻辑更简单些。检查是否数值是否溢出,没有溢出则引用计数+1;说完这两个函数,我们在回到rootTryRetain()函数。

  1. if (slowpath(tryRetain && newisa.deallocating)) {  
  2. ClearExclusive(&isa.bits);  
  3. if (!tryRetain && sideTableLocked) sidetable_unlock();  
  4. return nil;  

这里的逻辑判断对象是否在析构。如果在析构则会进行相关处理操作。这下来我们看看开启了指针优化后的retain逻辑

  1. newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); 

这行也是对引用计数+1的,是对其中的extra_rc进行+1

  1.  if (slowpath(carry)) { 
  2.      if (!handleOverflow) { 
  3.          ClearExclusive(&isa.bits); 
  4.          return rootRetain_overflow(tryRetain); 
  5.       } 
  6.      if (!tryRetain && !sideTableLocked) sidetable_lock(); 
  7.      sideTableLocked = true
  8.      transcribeToSideTable = true
  9.      newisa.extra_rc = RC_HALF; 
  10.      newisa.has_sidetable_rc = true

这里判断是否溢出,如果溢出了就会进入到rootRetain_overflow函数里边,而rootRetain_overflow函数则又调用了rootRetain,只不过handleOverflow会传true,同时会处理溢出的情况,这时候transcribeToSideTable为true,在结束后就会调用sidetable_addExtraRC_nolock(RC_HALF);,我们来看下这个函数的实现。

  1. bool  
  2. objc_object::sidetable_addExtraRC_nolock(size_t delta_rc) 
  3.     SideTable& table = SideTables()[this]; 
  4.  
  5.     size_t& refcntStorage = table.refcnts[this]; 
  6.     size_t oldRefcnt = refcntStorage; 
  7.    
  8.     if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true
  9.  
  10.     uintptr_t carry; 
  11.     size_t newRefcnt =  
  12.         addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry); 
  13.     if (carry) { 
  14.         refcntStorage = 
  15.             SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK); 
  16.         return true
  17.     } 
  18.     else { 
  19.         refcntStorage = newRefcnt; 
  20.         return false
  21.     } 

之前我们调用addc发现溢出后,我们把newisa.extra_rc 置位RC_HALF,同时我们调用sidetable_addExtraRC_nolock同时把剩下的RC_HALF加入散列表中;也是通过addc进行操作。如果这是溢出则恢复散列表中的值,至此retain的逻辑差不多结束了。

  1. - (oneway void)release { 
  2.     ((id)self)->rootRelease(); 
  3.  
  4. ALWAYS_INLINE bool objc_object::rootRelease() 
  5.     return rootRelease(truefalse); 
  6.  
  7. ALWAYS_INLINE bool objc_object::rootRelease(bool performDealloc, bool handleUnderflow) 
  8.     if (isTaggedPointer()) return false
  9.  
  10.     bool sideTableLocked = false
  11.  
  12.     isa_t oldisa; 
  13.     isa_t newisa; 
  14.  
  15.  retry: 
  16.     do { 
  17.         oldisa = LoadExclusive(&isa.bits); 
  18.         newisa = oldisa; 
  19.         if (slowpath(!newisa.nonpointer)) { 
  20.             ClearExclusive(&isa.bits); 
  21.             if (sideTableLocked) sidetable_unlock(); 
  22.             return sidetable_release(performDealloc); 
  23.         } 
  24.   
  25.         uintptr_t carry; 
  26.         newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); 
  27.         if (slowpath(carry)) { 
  28.             goto underflow; 
  29.         } 
  30.     } while (slowpath(!StoreReleaseExclusive(&isa.bits,  
  31.                                              oldisa.bits, newisa.bits))); 
  32.  
  33.     if (slowpath(sideTableLocked)) sidetable_unlock(); 
  34.     return false
  35.  
  36.  underflow: 
  37.     newisa = oldisa; 
  38.  
  39.     if (slowpath(newisa.has_sidetable_rc)) { 
  40.         if (!handleUnderflow) { 
  41.             ClearExclusive(&isa.bits); 
  42.             return rootRelease_underflow(performDealloc); 
  43.         } 
  44.  
  45.         if (!sideTableLocked) { 
  46.             ClearExclusive(&isa.bits); 
  47.             sidetable_lock(); 
  48.             sideTableLocked = true
  49.             goto retry; 
  50.         } 
  51.          
  52.         size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF); 
  53.  
  54.         if (borrowed > 0) { 
  55.             newisa.extra_rc = borrowed - 1;   
  56.             bool stored = StoreReleaseExclusive(&isa.bits,  
  57.                                                 oldisa.bits, newisa.bits); 
  58.             if (!stored) { 
  59.              
  60.                 isa_t oldisa2 = LoadExclusive(&isa.bits); 
  61.                 isa_t newisa2 = oldisa2; 
  62.                 if (newisa2.nonpointer) { 
  63.                     uintptr_t overflow; 
  64.                     newisa2.bits =  
  65.                         addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow); 
  66.                     if (!overflow) { 
  67.                         stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits,  
  68.                                                        newisa2.bits); 
  69.                     } 
  70.                 } 
  71.             } 
  72.  
  73.             if (!stored) { 
  74.                 sidetable_addExtraRC_nolock(borrowed); 
  75.                 goto retry; 
  76.             } 
  77.  
  78.             sidetable_unlock(); 
  79.             return false
  80.         } 
  81.         else { 
  82.          
  83.         } 
  84.     } 
  85.  
  86.     if (slowpath(newisa.deallocating)) { 
  87.         ClearExclusive(&isa.bits); 
  88.         if (sideTableLocked) sidetable_unlock(); 
  89.         return overrelease_error(); 
  90.     } 
  91.     newisa.deallocating = true
  92.     if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry; 
  93.  
  94.     if (slowpath(sideTableLocked)) sidetable_unlock(); 
  95.  
  96.     __sync_synchronize(); 
  97.     if (performDealloc) { 
  98.         ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc); 
  99.     } 
  100.     return true

看完调用顺序后,我们着重分析下这个函数吧

  1. objc_object::rootRelease(bool performDealloc, bool handleUnderflow) 

同样如果是TaggedPointer对象直接返回 false。我们先看retry:代码段这里边的部分逻辑与retain相似,我们不一一分析。如果没有开启指针优化的话会有调用这样关键函数

  1. uintptr_t 
  2. objc_object::sidetable_release(bool performDealloc) 
  3. #if SUPPORT_NONPOINTER_ISA 
  4.     assert(!isa.nonpointer); 
  5. #endif 
  6.     SideTable& table = SideTables()[this]; 
  7.  
  8.     bool do_dealloc = false
  9.  
  10.     table.lock(); 
  11.     RefcountMap::iterator it = table.refcnts.find(this); 
  12.     if (it == table.refcnts.end()) { 
  13.         do_dealloc = true
  14.         table.refcnts[this] = SIDE_TABLE_DEALLOCATING; 
  15.     } else if (it->second < SIDE_TABLE_DEALLOCATING) { 
  16.         do_dealloc = true
  17.         it->second |= SIDE_TABLE_DEALLOCATING; 
  18.     } else if (! (it->second & SIDE_TABLE_RC_PINNED)) { 
  19.         it->second -= SIDE_TABLE_RC_ONE; 
  20.     } 
  21.     table.unlock(); 
  22.     if (do_dealloc  &&  performDealloc) { 
  23.         ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc); 
  24.     } 
  25.     return do_dealloc; 

这里主要做了这几个逻辑,如果在散列表中没有找到对象,那么将其中的值置为SIDE_TABLE_DEALLOCATING。如果找到值比SIDE_TABLE_DEALLOCATING还小那么将it中second置位SIDE_TABLE_DEALLOCATING。如果找到的值不属于上面情况。那么检查是否溢出,没有溢出则引用计数-1;***如果这个do_dealloc为true(这个链路里边的performDealloc为true)那么就给会给发送一个SEL_dealloc 的消息进行释放。分析完这个函数后我们继续回到rootRelease中,下面代码是开启了指针优化的情况,接下来会调用

  1. uintptr_t carry;  
  2. newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); 

将引用计数-1;同时 会做溢出判断,如果已经溢出了,则会跳到underflow:代码段。这段代码的主要逻辑在一个长长的if语句里边。这里边先判断has_sidetable_rc这个属性,这个属性代表如果为yes,那么代表会有部分引用计数存到一table里边。如果没有那么说明已经没有引用了。直接走释放逻辑。如果有的话,那么要从table中取出引用计数,然后进行-1操作,然后赋值给newisa.extra_rc,如果-1操作失败会立即进行一次。如果还是失败那么要table中引用计数恢复,然后进入retry代码重复这样的逻辑.

autolrease

***说一下autolrease吧,先贴上调用栈。 @autoreleasepool{}经过clang -rewrite-objc命令后,我们可以看到

  1. struct __AtAutoreleasePool { 
  2.   __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();} 
  3.   ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);} 
  4.   void * atautoreleasepoolobj; 
  5. }; 

这样的结构体。初始化的时候会调用objc_autoreleasePoolPush()方法,~相当于OC中的delloc方法,他会调用objc_autoreleasePoolPop(atautoreleasepoolobj)方法,传入的参数就是我们刚刚通过objc_autoreleasePoolPush()生成的对象。关于@autoreleasepool{}的创建和释放逻辑我们看这两个函数就可以了。我们先从objc_autoreleasePoolPush()这个函数开始。

  1. objc_autoreleasePoolPush(void) 
  2.     return AutoreleasePoolPage::push(); 
  3.  
  4. static inline void *push()  
  5.     id *dest; 
  6.     if (DebugPoolAllocation) { 
  7.         dest = autoreleaseNewPage(POOL_BOUNDARY); 
  8.     } else { 
  9.         dest = autoreleaseFast(POOL_BOUNDARY); 
  10.     } 
  11.     assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY); 
  12.     return dest; 
  13.  
  14. static inline id *autoreleaseFast(id obj) 
  15.   AutoreleasePoolPage *page = hotPage(); 
  16.   if (page && !page->full()) { 
  17.       return page->add(obj); 
  18.   } else if (page) { 
  19.       return autoreleaseFullPage(obj, page); 
  20.   } else { 
  21.       return autoreleaseNoPage(obj); 
  22.  } 

这里边会调用AutoreleasePoolPage类的push()方法,我们看一下AutoreleasePoolPage结构

  1. class AutoreleasePoolPage  
  2.   
  3. #   define EMPTY_POOL_PLACEHOLDER ((id*)1) 
  4. #   define POOL_BOUNDARY nil 
  5.  
  6.     static pthread_key_t const key = AUTORELEASE_POOL_KEY; 
  7.     static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing 
  8.     static size_t const SIZE =  
  9. #if PROTECT_AUTORELEASEPOOL 
  10.         PAGE_MAX_SIZE;  // must be multiple of vm page size 
  11. #else 
  12.         PAGE_MAX_SIZE;  // size and alignment, power of 2 
  13. #endif 
  14.  
  15.     static size_t const COUNT = SIZE / sizeof(id); 
  16.  
  17.     magic_t const magic; 
  18.     id *next
  19.     pthread_t const thread; 
  20.     AutoreleasePoolPage * const parent; 
  21.     AutoreleasePoolPage *child; 
  22.     uint32_t const depth; 
  23.     uint32_t hiwat; 
  24.      
  25.  } 

EMPTY_POOL_PLACEHOLDER这个宏看名字意思是占位的意思。

从作用上来看,当一个外部调用***次调用创建AutoreleasePoolPage,但是没有任何要进栈的对象时候,那么他不会先创建一个AutoreleasePoolPage对象,而是把EMPTY_POOL_PLACEHOLDER作为指针返回,并用TLS技术绑定当前线程。这样的实现有点像懒加载,在需要的时候才创建对象。

POOL_BOUNDARY这个之前是POOL_SENTINEL,他们同样值都是nil。

作用都是在***次有对象入栈时候会push一个空的对象。这样以后在pop的时候通过判断值是不是nil,知道是不是栈底了。相比于POOL_SENTINEL我更觉得POOL_BOUNDARY意思简洁明了。

static pthread_key_t const key = AUTORELEASE_POOL_KEY 这个这个就是TLS把当前hotpage或者EMPTY_POOL_PLACEHOLDER存储在当前线程的key。没有什么好说的。

static uint8_t const SCRIBBLE = 0xA3;这个是常数值,唯一的作用就是在releasing的时候通过memset((void*)page->next, SCRIBBLE, sizeof(*page->next));把page的next置位0xA3A3A3A3

magic_t const magic;这个magic用来校验类的完整性。 id *next;栈的指针。 pthread_t const thread;用于保存线程。

  1. AutoreleasePoolPage * const parent;  
  2. AutoreleasePoolPage *child;  
  3. uint32_t const depth;  
  4. uint32_t hiwat; 

这几个属性都是跟双向链表有关系,parent指向父节点,child指向子节点。depth这个是层级,hiwat这个应该栈里数据的数量。

分析完这个类的结构。我们继续看调用的流程。再调用到static inline id *autoreleaseFast(id obj)方法时,里边有三个分支走向。我们首先看下一个关键一行 AutoreleasePoolPage *page = hotPage();这个hotPage()是通过TLS取当前的AutoreleasePoolPage的。如果是EMPTY_POOL_PLACEHOLDER的话直接返回nil,否则的话就会返回AutoreleasePoolPage,返回之前会做一个完整性检测。

  1. if (page && !page->full()) { 
  2.       return page->add(obj); 
  3.   } else if (page) { 
  4.       return autoreleaseFullPage(obj, page); 
  5.   } else { 
  6.       return autoreleaseNoPage(obj); 
  7.  } 

这个判断也是比较简单的,如果当前不为nil,且没有满则直接调用add函数,添加obj。这个add函数也是比较简单入栈操作。只是在入栈的时候做了线程保护。当然我们根据宏是没有启用这个线程保护功能的。如果当前page已经满了,那么会调用autoreleaseFullPage方法。我们看下autoreleaseFullPage怎么实现的。

  1. static __attribute__((noinline)) 
  2.   id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) 
  3.   { 
  4.       assert(page == hotPage()); 
  5.       assert(page->full()  ||  DebugPoolAllocation); 
  6.  
  7.       do { 
  8.           if (page->child) page = page->child; 
  9.           else page = new AutoreleasePoolPage(page); 
  10.       } while (page->full()); 
  11.  
  12.       setHotPage(page); 
  13.       return page->add(obj); 
  14.   } 

这个方法的逻辑也没有复杂的地方。你遍历子节点直到找到没有满的page,如果***都没有找到,那么就新建一个page,然后把这个page绑定到当前线程。同时调用add方法添加这个obj。然后我们再看下***一个分支走向autoreleaseNoPage(obj)方法

  1. static __attribute__((noinline)) 
  2.     id *autoreleaseNoPage(id obj) 
  3.     { 
  4.          
  5.         assert(!hotPage()); 
  6.  
  7.         bool pushExtraBoundary = false
  8.          
  9.         if (haveEmptyPoolPlaceholder()) { 
  10.              
  11.             pushExtraBoundary = true
  12.         } 
  13.         else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) { 
  14.             _objc_inform("MISSING POOLS: (%p) Object %p of class %s " 
  15.                          "autoreleased with no pool in place - " 
  16.                          "just leaking - break on " 
  17.                          "objc_autoreleaseNoPool() to debug",  
  18.                          pthread_self(), (void*)obj, object_getClassName(obj)); 
  19.             objc_autoreleaseNoPool(obj); 
  20.             return nil; 
  21.         } 
  22.         else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) { 
  23.              
  24.             return setEmptyPoolPlaceholder(); 
  25.         } 
  26.  
  27.        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil); 
  28.        setHotPage(page); 
  29.         
  30.        if (pushExtraBoundary) { 
  31.            page->add(POOL_BOUNDARY); 
  32.        } 
  33.   
  34.        return page->add(obj); 
  35.     } 

相比于前几个方法这个方法逻辑就稍稍复杂了点。bool pushExtraBoundary = false;这个属性表示要不要像栈里边添加POOL_BOUNDARY,这个只有在栈为空的时候才会是true。第二个if判断主要是用debug相关,这里先不管。第三个判断,如果传的是一个POOL_BOUNDARY对象且没有调试alloc的时候,会将当前线程绑定一个EMPTY_POOL_PLACEHOLDER的占位对象,并返回。经过这些判断,我们走到了这里

  1. AutoreleasePoolPage *page = new AutoreleasePoolPage(nil); 
  2.        setHotPage(page); 
  3.         
  4. if (pushExtraBoundary) { 
  5.     page->add(POOL_BOUNDARY); 
  6.   
  7. return page->add(obj); 

这里的代码比较简单,新建一个AutoreleasePoolPage对象,并且设置为hotpage,然后如果pushExtraBoundary为true,则把POOL_BOUNDARY入栈,然后把obj入栈。***返回page对象。这里大家可能有疑问了,这里有条件的将POOL_BOUNDARY入栈,为不为导致底不是POOL_BOUNDARY,有这个疑问是很好的。可以我们看整个NSObject.mm的代码,可以看到不会出现栈底元素不是POOL_BOUNDARY的。至此,我们把@autorelease{}代码的新建逻辑分析完毕。下面我们来看释放逻辑。

  1. void 
  2. objc_autoreleasePoolPop(void *ctxt) 
  3.     AutoreleasePoolPage::pop(ctxt); 
  4.  
  5.  static inline void pop(void *token)  
  6.     { 
  7.         AutoreleasePoolPage *page; 
  8.         id *stop; 
  9.  
  10.         if (token == (void*)EMPTY_POOL_PLACEHOLDER) { 
  11.             if (hotPage()) { 
  12.                 pop(coldPage()->begin()); 
  13.             } else { 
  14.                 setHotPage(nil); 
  15.             } 
  16.             return
  17.         } 
  18.  
  19.         page = pageForPointer(token); 
  20.         stop = (id *)token; 
  21.         if (*stop != POOL_BOUNDARY) { 
  22.             if (stop == page->begin()  &&  !page->parent) { 
  23.              
  24.             } else { 
  25.                 return badPop(token); 
  26.             } 
  27.         } 
  28.  
  29.         if (PrintPoolHiwat) printHiwat(); 
  30.  
  31.         page->releaseUntil(stop); 
  32.  
  33.         if (DebugPoolAllocation  &&  page->empty()) { 
  34.             AutoreleasePoolPage *parent = page->parent; 
  35.             page->kill(); 
  36.             setHotPage(parent); 
  37.         } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) { 
  38.             page->kill(); 
  39.             setHotPage(nil); 
  40.         }  
  41.         else if (page->child) { 
  42.             if (page->lessThanHalfFull()) { 
  43.                 page->child->kill(); 
  44.             } 
  45.             else if (page->child->child) { 
  46.                 page->child->child->kill(); 
  47.             } 
  48.         } 
  49.     } 

看调用流程,我们着重分析下pop(void *token)方法,我们先看下段代码块的逻辑:

  1. if (token == (void*)EMPTY_POOL_PLACEHOLDER) { 
  2.  
  3.     if (hotPage()) { 
  4.        pop(coldPage()->begin()); 
  5.     } else { 
  6.        setHotPage(nil); 
  7.     } 
  8.      return
  9.       

这段逻辑主要判断如果pop的是一个EMPTY_POOL_PLACEHOLDER,这个就是我们之前空池占位。那么先判断是否存在hotpage,若果存在的话,那么将调用pop方法,同时传入当前hotpage的最初的父节点,coldPage()返回的是***个节点。如果不存在hotpage,那么将TLS绑定的值置位nil。我们继续看下面的代码块:

  1. page = pageForPointer(token); 
  2. stop = (id *)token; 
  3. if (*stop != POOL_BOUNDARY) { 
  4.     if (stop == page->begin()  &&  !page->parent) { 
  5.  
  6.      } else {              
  7.          return badPop(token); 
  8.      } 

page = pageForPointer(token);这个函数根据传入的token获取page的首指针。获取到page后,下面检查一下token,通常下我们pop最终会传入一个page的beigin指针。这个通常应该是POOL_BOUNDARY,这里主要是做异常处理。接下来我们会走到这个函数

  1. page->releaseUntil(stop); 

这个函数的实现如下:

  1. void releaseUntil(id *stop)  
  2.  { 
  3.       
  4.    while (this->next != stop) { 
  5.             
  6.      AutoreleasePoolPage *page = hotPage(); 
  7.       
  8.      while (page->empty()) { 
  9.      page = page->parent; 
  10.      setHotPage(page); 
  11.      } 
  12.  
  13.      page->unprotect(); 
  14.      id obj = *--page->next; 
  15.      memset((void*)page->next, SCRIBBLE, sizeof(*page->next)); 
  16.      page->protect(); 
  17.  
  18.      if (obj != POOL_BOUNDARY) { 
  19.         objc_release(obj); 
  20.      } 
  21.      } 
  22.  
  23.      setHotPage(this); 
  24.  

这个函数的实现逻辑还是比较清楚的,他依次释放栈的内容直到遇到stop,并且把next指向的区域置为SCRIBBLE,然后把最近的栈为非空的置为当前的hotpage。***我们看一下kill的相关逻辑

  1. if (page->lessThanHalfFull()) { 
  2.     page->child->kill(); 
  3. }else if (page->child->child) { 
  4.     page->child->child->kill(); 

上面的判断逻辑主要是经过releaseUntil后,当前的page的栈已经被清空了,当前栈如果有子节点那么就释放子节点。***我们看一下kill方法。

  1. void kill()  
  2.     AutoreleasePoolPage *page = this; 
  3.     while (page->child) page = page->child; 
  4.  
  5.     AutoreleasePoolPage *deathptr; 
  6.     do { 
  7.         deathptr = page; 
  8.          page = page->parent; 
  9.          if (page) { 
  10.           page->unprotect(); 
  11.           page->child = nil; 
  12.          page->protect(); 
  13.         } 
  14.             delete deathptr; 
  15.    } while (deathptr != this); 
  16.     

这段逻辑就相当简单了,依次释放子节点。至此@autorelease{}就分析完毕了,关于autorelease方法这里就不再分析了,autorelease逻辑基本上与我们上面分析的高度重合,这里不展开。

常见的容易造成泄漏的点

分析完源码后,我们知道iOS中的引用计数是怎么实现的,但这只是初步。内存管理难点不是在原理,而是在复杂的场景下怎么保证内存不泄漏,这才是最难的。我们先列举常见的容易造成泄漏的点:

循环引用

引用计数计数***的缺点就是他无法解决循环引用的问题。如果出现循环引用了,需要我们手动打破循环引用。否则会一直占用内存。常见的循环引用情况主要是block。因为block会强引用外部变量,如果外部变量也在强引用这个block。那么他们就会造成循环引用。比如

  1. HasBlock *hasBlock = [[HasBlock alloc] init]; 
  2.  
  3. hasBlock setBlock:^{ 
  4.        hasBlock.name = @"abc"
  5. }]; 

修改方法也很简单通过一个弱引用间接使用改造如下

  1.  HasBlock *hasBlock = [[HasBlock alloc] init]; 
  2.  __weak HasBlock* weakHasBlock = hasBlock; 
  3. [hasBlock setBlock:^{ 
  4.         weakHasBlock.name = @"abc"
  5.  }]; 

这样就可以解决循环引用,这个是比较常见循环引用情况网上有很多宏解决这个问题。这里不展开。

使用单例的的一些情况

在使用单例的时候要注意,特别是单例含有block回调方法时候。有些单例会强持有这些block。这种情况虽然不是循环引用,但也是造成了喜欢引用。所以在使用单例的时候要清楚。如系统有些方法这样使用会造成无法释放:

  1. - (void)viewDidLoad { 
  2.     [super viewDidLoad]; 
  3.     [[NSNotificationCenter defaultCenter] addObserverForName:@"boyce" object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) { 
  4.         self.name = @"boyce"
  5.     }]; 
  6.      
  7.  
  8. - (void)dealloc{ 
  9.     [[NSNotificationCenter defaultCenter] removeObserver:self]; 
  10.  

这里就造成了内存泄漏,这是因为NSNotificationCenter强引用了usingBlock,而usingBlock强引用了self,而NSNotificationCenter是个单例不会被释放,而self在被释放的时候才会去把自己从NSNotificationCenter中移除。类似的情况还有很多,比如一个数组中对象等等。这些内存泄漏不容易发现。

NSTimer

NSTimer会强引用传入的target,这时候如果加入NSRunLoop这个timer又会被NSRunLoop强引用

  1. NSTimer *timer = [NSTimer timerWithTimeInterval:10 target:self selector:@selector(commentAnimation) userInfo:nil repeats:YES];  
  2. [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; 

解决这个方法主动stoptimer,至少是不能在dealloc中stoptimer的。另外可以设置一个中间类,把target变成中间类。

NSURLSession

这个问题和上面的NSTimer类似

  1. NSURLSession *section = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]  
  2. delegate:self  
  3. delegateQueue:[[NSOperationQueue alloc] init]];  
  4. NSURLSessionDataTask *task = [section dataTaskWithURL:[NSURL URLWithString:path]  
  5. completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {  
  6. //Do something  
  7. }];  
  8. [task resume]; 

这里NSURLSession会强引用了self。同时本地SSL会对一个NSURLSession缓存一段时间。所以即使没有强引用。也会造成内存泄漏。这里比较好的使用单例[NSURLSession sharedSession]

非OC对象的内存问题

在OC对象转换为非OC对象时候,要进行桥接。要把对象的控制权由ARC转换为程序员自己控制,这时候程序员要自己控制对象创建和释放。如下面的简单代码

  1. NSString *name = @"boyce" 
  2. CFStringRef cfStringRef = (__bridge CFStringRef) name 
  3. CFRelease(cfStringRef); 

其他泄漏情况

如果present一个UINavigationController,如果返回的姿势不正确。会造成内存泄漏

  1. UIViewController *vc = [[UIViewController alloc]init];  
  2. UINavigationController *nav = [[UINavigationController alloc]initWithRootViewController:vc];  
  3. [self presentViewController:nav animated:YES completion:NULL]; 

如果在UIViewController里边调用的是

  1. [self dismissViewControllerAnimated:YES completion:NULL]; 

那么就会造成内存泄漏,这里边测试发现vc是没有被释放的。需要这样调用

  1. if (self.navigationController.topViewController == self) { 
  2.        [self.navigationController dismissViewControllerAnimated:YES completion:nil]; 
  3.    } 

想说的

我认为内存管理的一些基本原理还是比较简单容易理解,难就难在结合复杂的场景,在一些复杂的场景下我们比较不容易发现内存泄漏的点。但是当我们把内存泄漏解决后你会发现,原来就是这么回事!!!

结束语

这部分就到此结束了,我们介绍了内存管理的原理,实现以及造成泄漏的常见场景。下篇介绍一些开源检测内存泄漏工具以及他们的实现。谢谢大家。

作者简介:boyce,饿了么物流团队资深iOS开发。曾在格瓦拉等公司从事iOS相关研发工作。


本文转载自网络,原文链接:https://juejin.im/post/5c0744f6e51d45598b76f481

版权声明:本文转载自网络,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。本站转载出于传播更多优秀技术知识之目的,如有侵权请联系QQ/微信:153890879删除

相关文章
  • 小技巧:如何在iPhone上将网页转为PDF

    小技巧:如何在iPhone上将网页转为PDF

  • iOS不这样写简历,都找不到工作了

    iOS不这样写简历,都找不到工作了

  • 保护微信支付宝安全,这三大功能你需要

    保护微信支付宝安全,这三大功能你需要

  • iPhone用户打车比Android用户贵,真的

    iPhone用户打车比Android用户贵,真的

腾讯云代理商
海外云服务器