聊聊iOS和Mac OS中的AutoreleasePool

【原創(chuàng)博文亚隙,轉(zhuǎn)載請注明出處!】

寫在最前面:你們別光看啊违崇,發(fā)現(xiàn)我說的不對的地方請指出或留言阿弃,希望我們一起進步。

iOS程序的main()函數(shù)我們都很熟悉羞延,在函數(shù)入口處有一個自動釋放池autoreleasepool渣淳,今天我們從這里開始探究autoreleasepool究竟是何方神圣??

int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSLog(@"Hello, AutoreleasePool!");
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

借助clang轉(zhuǎn)換為C++代碼實現(xiàn)main.cpp文件,窺探一下這個main()函數(shù):

int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_301f30_mi_0);
        return UIApplicationMain(argc, argv, __null, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class"))));
    }
}

發(fā)現(xiàn)@autoreleasepool對應(yīng)的是一個__AtAutoreleasePool類型的變量__autoreleasepool伴箩;在main.cpp文件中找到__AtAutoreleasePool的定義如下:

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

可見__AtAutoreleasePool是一個C++結(jié)構(gòu)體入愧,在C++中結(jié)構(gòu)體類似我們iOS中的“類”這個概念,結(jié)構(gòu)體里面有兩個與結(jié)構(gòu)體同名的函數(shù)__AtAutoreleasePool()嗤谚、 ~__AtAutoreleasePool()分別稱之為構(gòu)造函數(shù)和析構(gòu)函數(shù)棺蛛,他們分別在結(jié)構(gòu)體創(chuàng)建和銷毀的時候調(diào)用,功能類似于iOS中的- (void)init - (void)dealloc巩步。
因此__autoreleasepool對象一創(chuàng)建就會調(diào)用objc_autoreleasePoolPush();旁赊,銷毀的時候則調(diào)用objc_autoreleasePoolPop(atautoreleasepoolobj);,知道了這些椅野,我們沿著objc_autoreleasePoolPush(); objc_autoreleasePoolPush();這兩個函數(shù)的實現(xiàn)繼續(xù)往下看就好了(從哪里看终畅?可以從蘋果開源的objc源碼中獲燃琛)。

我將源碼中相關(guān)調(diào)用函數(shù)摘抄如下(不摘抄怎么叫“讀源碼”呢??):

void *_objc_autoreleasePoolPush(void)
{
    return objc_autoreleasePoolPush();
}

void *objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

static inline void *push() 
    {
        id *dest;
        if (DebugPoolAllocation) {
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }

 id *autoreleaseNewPage(id obj)
 {
      AutoreleasePoolPage *page = hotPage();     
      if (page) return autoreleaseFullPage(obj, page);

      else return autoreleaseNoPage(obj);
 }

 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杖狼,從源碼中發(fā)現(xiàn)AutoreleasePoolPage有以下7個成員及一些內(nèi)部函數(shù)(已省略):

AutoreleasePoolPage.png
  • 每個AutoreleasePoolPage對象占用4096字節(jié)內(nèi)存,除了用來存放它內(nèi)部的成員變量术徊,剩下的空間用來存放autorelease對象的地址本刽。
  • 所有的AutoreleasePoolPage對象通過雙向鏈表的形式連接在一起。
AutoreleasePoolPage對象之間的關(guān)系網(wǎng).png

autoreleasePool以autoreleasePoolPage為單位進行展開的赠涮,因為每一個autoreleasePoolPage只有4096 byte子寓,如果自動釋放池中的對象太多就需要分頁(n個autoreleasePoolPage)存儲。當(dāng)開啟一個自動釋放池的時候笋除,執(zhí)行push()函數(shù)中autoreleaseNewPage(POOL_BOUNDARY);斜友,將一個POOL_BOUNDARY入棧自動釋放池并返回POOL_BOUNDARY在池中指針。(POOL_BOUNDARY是一個系統(tǒng)定義的宏垃它,其定義為# define POOL_BOUNDARY nil)鲜屏。自動釋放池結(jié)束的時候調(diào)用pop(void ctxt)函數(shù),同時傳入這個POOL_BOUNDARY地址国拇,這個時候會從最后一個進棧的對象開始發(fā)送release消息洛史,直到遇到這個POOL_BOUNDARY。自動釋放池內(nèi)部可以多層嵌套酱吝,形如下面這樣(demo運行環(huán)境為MRC*):

多級嵌套的自動釋放池.png

多層嵌套之后push()也會多次調(diào)用并將POOL_BOUNDARY壓入棧中也殖,每次返回一個新的存放POOL_BOUNDARY的指針,每一個push()都有與之對應(yīng)的pop(void *ctxt)函數(shù)务热。

多層@autoreleasepool之后忆嗜,自動釋放池中狀態(tài).png

多層嵌套的C++代碼:

多層嵌套的C++代碼.png

demo中選擇了4層@autoreleasepool,借助私有函數(shù)void _objc_autoreleasePoolPrint(void);可以獲取自動釋放池的狀態(tài)崎岂。我們看到自動釋放池中有7個待釋放的對象捆毫,其中__NSArray與__NSSetI是系統(tǒng)的,最后一個TestObject是我們自己創(chuàng)建的冲甘,中間四個POOL對象就是@autoreleasepool 4次绩卤,累計調(diào)用4次push()函數(shù),每次push()之后都會將一個POOL_BOUNDARY壓入池子中的對象江醇。

前面說過autoreleasePool以autoreleasePoolPage為單位進行展開的省艳,每個AutoreleasePoolPage對象占用4096字節(jié)內(nèi)存,如果自動釋放池中的對象總大小超過4096 byte嫁审,自動釋放池中的對象太多就需要分頁(n個autoreleasePoolPage)存儲,這里我臨時創(chuàng)建了700個對象赖晶,(700*8 byte = 5600 byte),自動釋放池的狀態(tài)如下:

code:
int main(int argc, char * argv[]) {
    @autoreleasepool {
        @autoreleasepool {
            NSLog(@"11111");
            @autoreleasepool {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-variable"
             
                for (int i = 0; i < 700; i++) {
                    
                    TestObject *testObj = [[[TestObject alloc] init] autorelease];
                }
#pragma clang diagnostic pop
                _objc_autoreleasePoolPrint();
            }
            NSLog(@"22222");
        }
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

pool state

objc[5438]: 705 releases pending.
objc[5438]: [0x7f8a1e003000]  ................  PAGE (full)  (cold)
objc[5438]: [0x7f8a1e003038]    0x6000009d1a80  __NSArrayI
objc[5438]: [0x7f8a1e003040]    0x600003ffa8f0  __NSSetI
objc[5438]: [0x7f8a1e003048]  ################  POOL 0x7f8a1e003048
objc[5438]: [0x7f8a1e003050]  ################  POOL 0x7f8a1e003050
objc[5438]: [0x7f8a1e003058]  ################  POOL 0x7f8a1e003058
objc[5438]: [0x7f8a1e003060]    0x600001ee4120  TestObject
objc[5438]: [0x7f8a1e003068]    0x600001ee4160  TestObject
.
.
.
objc[5438]: [0x7f8a1e003ff0]    0x600001ee6010  TestObject
objc[5438]: [0x7f8a1e003ff8]    0x600001ee6020  TestObject
objc[5438]: [0x7f8a1e001000]  ................  PAGE  (hot) 
objc[5438]: [0x7f8a1e001038]    0x600001ee6030  TestObject
objc[5438]: [0x7f8a1e001040]    0x600001ee6040  TestObject
.
.
objc[5438]: [0x7f8a1e001670]    0x600001ee6ca0  TestObject
objc[5438]: ##############

700個OC對象指針大小超過了一個PAGE的承載量律适,因此系統(tǒng)又重啟了一個新的PAGE辐烂,兩個PAGE 地址及狀態(tài)分別為:
objc[5438]: [0x7f8a1e003000] ................ PAGE (full) (cold)
objc[5438]: [0x7f8a1e001000] ................ PAGE (hot)
第一個PAGE標(biāo)記為full、cold捂贿,“full”表示該PAGE空間已滿纠修,“cold”表示該PAGE當(dāng)前不活躍(可以理解為存滿了,其他的自動釋放對象暫時不會去訪問該PAGE)厂僧;
第二個PAGE標(biāo)記hot扣草,表明當(dāng)前正處在使用狀態(tài),因為還沒有放滿颜屠。
再細看一下PAGE的內(nèi)存地址辰妙,PAGE起始地址0x7f8a1e001000,存放的第一個TestObject地址0x7f8a1e001038甫窟,地址相差56 byte密浑,根據(jù)前面AutoreleasePoolPage這個類的結(jié)構(gòu),里面有7個成員對象粗井, 7*8 byte = 56 byte尔破,匹配。因此對于一個AutoreleasePoolPage而言浇衬,總大小為4096 byte懒构,前56 byte存放AutoreleasePoolPage自己的成員指針,后面的內(nèi)存存儲 autorelease 對象指針耘擂。

既然池子中的對象在池子結(jié)束的時候就會被釋放胆剧,那對于iOS應(yīng)用而言,只要程序不異常退出梳星,因為RunLoop赞赖,main函數(shù)就會一直在運行,@autoreleasepool也就不會結(jié)束冤灾,那程序中的那么多函數(shù)中局部變量怎么釋放的呢前域?顯然不是依賴于main()中的 @autoreleasepool

那autorelease對象在什么時機會被調(diào)用release?
這個分情況看:

  • 如果是@autoreleasepool中的autorelease對象韵吨,那等autoreleasepool結(jié)束的時候就會調(diào)用對象的release方法匿垄,通過上面demo的結(jié)果,這毫無疑問了归粉。
  • 那對于程序中局部函數(shù)內(nèi)部的autorelease對象什么時候被調(diào)用release呢椿疗?
    對于第二個問題我之前錯誤地以為局部函數(shù)一旦調(diào)用結(jié)束,局部autorelease對象就立馬被釋放了糠悼。事實并非如此届榄,我用一張圖表示一下:
autorelease對象被dealloc時序.png

現(xiàn)象是viewDidLoad中的autorelease對象testObj并沒有在viewDidLoad調(diào)用結(jié)束就立馬釋放,而是在viewWillAppear:viewDidAppear:之間被釋放倔喂,這取決于RunLoop铝条。
實際上iOS在主線程的Runloop中注冊了2個Observer靖苇,第1個Observer監(jiān)聽了kCFRunLoopEntry事件,會調(diào)用objc_autoreleasePoolPush()班缰;
第2個Observer監(jiān)聽了kCFRunLoopBeforeWaiting與kCFRunLoopBeforeExit事件贤壁。kCFRunLoopBeforeWaiting的時候會調(diào)用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()埠忘;
kCFRunLoopBeforeExit的時候會調(diào)用objc_autoreleasePoolPop()脾拆。
因此程序在即將休眠的時候會集中處理待release的局部對象。

那在ARC下莹妒,函數(shù)中的局部對象又是在什么時機被dealloc的呢名船?
ARC下局部函數(shù)中局部對象dealloc時機.png

發(fā)現(xiàn)其局部對象確實是馬上釋放了。ARC下局部對象dealloc時機的表現(xiàn)與MRC下autorelease對象不一致动羽,既然剛才已經(jīng)驗證autorelease對象被dealloc的時機依賴于RunLoop包帚,那從“ARC下局部對象的銷毀與MRC下表現(xiàn)不一致的結(jié)果”猜測編譯器并不是在創(chuàng)建對象的時候加了autoRelease方法,而應(yīng)該是在要出局部函數(shù)之前直接調(diào)用了對象的release方法运吓。

ARC為我們做了什么渴邦?(LLVM + Runtime相互協(xié)作的結(jié)果)
在ARC環(huán)境下,系統(tǒng)利用LLVM編譯器自動為我們給對象添加retain拘哨、release谋梭、autorelease等方法。
像弱引用對象這樣的存在是需要運行時runtime倦青,runtime檢測到弱引用對象銷毀的時候瓮床,將弱引用對象清空掉。

看到這里产镐,點個贊唄??.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末隘庄,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子癣亚,更是在濱河造成了極大的恐慌丑掺,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件述雾,死亡現(xiàn)場離奇詭異街州,居然都是意外死亡,警方通過查閱死者的電腦和手機玻孟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門唆缴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人黍翎,你說我怎么就攤上這事面徽。” “怎么了匣掸?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵斗忌,是天一觀的道長质礼。 經(jīng)常有香客問我,道長织阳,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任砰粹,我火速辦了婚禮唧躲,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘碱璃。我一直安慰自己弄痹,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布嵌器。 她就那樣靜靜地躺著肛真,像睡著了一般。 火紅的嫁衣襯著肌膚如雪爽航。 梳的紋絲不亂的頭發(fā)上蚓让,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天,我揣著相機與錄音讥珍,去河邊找鬼历极。 笑死,一個胖子當(dāng)著我的面吹牛衷佃,可吹牛的內(nèi)容都是我干的趟卸。 我是一名探鬼主播,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼氏义,長吁一口氣:“原來是場噩夢啊……” “哼锄列!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起惯悠,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤邻邮,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后吮螺,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體饶囚,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年鸠补,在試婚紗的時候發(fā)現(xiàn)自己被綠了萝风。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡紫岩,死狀恐怖规惰,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情泉蝌,我是刑警寧澤歇万,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布揩晴,位于F島的核電站,受9級特大地震影響贪磺,放射性物質(zhì)發(fā)生泄漏硫兰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一寒锚、第九天 我趴在偏房一處隱蔽的房頂上張望劫映。 院中可真熱鬧,春花似錦刹前、人聲如沸泳赋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽祖今。三九已至,卻和暖如春拣技,著一層夾襖步出監(jiān)牢的瞬間千诬,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工过咬, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留大渤,地道東北人。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓掸绞,卻偏偏與公主長得像泵三,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子衔掸,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容