iOS内存管理的那些事儿-原理及实现
栏目: Objective-C · 发布时间: 6年前
内容简介:作者简介boyce,饿了么物流团队资深iOS开发。曾在格瓦拉等公司从事iOS相关研发工作。最近在做内存优化相关的问题,趁着这个机会把内存相关知识捋一捋。虽然现在语言设计的趋势之一就是,让程序员不在关心内存管理这件事。但是作为一名程序开发,如果因为语言这个特性,而忽略这方面的知识的话,那是很不可取的,不懂这方面知识,遇到问题会让我们知其然还不知其所以然。因为内存设计的知识比较多,因此我把他做成了系列。第一部分讲下基础的知识和原理,第二部分讲下一些开源监测内存泄漏的实现。第三部分讲下如何利用开源工具做相关的A
作者简介
boyce,饿了么物流团队资深iOS开发。曾在格瓦拉等公司从事iOS相关研发工作。
注:本篇文章是《iOS内存管理的那些事儿》系列文章的第一部分。稍后我们会持续更新第二部分(开源监测内存泄漏的实现)和第三部分(如何利用开源 工具 做相关的APM),感兴趣的童鞋可以关注我们专栏并获取实时推送信息哦~
为什么要写这篇文章
最近在做内存优化相关的问题,趁着这个机会把内存相关知识捋一捋。虽然现在语言设计的趋势之一就是,让 程序员 不在关心内存管理这件事。但是作为一名程序开发,如果因为语言这个特性,而忽略这方面的知识的话,那是很不可取的,不懂这方面知识,遇到问题会让我们知其然还不知其所以然。因为内存设计的知识比较多,因此我把他做成了系列。第一部分讲下基础的知识和原理,第二部分讲下一些开源监测内存泄漏的实现。第三部分讲下如何利用开源工具做相关的APM。文章中难免有出错的地方,还请各位斧正。
为什么要进行内存管理
内存是计算机的稀缺资源,在移动设备乃至嵌入设备就显得更为稀缺。不同的操作系统对程序运行时所占用的内存要求不一样。在这里我们主要说一下移动操作系统对运行中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)技术的发展。现在已经很少看到ARC的代码。在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把源码现在下来看,具体命令如下所示
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);
+ (id)alloc { return _objc_rootAlloc(self); } 复制代码
而 _objc_rootAlloc
则调用了 callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
函数;
id _objc_rootAlloc(Class cls) { return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/); } 复制代码
因此我们只需要重点关注callAlloc这个函数的逻辑,剖析这个函数的行为和功能。
static ALWAYS_INLINE id callAlloc(Class cls, bool checkNil, bool allocWithZone=false) { if (slowpath(checkNil && !cls)) return nil; #if __OBJC2__ if (fastpath(!cls->ISA()->hasCustomAWZ())) { if (fastpath(cls->canAllocFast())) { bool dtor = cls->hasCxxDtor(); id obj = (id)calloc(1, cls->bits.fastInstanceSize()); if (slowpath(!obj)) return callBadAllocHandler(cls); obj->initInstanceIsa(cls, dtor); return obj; } else { id obj = class_createInstance(cls, 0); if (slowpath(!obj)) return callBadAllocHandler(cls); return obj; } } #endif if (allocWithZone) return [cls allocWithZone:nil]; return [cls alloc]; } 复制代码
fastpath(!cls->ISA()->hasCustomAWZ()) 复制代码
fastpath 是一个编译优化的宏,他会告诉编译器刮号里边的值大概率是什么,从而编译器在代码优化过程中进行相应汇编指令的优化。这里主要是判断子类或者当前类有没有实现 alloc/allocWithZone
。如果有实现的话则直接进入
if (allocWithZone) return [cls allocWithZone:nil]; return [cls alloc]; 复制代码
没有实现的话,那么会进入稍复杂的判断逻辑里边,通过宏定义可以看出我们是不支持fastalloc的,所以相关部分逻辑我们暂时忽略过。所以我们只需要关注class_createInstance这个函数的实现。
id class_createInstance(Class cls, size_t extraBytes) { return _class_createInstanceFromZone(cls, extraBytes, nil); } static __attribute__((always_inline)) id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, bool cxxConstruct = true, size_t *outAllocatedSize = nil) { if (!cls) return nil; assert(cls->isRealized()); bool hasCxxCtor = cls->hasCxxCtor(); bool hasCxxDtor = cls->hasCxxDtor(); bool fast = cls->canAllocNonpointer(); size_t size = cls->instanceSize(extraBytes); if (outAllocatedSize) *outAllocatedSize = size; id obj; if (!zone && fast) { obj = (id)calloc(1, size); if (!obj) return nil; obj->initInstanceIsa(cls, hasCxxDtor); } else { if (zone) { obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size); } else { obj = (id)calloc(1, size); } if (!obj) return nil; obj->initIsa(cls); } if (cxxConstruct && hasCxxCtor) { obj = _objc_constructOrFree(obj, cls); } return obj; } 复制代码
在这个 _class_createInstanceFromZone
方法中给对象分配了相应的内存。而初始化则调用了 initInstanceIsa
和 initIsa
两个方法。而 initInstanceIsa
只是在调用 initIsa
前进行了判断。因此我们只需要分析 initIsa
方法。从方法名字看,似乎是对 isa
进行初始化。是不是这样呢?我们进入到方法内部看看具体实现:
inline void objc_object::initIsa(Class cls) { initIsa(cls, false, false); } inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) { assert(!isTaggedPointer()); if (!nonpointer) { isa.cls = cls; } else { assert(!DisableNonpointerIsa); assert(!cls->instancesRequireRawIsa()); isa_t newisa(0); #if SUPPORT_INDEXED_ISA assert(cls->classArrayIndex() > 0); newisa.bits = ISA_INDEX_MAGIC_VALUE; newisa.has_cxx_dtor = hasCxxDtor; newisa.indexcls = (uintptr_t)cls->classArrayIndex(); #else newisa.bits = ISA_MAGIC_VALUE; newisa.has_cxx_dtor = hasCxxDtor; newisa.shiftcls = (uintptr_t)cls >> 3; #endif isa = newisa; } } 复制代码
这里代码很简单只是简单的赋值操作这里不做细讲,可以说从名字上就可以看出来这个函数要干嘛了。
retain
retain
是对引用计数+1操作。分配完内存后我来看看 retain
是如何实现的
- (id)retain { return ((id)self)->rootRetain(); } ALWAYS_INLINE id objc_object::rootRetain() { return rootRetain(false, false); } ALWAYS_INLINE id objc_object::rootRetain(bool tryRetain, bool handleOverflow) { if (isTaggedPointer()) return (id)this; bool sideTableLocked = false; bool transcribeToSideTable = false; isa_t oldisa; isa_t newisa; do { transcribeToSideTable = false; oldisa = LoadExclusive(&isa.bits); newisa = oldisa; if (slowpath(!newisa.nonpointer)) { ClearExclusive(&isa.bits); if (!tryRetain && sideTableLocked) sidetable_unlock(); if (tryRetain) return sidetable_tryRetain() ? (id)this : nil; else return sidetable_retain(); } if (slowpath(tryRetain && newisa.deallocating)) { ClearExclusive(&isa.bits); if (!tryRetain && sideTableLocked) sidetable_unlock(); return nil; } uintptr_t carry; newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++ if (slowpath(carry)) { if (!handleOverflow) { ClearExclusive(&isa.bits); return rootRetain_overflow(tryRetain); } if (!tryRetain && !sideTableLocked) sidetable_lock(); sideTableLocked = true; transcribeToSideTable = true; newisa.extra_rc = RC_HALF; newisa.has_sidetable_rc = true; } } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits))); if (slowpath(transcribeToSideTable)) { sidetable_addExtraRC_nolock(RC_HALF); } if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock(); return (id)this; } 复制代码
我们来主要看 rootRetain
的逻辑,他接受两个bool参数。如果是 TaggedPointer
对象的话直接返回this。因此 TaggedPointer
的对象调用reatin不会改变引用计数。这个函数里边有个 do{}while()
的循环,当 isa.bits
中的值被更新后则循环结束。我们一步一步看下do里边的逻辑。
if (slowpath(!newisa.nonpointer)) { ClearExclusive(&isa.bits); if (!tryRetain && sideTableLocked) sidetable_unlock(); if (tryRetain) return sidetable_tryRetain() ? (id)this : nil; else return sidetable_retain(); } 复制代码
这段逻辑主要处理当前类没有开启进行内存优化的情况。这里主要有两个函数 sidetable_tryRetain
和 sidetable_retain
。
bool objc_object::sidetable_tryRetain() { #if SUPPORT_NONPOINTER_ISA assert(!isa.nonpointer); #endif SideTable& table = SideTables()[this]; bool result = true; RefcountMap::iterator it = table.refcnts.find(this); if (it == table.refcnts.end()) { table.refcnts[this] = SIDE_TABLE_RC_ONE; } else if (it->second & SIDE_TABLE_DEALLOCATING) { result = false; } else if (! (it->second & SIDE_TABLE_RC_PINNED)) { it->second += SIDE_TABLE_RC_ONE; } return result; } id objc_object::sidetable_retain() { #if SUPPORT_NONPOINTER_ISA assert(!isa.nonpointer); #endif SideTable& table = SideTables()[this]; table.lock(); size_t& refcntStorage = table.refcnts[this]; if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) { refcntStorage += SIDE_TABLE_RC_ONE; } table.unlock(); return (id)this; } 复制代码
sidetable_tryRetain
函数主要做了这几件事,先从散列表中取出数值,如果这个数值找不到,就在Map添加 SIDE_TABLE_RC_ONE
值,如果这个数值所在的对象正在析构,那么将result置位false。最后检查下这个数字是否溢出,如果没有溢出则将引用计数+1;而 sidetable_retain
函数加了个自旋锁,同时逻辑更简单些。检查是否数值是否溢出,没有溢出则引用计数+1; 说完这两个函数,我们在回到 rootTryRetain()
函数。
if (slowpath(tryRetain && newisa.deallocating)) { ClearExclusive(&isa.bits); if (!tryRetain && sideTableLocked) sidetable_unlock(); return nil; } 复制代码
这里的逻辑判断对象是否在析构。如果在析构则会进行相关处理操作。这下来我们看看开启了指针优化后的 retain
逻辑
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); 复制代码
这行也是对引用计数+1的,是对其中的extra_rc进行+1
if (slowpath(carry)) { if (!handleOverflow) { ClearExclusive(&isa.bits); return rootRetain_overflow(tryRetain); } if (!tryRetain && !sideTableLocked) sidetable_lock(); sideTableLocked = true; transcribeToSideTable = true; newisa.extra_rc = RC_HALF; newisa.has_sidetable_rc = true; } 复制代码
这里判断是否溢出,如果溢出了就会进入到rootRetain_overflow函数里边,而rootRetain_overflow函数则又调用了rootRetain,只不过handleOverflow会传true,同时会处理溢出的情况,这时候 transcribeToSideTable
为true,在结束后就会调用 sidetable_addExtraRC_nolock(RC_HALF);
,我们来看下这个函数的实现。
bool objc_object::sidetable_addExtraRC_nolock(size_t delta_rc) { SideTable& table = SideTables()[this]; size_t& refcntStorage = table.refcnts[this]; size_t oldRefcnt = refcntStorage; if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true; uintptr_t carry; size_t newRefcnt = addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry); if (carry) { refcntStorage = SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK); return true; } else { refcntStorage = newRefcnt; return false; } } 复制代码
之前我们调用addc发现溢出后,我们把 newisa.extra_rc
置位 RC_HALF
,同时我们调用 sidetable_addExtraRC_nolock
同时把剩下的 RC_HALF
加入散列表中;也是通过addc进行操作。如果这是溢出则恢复散列表中的值,至此retain的逻辑差不多结束了。
release
看完 retain
源码,喘口气继续看看 release
是怎么实现的吧
- (oneway void)release { ((id)self)->rootRelease(); } ALWAYS_INLINE bool objc_object::rootRelease() { return rootRelease(true, false); } ALWAYS_INLINE bool objc_object::rootRelease(bool performDealloc, bool handleUnderflow) { if (isTaggedPointer()) return false; bool sideTableLocked = false; isa_t oldisa; isa_t newisa; retry: do { oldisa = LoadExclusive(&isa.bits); newisa = oldisa; if (slowpath(!newisa.nonpointer)) { ClearExclusive(&isa.bits); if (sideTableLocked) sidetable_unlock(); return sidetable_release(performDealloc); } uintptr_t carry; newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); if (slowpath(carry)) { goto underflow; } } while (slowpath(!StoreReleaseExclusive(&isa.bits, oldisa.bits, newisa.bits))); if (slowpath(sideTableLocked)) sidetable_unlock(); return false; underflow: newisa = oldisa; if (slowpath(newisa.has_sidetable_rc)) { if (!handleUnderflow) { ClearExclusive(&isa.bits); return rootRelease_underflow(performDealloc); } if (!sideTableLocked) { ClearExclusive(&isa.bits); sidetable_lock(); sideTableLocked = true; goto retry; } size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF); if (borrowed > 0) { newisa.extra_rc = borrowed - 1; bool stored = StoreReleaseExclusive(&isa.bits, oldisa.bits, newisa.bits); if (!stored) { isa_t oldisa2 = LoadExclusive(&isa.bits); isa_t newisa2 = oldisa2; if (newisa2.nonpointer) { uintptr_t overflow; newisa2.bits = addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow); if (!overflow) { stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits, newisa2.bits); } } } if (!stored) { sidetable_addExtraRC_nolock(borrowed); goto retry; } sidetable_unlock(); return false; } else { } } if (slowpath(newisa.deallocating)) { ClearExclusive(&isa.bits); if (sideTableLocked) sidetable_unlock(); return overrelease_error(); } newisa.deallocating = true; if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry; if (slowpath(sideTableLocked)) sidetable_unlock(); __sync_synchronize(); if (performDealloc) { ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc); } return true; } 复制代码
看完调用顺序后,我们着重分析下这个函数吧
objc_object::rootRelease(bool performDealloc, bool handleUnderflow) 复制代码
同样如果是 TaggedPointer
对象直接返回 false。我们先看 retry:
代码段 这里边的部分逻辑与 retain
相似,我们不一一分析。如果没有开启指针优化的话会有调用这样关键函数
uintptr_t objc_object::sidetable_release(bool performDealloc) { #if SUPPORT_NONPOINTER_ISA assert(!isa.nonpointer); #endif SideTable& table = SideTables()[this]; bool do_dealloc = false; table.lock(); RefcountMap::iterator it = table.refcnts.find(this); if (it == table.refcnts.end()) { do_dealloc = true; table.refcnts[this] = SIDE_TABLE_DEALLOCATING; } else if (it->second < SIDE_TABLE_DEALLOCATING) { do_dealloc = true; it->second |= SIDE_TABLE_DEALLOCATING; } else if (! (it->second & SIDE_TABLE_RC_PINNED)) { it->second -= SIDE_TABLE_RC_ONE; } table.unlock(); if (do_dealloc && performDealloc) { ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc); } return do_dealloc; } 复制代码
这里主要做了这几个逻辑,如果在散列表中没有找到对象,那么将其中的值置为 SIDE_TABLE_DEALLOCATING
。如果找到值比 SIDE_TABLE_DEALLOCATING
还小那么将it中 second
置位 SIDE_TABLE_DEALLOCATING
。如果找到的值不属于上面情况。那么检查是否溢出,没有溢出则引用计数-1;最后如果这个 do_dealloc
为true(这个链路里边的performDealloc为true)那么就给会给发送一个SEL_dealloc 的消息进行释放。分析完这个函数后我们继续回到 rootRelease
中,下面代码是开启了指针优化的情况,接下来会调用
uintptr_t carry; 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
命令后,我们可以看到
struct __AtAutoreleasePool { __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();} ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);} void * atautoreleasepoolobj; }; 复制代码
这样的结构体。初始化的时候会调用objc_autoreleasePoolPush()方法,~相当于OC中的delloc方法,他会调用objc_autoreleasePoolPop(atautoreleasepoolobj)方法,传入的参数就是我们刚刚通过objc_autoreleasePoolPush()生成的对象。关于 @autoreleasepool{}
的创建和释放逻辑我们看这两个函数就可以了。我们先从 objc_autoreleasePoolPush()
这个函数开始。
objc_autoreleasePoolPush(void) { return AutoreleasePoolPage::push(); } static inline void *push() { id *dest; if (DebugPoolAllocation) { dest = autoreleaseNewPage(POOL_BOUNDARY); } else { dest = autoreleaseFast(POOL_BOUNDARY); } assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY); return dest; } static inline id *autoreleaseFast(id obj) { AutoreleasePoolPage *page = hotPage(); if (page && !page->full()) { return page->add(obj); } else if (page) { return autoreleaseFullPage(obj, page); } else { return autoreleaseNoPage(obj); } } 复制代码
这里边会调用AutoreleasePoolPage类的push()方法,我们看一下AutoreleasePoolPage结构
class AutoreleasePoolPage { # define EMPTY_POOL_PLACEHOLDER ((id*)1) # define POOL_BOUNDARY nil static pthread_key_t const key = AUTORELEASE_POOL_KEY; static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasing static size_t const SIZE = #if PROTECT_AUTORELEASEPOOL PAGE_MAX_SIZE; // must be multiple of vm page size #else PAGE_MAX_SIZE; // size and alignment, power of 2 #endif static size_t const COUNT = SIZE / sizeof(id); magic_t const magic; id *next; pthread_t const thread; AutoreleasePoolPage * const parent; AutoreleasePoolPage *child; uint32_t const depth; uint32_t hiwat; } 复制代码
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;
用于保存线程。
AutoreleasePoolPage * const parent; AutoreleasePoolPage *child; uint32_t const depth; uint32_t hiwat; 复制代码
这几个属性都是跟双向链表有关系, parent
指向父节点, child
指向子节点。 depth
这个是层级, hiwat
这个应该栈里数据的数量。
分析完这个类的结构。我们继续看调用的流程。再调用到 static inline id *autoreleaseFast(id obj)
方法时,里边有三个分支走向。我们首先看下一个关键一行 AutoreleasePoolPage *page = hotPage();
这个 hotPage()
是通过TLS取当前的AutoreleasePoolPage的。如果是EMPTY_POOL_PLACEHOLDER的话直接返回nil,否则的话就会返回AutoreleasePoolPage,返回之前会做一个完整性检测。
if (page && !page->full()) { return page->add(obj); } else if (page) { return autoreleaseFullPage(obj, page); } else { return autoreleaseNoPage(obj); } 复制代码
这个判断也是比较简单的,如果当前不为nil,且没有满则直接调用add函数,添加obj。这个add函数也是比较简单入栈操作。只是在入栈的时候做了线程保护。当然我们根据宏是没有启用这个线程保护功能的。如果当前page已经满了,那么会调用 autoreleaseFullPage
方法。我们看下 autoreleaseFullPage
怎么实现的。
static __attribute__((noinline)) id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) { assert(page == hotPage()); assert(page->full() || DebugPoolAllocation); do { if (page->child) page = page->child; else page = new AutoreleasePoolPage(page); } while (page->full()); setHotPage(page); return page->add(obj); } 复制代码
这个方法的逻辑也没有复杂的地方。你遍历子节点直到找到没有满的page,如果最后都没有找到,那么就新建一个page,然后把这个page绑定到当前线程。同时调用add方法添加这个obj。然后我们再看下最后一个分支走向 autoreleaseNoPage(obj)
方法
static __attribute__((noinline)) id *autoreleaseNoPage(id obj) { assert(!hotPage()); bool pushExtraBoundary = false; if (haveEmptyPoolPlaceholder()) { pushExtraBoundary = true; } else if (obj != POOL_BOUNDARY && DebugMissingPools) { _objc_inform("MISSING POOLS: (%p) Object %p of class %s " "autoreleased with no pool in place - " "just leaking - break on " "objc_autoreleaseNoPool() to debug", pthread_self(), (void*)obj, object_getClassName(obj)); objc_autoreleaseNoPool(obj); return nil; } else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) { return setEmptyPoolPlaceholder(); } AutoreleasePoolPage *page = new AutoreleasePoolPage(nil); setHotPage(page); if (pushExtraBoundary) { page->add(POOL_BOUNDARY); } return page->add(obj); } 复制代码
相比于前几个方法这个方法逻辑就稍稍复杂了点。 bool pushExtraBoundary = false;
这个属性表示要不要像栈里边添加 POOL_BOUNDARY
,这个只有在栈为空的时候才会是 true
。第二个if判断主要是用debug相关,这里先不管。第三个判断,如果传的是一个 POOL_BOUNDARY
对象且没有调试alloc的时候,会将当前线程绑定一个EMPTY_POOL_PLACEHOLDER的占位对象,并返回。经过这些判断,我们走到了这里
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil); setHotPage(page); if (pushExtraBoundary) { page->add(POOL_BOUNDARY); } return page->add(obj); 复制代码
这里的代码比较简单,新建一个 AutoreleasePoolPage
对象,并且设置为hotpage,然后如果 pushExtraBoundary
为true,则把 POOL_BOUNDARY
入栈,然后把obj入栈。最后返回page对象。这里大家可能有疑问了,这里有条件的将 POOL_BOUNDARY
入栈,为不为导致底不是 POOL_BOUNDARY
,有这个疑问是很好的。可以我们看整个NSObject.mm的代码,可以看到不会出现栈底元素不是 POOL_BOUNDARY
的。至此,我们把 @autorelease{}
代码的新建逻辑分析完毕。下面我们来看释放逻辑。
void objc_autoreleasePoolPop(void *ctxt) { AutoreleasePoolPage::pop(ctxt); } static inline void pop(void *token) { AutoreleasePoolPage *page; id *stop; if (token == (void*)EMPTY_POOL_PLACEHOLDER) { if (hotPage()) { pop(coldPage()->begin()); } else { setHotPage(nil); } return; } page = pageForPointer(token); stop = (id *)token; if (*stop != POOL_BOUNDARY) { if (stop == page->begin() && !page->parent) { } else { return badPop(token); } } if (PrintPoolHiwat) printHiwat(); page->releaseUntil(stop); if (DebugPoolAllocation && page->empty()) { AutoreleasePoolPage *parent = page->parent; page->kill(); setHotPage(parent); } else if (DebugMissingPools && page->empty() && !page->parent) { page->kill(); setHotPage(nil); } else if (page->child) { if (page->lessThanHalfFull()) { page->child->kill(); } else if (page->child->child) { page->child->child->kill(); } } } 复制代码
看调用流程,我们着重分析下 pop(void *token)
方法,我们先看下段代码块的逻辑:
if (token == (void*)EMPTY_POOL_PLACEHOLDER) { if (hotPage()) { pop(coldPage()->begin()); } else { setHotPage(nil); } return; } 复制代码
这段逻辑主要判断如果pop的是一个 EMPTY_POOL_PLACEHOLDER
,这个就是我们之前空池占位。那么先判断是否存在hotpage,若果存在的话,那么将调用pop方法,同时传入当前hotpage的最初的父节点, coldPage()
返回的是第一个节点。如果不存在hotpage,那么将TLS绑定的值置位nil。我们继续看下面的代码块:
page = pageForPointer(token); stop = (id *)token; if (*stop != POOL_BOUNDARY) { if (stop == page->begin() && !page->parent) { } else { return badPop(token); } } 复制代码
page = pageForPointer(token);
这个函数根据传入的token获取page的首指针。获取到page后,下面检查一下token,通常下我们pop最终会传入一个page的beigin指针。这个通常应该是POOL_BOUNDARY,这里主要是做异常处理。接下来我们会走到这个函数
page->releaseUntil(stop); 复制代码
这个函数的实现如下:
void releaseUntil(id *stop) { while (this->next != stop) { AutoreleasePoolPage *page = hotPage(); while (page->empty()) { page = page->parent; setHotPage(page); } page->unprotect(); id obj = *--page->next; memset((void*)page->next, SCRIBBLE, sizeof(*page->next)); page->protect(); if (obj != POOL_BOUNDARY) { objc_release(obj); } } setHotPage(this); } 复制代码
这个函数的实现逻辑还是比较清楚的,他依次释放栈的内容直到遇到stop,并且把next指向的区域置为 SCRIBBLE
,然后把最近的栈为非空的置为当前的hotpage。最后我们看一下kill的相关逻辑
if (page->lessThanHalfFull()) { page->child->kill(); }else if (page->child->child) { page->child->child->kill(); } 复制代码
上面的判断逻辑主要是经过 releaseUntil
后,当前的page的栈已经被清空了,当前栈如果有子节点那么就释放子节点。最后我们看一下 kill
方法。
void kill() { AutoreleasePoolPage *page = this; while (page->child) page = page->child; AutoreleasePoolPage *deathptr; do { deathptr = page; page = page->parent; if (page) { page->unprotect(); page->child = nil; page->protect(); } delete deathptr; } while (deathptr != this); } 复制代码
这段逻辑就相当简单了,依次释放子节点。至此 @autorelease{}
就分析完毕了,关于 autorelease
方法这里就不再分析了, autorelease
逻辑基本上与我们上面分析的高度重合,这里不展开。
常见的容易造成泄漏的点
分析完源码后,我们知道iOS中的引用计数是怎么实现的,但这只是初步。内存管理难点不是在原理,而是在复杂的场景下怎么保证内存不泄漏,这才是最难的。我们先列举常见的容易造成泄漏的点:
循环引用
引用计数计数最大的缺点就是他无法解决循环引用的问题。如果出现循环引用了,需要我们手动打破循环引用。否则会一直占用内存。常见的循环引用情况主要是block。因为block会强引用外部变量,如果外部变量也在强引用这个block。那么他们就会造成循环引用。比如
HasBlock *hasBlock = [[HasBlock alloc] init]; [hasBlock setBlock:^{ hasBlock.name = @"abc"; }]; 复制代码
修改方法也很简单通过一个弱引用间接使用改造如下
HasBlock *hasBlock = [[HasBlock alloc] init]; __weak HasBlock* weakHasBlock = hasBlock; [hasBlock setBlock:^{ weakHasBlock.name = @"abc"; }]; 复制代码
这样就可以解决循环引用,这个是比较常见循环引用情况网上有很多宏解决这个问题。这里不展开。
使用单例的的一些情况
在使用单例的时候要注意,特别是单例含有block回调方法时候。有些单例会强持有这些block。这种情况虽然不是循环引用,但也是造成了喜欢引用。所以在使用单例的时候要清楚。如系统有些方法这样使用会造成无法释放:
- (void)viewDidLoad { [super viewDidLoad]; [[NSNotificationCenter defaultCenter] addObserverForName:@"boyce" object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) { self.name = @"boyce"; }]; } - (void)dealloc{ [[NSNotificationCenter defaultCenter] removeObserver:self]; } 复制代码
这里就造成了内存泄漏,这是因为NSNotificationCenter强引用了usingBlock,而usingBlock强引用了self,而NSNotificationCenter是个单例不会被释放,而self在被释放的时候才会去把自己从NSNotificationCenter中移除。类似的情况还有很多,比如一个数组中对象等等。这些内存泄漏不容易发现。
NSTimer
NSTimer会强引用传入的target,这时候如果加入NSRunLoop这个timer又会被NSRunLoop强引用
NSTimer *timer = [NSTimer timerWithTimeInterval:10 target:self selector:@selector(commentAnimation) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; 复制代码
解决这个方法主动stoptimer,至少是不能在dealloc中stoptimer的。另外可以设置一个中间类,把target变成中间类。
NSURLSession
这个问题和上面的NSTimer类似
NSURLSession *section = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[[NSOperationQueue alloc] init]]; NSURLSessionDataTask *task = [section dataTaskWithURL:[NSURL URLWithString:path] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { //Do something }]; [task resume]; 复制代码
这里NSURLSession会强引用了self。同时本地SSL会对一个NSURLSession缓存一段时间。所以即使没有强引用。也会造成内存泄漏。这里比较好的使用单例[NSURLSession sharedSession]
非OC对象的内存问题
在OC对象转换为非OC对象时候,要进行桥接。要把对象的控制权由ARC转换为程序员自己控制,这时候程序员要自己控制对象创建和释放。如下面的简单代码
NSString *name = @"boyce"; CFStringRef cfStringRef = (__bridge CFStringRef) name; CFRelease(cfStringRef); 复制代码
其他泄漏情况
如果present一个UINavigationController,如果返回的姿势不正确。会造成内存泄漏
UIViewController *vc = [[UIViewController alloc]init]; UINavigationController *nav = [[UINavigationController alloc]initWithRootViewController:vc]; [self presentViewController:nav animated:YES completion:NULL]; 复制代码
如果在UIViewController里边调用的是
[self dismissViewControllerAnimated:YES completion:NULL]; 复制代码
那么就会造成内存泄漏,这里边测试发现vc是没有被释放的。需要这样调用
if (self.navigationController.topViewController == self) { [self.navigationController dismissViewControllerAnimated:YES completion:nil]; } 复制代码
想说的
我认为内存管理的一些基本原理还是比较简单容易理解,难就难在结合复杂的场景,在一些复杂的场景下我们比较不容易发现内存泄漏的点。但是当我们把内存泄漏解决后你会发现,原来就是这么回事!!!
结束语
这部分就到此结束了,我们介绍了内存管理的原理,实现以及造成泄漏的常见场景。下篇介绍一些开源检测内存泄漏工具以及他们的实现。谢谢大家。
阅读博客还不过瘾?
欢迎大家扫二维码通过添加群助手,加入交流群,讨论和博客有关的技术问题,还可以和博主有更多互动
博客转载、线下活动及合作等问题请邮件至 shadowfly_zyl@hotmail.com 进行沟通
以上所述就是小编给大家介绍的《iOS内存管理的那些事儿-原理及实现》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。