第二十二篇:autoreleasepool自動釋放池

autoreleasepool前言

下面是我們自己創(chuàng)建了一個autoreleasepool,創(chuàng)建的對象有自動添加到自動釋放池,和手動添加到釋放池的。當(dāng)我們自己創(chuàng)建了autoreleasepool然后把對象添加進(jìn)去,這個就是手動添加到釋放池。每個autoreleasepool都會對應(yīng)一個runloop。如果手動添加到autoreleasepool里這個就和runloop沒有關(guān)系了,出了autoreleasepool作用域就會被釋放。

WechatIMG9.jpeg

通過下圖的寫法發(fā)現(xiàn)內(nèi)存會暴增

WechatIMG10.jpeg

通過下圖的寫法發(fā)現(xiàn)內(nèi)存不會暴增,因為我們在其位置上添加了autoreleasepool,其出了這個autoreleasepool就會被釋放掉。

WechatIMG11.jpeg

AutoreleasePool底層:

    首先我們寫個使用@autoreleasepool的釋放池,代碼如下,然后進(jìn)行clang命令進(jìn)行一個操作。
int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
    }
    
    return 0;
}

clang后的代碼,在這里點擊__AtAutoreleasePool進(jìn)去發(fā)現(xiàn)其是一個struct的結(jié)構(gòu)體。

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_v4jdthx95753k1gbfyy30w0w0000gn_T_main_cf4d78_mi_0);
    }
    return 0;
}

__AtAutoreleasePool構(gòu)造函數(shù)和析構(gòu)函數(shù)。

struct __AtAutoreleasePool {
 構(gòu)造函數(shù) __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
析構(gòu)函數(shù)  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

通過上面其實AutoreleasePool底層代碼可以寫成這幾個:
objc_autoreleasePoolPush()
NSLog(@"Hello, World!")
objc_autoreleasePoolPop(atautoreleasepoolobj)

我們通過匯編調(diào)試也可以看到AutoreleasePool其實其是走objc_autoreleasePoolPush和objc_autoreleasePoolPop方法


WechatIMG91.jpeg

objc_autoreleasePoolPush方法

下面是objc_autoreleasePoolPush執(zhí)行的代碼,其中::符號是一個命名空間,就是說明AutoreleasePoolPage去調(diào)用push()方法
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}

下面是官方對自動釋放池的介紹:

 Autorelease pool implementation

   A thread's autorelease pool is a stack of pointers. 
   Each pointer is either an object to release, or POOL_BOUNDARY which is 
     an autorelease pool boundary.
   A pool token is a pointer to the POOL_BOUNDARY for that pool. When 
     the pool is popped, every object hotter than the sentinel is released.
   The stack is divided into a doubly-linked list of pages. Pages are added 
     and deleted as necessary. 
   Thread-local storage points to the hot page, where newly autoreleased 
     objects are stored. 

A thread's autorelease pool is a stack of pointers.
線程的?動釋放池是?個指針堆棧。(說明?動釋放池肯定和線程是有關(guān)聯(lián)的)
Each pointer is either an object to release, or POOL_BOUNDARY which is an
autorelease pool boundary.
每個指針要么是將要釋放的對象,要么是?個POOL_BOUNDARY,作為?動釋放池的邊界。
(?動釋放池??的數(shù)據(jù)有倆種類型,?種是將要被釋放的對象,另?種是
POOL_BOUNDARY,超出POOL_BOUNDARY,就相當(dāng)于不在當(dāng)前釋放池的范圍之內(nèi)了。)
A pool token is a pointer to the POOL_BOUNDARY for that pool. When the pool is
popped, every object hotter than the sentinel is released.
有?個指向該池的 POOL_BOUNDARY 的指針。當(dāng)池被彈出時,每個?哨兵更熱的對象都會被
釋放。(在調(diào)?pop函數(shù)的時候,整個?動釋放池??的對象都會被釋放。)
The stack is divided into a doubly-linked list of pages. Pages are added and
deleted as necessary.
棧被分成?個雙向鏈接的??列表。根據(jù)需要添加和刪除??。(?動釋放池是?個雙向列表
的棧結(jié)構(gòu)。它的每?個節(jié)點都是AutoreleasePoolPage。)
Thread-local storage points to the hot page, where newly autoreleased objects
are stored.
線程本地存儲指向熱?,其中存儲了新的?動釋放對象。(?動釋放池??的數(shù)據(jù)也有線程緩
存)

下面是AutoreleasePoolPage里的data數(shù)據(jù),其內(nèi)部里是存儲一些數(shù)據(jù)。

class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
    struct AutoreleasePoolEntry {
        uintptr_t ptr: 48;
        uintptr_t count: 16;

        static const uintptr_t maxCount = 65535; // 2^16 - 1
    };
    static_assert((AutoreleasePoolEntry){ .ptr = MACH_VM_MAX_ADDRESS }.ptr == MACH_VM_MAX_ADDRESS, "MACH_VM_MAX_ADDRESS doesn't fit into AutoreleasePoolEntry::ptr!");
#endif

    magic_t const magic;
    __unsafe_unretained id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;

    AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
        : magic(), next(_next), thread(_thread),
          parent(_parent), child(nil),
          depth(_depth), hiwat(_hiwat)
    {
    }
};

下面是AutoreleasePoolPageData里的成員變量意思:

magic 用來校驗 AutoreleasePoolPage 的結(jié)構(gòu)是否完整;
next 指向最新添加的 autoreleased 對象的下一個位置,初始化時指向 begin() ;
thread 指向當(dāng)前線程;
parent 指向父結(jié)點,第一個結(jié)點的 parent 值為 nil ;
child 指向子結(jié)點,最后一個結(jié)點的 child 值為 nil ;
depth 代表深度,從 0 開始,往后遞增 1;
hiwat 代表 high water mark 最大入棧數(shù)量標(biāo)記

    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);
        }
    }


 static inline AutoreleasePoolPage *hotPage() 
    {
        AutoreleasePoolPage *result = (AutoreleasePoolPage *)
            tls_get_direct(key);
        if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
        if (result) result->fastcheck();
        return result;
    }

下面的是POOL_BOUNDARY也就是一個哨兵(?動釋放池的邊界)代碼。

    bool pushExtraBoundary = false;
        if (haveEmptyPoolPlaceholder()) {
            // We are pushing a second pool over the empty placeholder pool
            // or pushing the first object into the empty placeholder pool.
            // Before doing that, push a pool boundary on behalf of the pool 
            // that is currently represented by the empty placeholder.
            pushExtraBoundary = true;
        }
        else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
            // We are pushing an object with no pool in place, 
            // and no-pool debugging was requested by environment.
            _objc_inform("MISSING POOLS: (%p) Object %p of class %s "
                         "autoreleased with no pool in place - "
                         "just leaking - break on "
                         "objc_autoreleaseNoPool() to debug", 
                         objc_thread_self(), (void*)obj, object_getClassName(obj));
            objc_autoreleaseNoPool(obj);
            return nil;
        }
        else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
            // We are pushing a pool with no pool in place,
            // and alloc-per-pool debugging was not requested.
            // Install and return the empty pool placeholder.
            return setEmptyPoolPlaceholder();
        }

當(dāng)?shù)谝淮芜M(jìn)來時候,會進(jìn)行AutoreleasePoolPage進(jìn)行創(chuàng)建,然后把其加入到hotPage里,hotPage也就是我們進(jìn)行設(shè)置的頁。 page->add(POOL_BOUNDARY)就是把哨兵對象添加到page里。

    // Install the first page.
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        setHotPage(page);
        
        // Push a boundary on behalf of the previously-placeholder'd pool.
        if (pushExtraBoundary) {
            page->add(POOL_BOUNDARY);
        }
        
        // Push the requested object or pool.
        return page->add(obj);

當(dāng)AutoreleasePoolPage不是第一次創(chuàng)建時候就會走下面的autoreleaseFast方法進(jìn)行創(chuàng)建。也是通過hotPage進(jìn)行創(chuàng)建,然后判斷page是否滿了,如果沒有滿了的話就進(jìn)行一個添加到page里。

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);
      }
  }

AutoreleasePoolPageData使用

  下面我們看到AutoreleasePoolPage其實就是調(diào)用AutoreleasePoolPageData里的數(shù)據(jù),在AutoreleasePoolPageData里有個begin()參數(shù),這個是begin()點進(jìn)去其實是一個內(nèi)存平移的操作。
    AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
        AutoreleasePoolPageData(begin(),
                                objc_thread_self(),
                                newParent,
                                newParent ? 1+newParent->depth : 0,
                                newParent ? newParent->hiwat : 0)

    id * begin() {
        return (id *) ((uint8_t *)this+sizeof(*this));
    }

直接在autoreleasepool里創(chuàng)建p對象是不會進(jìn)入到autoreleasepool進(jìn)行管理的,其是通過ARC進(jìn)行控制其釋放和銷毀。通過alloc,new ,copy,mutablecopy,allocwith這些創(chuàng)建的對象在autoreleasepool里是不會放到autoreleasepool里進(jìn)行管理的。 那如果我們要添加到自動釋放池,需要在對象前面加__autoreleasing,這樣就會受autoreleasepool釋放池進(jìn)行管理( __autoreleasing HPWPerson *p = [ [HPWPerson alloc] init])。其中我們也可以通過系統(tǒng)提供的_objc_autoreleasePoolPrint()進(jìn)行打印釋放池里的對象。如果是通過stringwithformat這類創(chuàng)建的話就會自動添加到autoreleasepool里無需在代碼前加入__autoreleasing,其中需要注意stringwithformat后面的string要大于9位數(shù),如果小于等于9位數(shù),那么這個就是taggerPoint小對象了,這個是不會添加到autoreleasepool里的。我們可以通過_objc_autoreleasePoolPrint函數(shù)進(jìn)行打印驗證。

int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        // insert code here...
        HPWPerson *p =  [ [HPWPerson alloc] init];
    }
    
    return 0;
}

下面是通過_objc_autoreleasePoolPrint的打印,發(fā)現(xiàn)其值添加到page里了,其中用的是__autoreleasing進(jìn)行修飾。


WechatIMG125.jpeg

下面是沒有__autoreleasing進(jìn)行修飾,發(fā)現(xiàn)其沒有加入到autoreleasepool里


WechatIMG126.jpeg

其實添加__autoreleasing這個也就是在底層操作調(diào)用obje_autorelease,在其里面最終調(diào)用的是autroreleaseFast這個函數(shù)。autroreleaseFast這個函數(shù)我們之前說過,其會調(diào)用創(chuàng)建page然后進(jìn)行一個添加操作,也就是添加到autoreleasepool里。

思考1:當(dāng)我們把下面紅圈里的代碼放到我們手寫的自動釋放池autoreleasepool外,想想這句代碼會被放到自動釋放遲里嗎?

WechatIMG859.jpeg

我們打印后發(fā)現(xiàn)其String代碼還是被加到了自動釋放遲中,這個是為什么呢,其實這個是和runloop有關(guān)系,當(dāng)我們程序運行的時候,其就會有開啟一個runloop,這個runloop會自動的幫我們創(chuàng)建自動釋放池。


WechatIMG860.jpeg

下面是runloop運行的示意圖,當(dāng)APP啟動的時候,主線程的runloop是自動開啟的,在循環(huán)的時候,其會注冊兩個監(jiān)聽,一個監(jiān)聽是當(dāng)我們runloop即將進(jìn)入的時候(也或是從休眠狀態(tài)進(jìn)入的時候),就會 創(chuàng)建一個自動釋放池,這個優(yōu)先級最高。當(dāng)要休眠的時候,其也會有一個監(jiān)聽的函數(shù),監(jiān)聽runloop休眠的時候,也就是等runloop里任務(wù)完成后,也就是一次循環(huán)結(jié)束之前就會傾倒里面的釋放池,銷毀里面的對象和變量,這個優(yōu)先級別是最低的,這個是因為其要等runloop里其他事件都完成后才進(jìn)行此操作。


WechatIMG861.jpeg

通過上面的解釋也就知道了為什么我們寫在主線程里自己創(chuàng)建的autoreleasepool外面的程序也會被添加到自動釋放池里。

上面我們是寫在主線程中的,那如果我們寫在子線程中,其會被加入到autoreleasepool中嗎?下面我們看一個官方文檔的介紹:

WechatIMG862.jpeg

上面也就是說,當(dāng)放在子線程中時候,其也會放到子線程的autoreleasepool中,和主線程中的一致。

總結(jié):子線程的autoreleasepool什么時候銷毀,其是在子線程銷毀的時候會釋放其autoreleasepool,這個是在子線程中沒有開啟runloop的時候。 如果子線程開啟了runloop也就和我們之前說的主線程那個一樣了。

autoreleasepool對象在ARC中什么時候釋放總結(jié)分兩種情況

1.手動添加自動釋放池里的對象,對象是出了autoreleasepool作用域就會釋放
2.系統(tǒng)自動添加到釋放池的autoreleasepool對象,這種情況下也要分兩種,
1)如果在主線程中或者是在子線程中開啟了runloop那就在runloop即將休眠的時候進(jìn)行釋放掉(如界面不動時候runloop也就會休眠)
2)如果在子線程中沒有開啟runloop,那就在子線程銷毀的時候被釋放。

下面我們探討下一個hotPage的大小

一個hotPage的大小是4k,如下圖1左移動12位就位4096,也就是4k


WechatIMG863.jpeg

那我們?nèi)绾悟炞C呢,4096 - 56(本身大?。? 8(哨兵對象) 為 4032。 4032/8 一個hotpage可存504個對象。下面就是對應(yīng)的驗證,我們創(chuàng)建505個對象,就會發(fā)現(xiàn)在505的地方會再開啟一個新page,當(dāng)前的page后面會顯示hot,而前面放了504個對象的page就會顯示cold。如果我們把505改成1009,大家想想會存多少,和開啟幾個page,其實會開啟兩個hotpage,第一個存504個對象,第二個存505個對象,這個是因為一個autorelease只有一個哨兵對象(占8字節(jié)),所以第二個hotpage就可以存505個對象。

WechatIMG864.jpeg

上面這些操作都是自動釋放池push操作,那自動釋放池pop操作又是做了些啥呢,我們下面看到再pop時候傳了一個參數(shù),其實這個參數(shù)就是之前push進(jìn)來的哨兵對象。

WechatIMG865.jpeg

下面的token就是傳進(jìn)來的哨兵對象,哨兵對象是放在第一位的,通過這個哨兵對象找到哨兵對象所在的page。

8671669474582_.pic.jpg
8661669474556_.pic.jpg

下面的popPage里releaseUntil是進(jìn)行釋放,釋放到哨兵對象為止,會從最后面的hot頁往前面找,找到哨兵對象所在的page為止,然后調(diào)用objc_release(obj)進(jìn)行釋放。

8681669475204_.pic.jpg

那自動釋放池我們可以用來做什么呢,可以防止內(nèi)存短時間內(nèi)爆漲

其是一個內(nèi)存自動回收機制,如下,當(dāng)我們把autoreleasepool去掉的時候,其內(nèi)存會爆漲。


8691669475895_.pic.jpg

當(dāng)我們一同autoreleasepool去包裹的時候,其內(nèi)存變化就會很小,因為得到了及時的釋放


8701669475977_.pic.jpg
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容