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

做者简介html

boyce,饿了么物流团队资深iOS开发。曾在格瓦拉等公司从事iOS相关研发工做。c++

注:本篇文章是《iOS内存管理的那些事儿》系列文章的第一部分。稍后咱们会持续更新第二部分(开源监测内存泄漏的实现)和第三部分(如何利用开源工具作相关的APM),感兴趣的童鞋能够关注咱们专栏并获取实时推送信息哦~程序员

为何要写这篇文章

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

为何要进行内存管理

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

程序内存空间布局

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

常见的内存管理技术

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

  • 标记清除算法session

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

  • 标记复制算法less

    标记复制算法就是把活动对象复制到新的空间,而后把旧的控件所有释放掉。这个算法不会像清除算法同样产生大量的碎片,由于他是一次把就有空间释放掉,所以吞吐量比较大。速度较快。他缺点也很明显,算法使用可能会用到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()方法,~__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 进行沟通

相关文章
相关标签/搜索