阅读 1890

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

作者简介

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时代,程序员要手动管理引用计数,通常要遵循一下几个原则

  • 开头为allocnewcopymutableCopy的方法创建的对象,引用计数都会被+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方法中给对象分配了相应的内存。而初始化则调用了initInstanceIsainitIsa两个方法。而 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_tryRetainsidetable_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 进行沟通

关注下面的标签,发现更多相似文章
评论

查看更多 >