作者简介
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)技术的发展。现在已经很少看到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把源码现在下来看,具体命令如下所示
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()方法,~__AtAutoreleasePool() 是c++结构体中的析构方法,类似于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];
self.obser = [[NSNotificationCenter defaultCenter] addObserverForName:@"boyce" object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
self.name = @"boyce";
}];
}
- (void)dealloc{
[[NSNotificationCenter defaultCenter] removeObserver:self.obser];
}
复制代码
这里就造成了内存泄漏,这是因为NSNotificationCenter强引用了usingBlock,而usingBlock强引用了self,而NSNotificationCenter是个单例不会被释放,而self在被释放的时候才会去把self.obser从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 进行沟通