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 進行溝通

相關文章
相關標籤/搜索