Autorelease

引用計數(shù)

Objective-C內(nèi)存管理中,每個對象都有屬于自己的計數(shù)器捣染;如果想讓某個對象繼續(xù)存活,就增加它的引用計數(shù)停巷;當用完它之后耍攘,就減少該計數(shù);當沒人引用該對象畔勤,它的計數(shù)變?yōu)?之后蕾各,系統(tǒng)就把它銷毀。

所以庆揪,在objective-C的內(nèi)存管理中式曲,關(guān)鍵就在于對象釋放的時機,autorelease的妙處在于缸榛,它找到了一個合適的時機來釋放返回對象吝羞,這個時機就是本次消息循環(huán)結(jié)束的時候。我們只需要在返回對象前内颗,調(diào)用autorelease钧排,對象被加入autorelease pool(但沒有減少對象的引用計數(shù),所以這時候返回的對象仍是有效的)起暮,然后返回卖氨,程序繼續(xù)執(zhí)行,直到完成本次消息循環(huán)之時负懦,再把autorelease pool中記錄的臨時對象一個個分別release--減少引用計數(shù)筒捺。

AutoreleasePool

App啟動后,蘋果在主線程RunLoop里注冊了兩個Observer纸厉,其回調(diào)都是_wrapRunLoopWithAutoreleasePoolHandler()系吭。

第一個Observer監(jiān)視的事件是Entry(即將進入Loop),其回調(diào)內(nèi)會調(diào)用_objc_autoreleasePoolPush()創(chuàng)建自動釋放池颗品。其order是-2147483647肯尺,優(yōu)先級最高沃缘,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前。

第二個Observer監(jiān)視了兩個事件: BeforeWaiting(準備進入休眠) 時調(diào)用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush()釋放舊的池并創(chuàng)建新池则吟;Exit(即將退出Loop) 時調(diào)用_objc_autoreleasePoolPop()來釋放自動釋放池槐臀。這個Observerorder是 2147483647,優(yōu)先級最低氓仲,保證其釋放池子發(fā)生在其他所有回調(diào)之后水慨。

在主線程執(zhí)行的代碼,通常是寫在諸如事件回調(diào)敬扛、Timer回調(diào)內(nèi)的晰洒。這些回調(diào)會被RunLoop創(chuàng)建好的AutoreleasePool環(huán)繞著,所以不會出現(xiàn)內(nèi)存泄漏啥箭,開發(fā)者也不必顯示創(chuàng)建Pool了谍珊。

Autorelease原理

@autoreleasepool{ }

使用clang編譯器把main.m轉(zhuǎn)化成main.cpp文件,查看一下@autoreleasepool{ }C++源碼急侥,可以看到@autoreleasepool{ }其實很簡單砌滞,是一個__AtAutoreleasePool結(jié)構(gòu)體,這個結(jié)構(gòu)體的構(gòu)造也很簡單缆巧,一個構(gòu)造函數(shù)和一個析構(gòu)函數(shù)布持,構(gòu)造函數(shù)中通過調(diào)用objc_autoreleasePoolPush()函數(shù)來實現(xiàn)構(gòu)造豌拙;析構(gòu)函數(shù)通過調(diào)用objc_autoreleasePoolPop()函數(shù)來實現(xiàn)析構(gòu)陕悬。
OC代碼:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSObject *obj = [[NSObject alloc]init];
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

C++代碼:

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

int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
        return UIApplicationMain(argc, argv, __null, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class"))));
    }
}

AutoreleasePoolPage

objc_autoreleasePoolPush()objc_autoreleasePoolPop()的函數(shù)實現(xiàn)我們可以在蘋果開源代碼objc4NSObject.mm中找到,其實就是直接對AutoreleasePoolPage的調(diào)用按傅。

push

  • AutoreleasePool并沒有單獨的結(jié)構(gòu)捉超,而是由若干個AutoreleasePoolPage以雙向鏈表的形式組合而成(分別對應(yīng)結(jié)構(gòu)中的parent指針和child指針)
  • AutoreleasePool是按線程一一對應(yīng)的(結(jié)構(gòu)中的thread指針指向當前線程)
  • AutoreleasePoolPage每個對象會開辟4096字節(jié)內(nèi)存(也就是虛擬內(nèi)存一頁的大小)唯绍,除了上面的實例變量所占空間拼岳,剩下的空間全部用來儲存autorelease對象的地址
  • 上面的id *next指針作為游標指向棧頂最新add進來的autorelease對象的下一個位置
  • 一個AutoreleasePoolPage的空間被占滿時,會新建一個AutoreleasePoolPage對象况芒,連接鏈表惜纸,后來的autorelease對象在新的page加入
class AutoreleasePoolPage 
{
    magic_t const magic;        // 用來校驗 AutoreleasePoolPage 的結(jié)構(gòu)是否完整
    id *next;                   // 指向棧頂,也就是最新入棧的autorelease對象的下一個位置
    pthread_t const thread;     // 指向當前線程
    AutoreleasePoolPage * const parent; // 指向父節(jié)點
    AutoreleasePoolPage *child; // 指向子節(jié)點
    uint32_t const depth;       // 表示鏈表的深度绝骚,也就是鏈表節(jié)點的個數(shù)
    uint32_t hiwat;             // 表示high water mark(最高水位標記)

    static inline void *push() {
        id *dest;
        if (DebugPoolAllocation) { // Debug模式
            // 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;
    }

    static inline id *autoreleaseFast(id obj)  {
        AutoreleasePoolPage *page = hotPage();  // 獲取當前PoolPage
        if (page && !page->full()) {            // 當前Page存在且未滿
            return page->add(obj);              // 把obj添加到當前Page中
        } else if (page) {                      // 當前Page已滿
            return autoreleaseFullPage(obj, page);
        } else {                                // 不存在PoolPage
            return autoreleaseNoPage(obj);
        }
    }
}

pop

每當進行一次objc_autoreleasePoolPush調(diào)用時耐版,runtime向當前的AutoreleasePoolPageadd進一個哨兵對象,值為0(也就是個nil)压汪,那么這一個page就變成了下面的樣子:

AutoReleasePool

objc_autoreleasePoolPush的返回值正是這個哨兵對象的地址粪牲,被objc_autoreleasePoolPop(哨兵對象)作為入?yún)ⅲ谑牵?/p>

  1. 根據(jù)傳入的哨兵對象地址找到哨兵對象所處的page
  2. 在當前page中止剖,將晚于哨兵對象插入的所有autorelease對象都發(fā)送一次- release消息腺阳,并向回移動next指針到正確位置
  3. 補充2:從最新加入的對象一直向前清理落君,可以向前跨越若干個page,直到哨兵所在的page
AutoReleasePool pop后
static inline void pop(void *token) 
    {
        AutoreleasePoolPage *page;
        id *stop;

        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {  // 是否占位池
            // Popping the top-level placeholder pool.
            // 彈出頂層占位池
            if (hotPage()) {
                // Pool was used. Pop its contents normally.
                // Pool pages remain allocated for re-use as usual.
                // 使用了游泳池亭引, 正常彈出內(nèi)容
                // 池仍像往常一樣分配給重用
                pop(coldPage()->begin());
            } else {
                // Pool was never used. Clear the placeholder.
                // 游泳池從未使用 清除占位符
                setHotPage(nil);
            }
            return;
        }

        page = pageForPointer(token); // 通過哨兵地址獲取哨兵對象所在page對象
        stop = (id *)token;
        if (*stop != POOL_BOUNDARY) {
            if (stop == page->begin()  &&  !page->parent) {
                // Start of coldest page may correctly not be POOL_BOUNDARY:
                // 1. top-level pool is popped, leaving the cold page in place
                // 2. an object is autoreleased with no pool
            } else {
                // Error. For bincompat purposes this is not 
                // fatal in executables built with old SDKs.
                return badPop(token);
            }
        }

        if (PrintPoolHiwat) printHiwat(); // 記錄最高水位標記

        page->releaseUntil(stop);  // 把哨兵對象之后入棧的對象進行出棧操作绎速,并對出棧對象發(fā)出release消息


        // memory: delete empty children
        // // 刪除空掉的page
        if (DebugPoolAllocation  &&  page->empty()) {
            // special case: delete everything during page-per-pool debugging
            // 在Debug期間進行刪除操作
            AutoreleasePoolPage *parent = page->parent;
            page->kill();
            setHotPage(parent);
        } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
            // special case: delete everything for pop(top) 
            // when debugging missing autorelease pools
            // 在不存在autorelease pools的情況刪除空的page
            page->kill();
            setHotPage(nil);
        } 
        else if (page->child) {
            // hysteresis: keep one empty child if page is more than half full
            // 如果當前page使用超過一半,就保留一個空的child page
            if (page->lessThanHalfFull()) {
                page->child->kill();
            }
            else if (page->child->child) {
                page->child->child->kill();
            }
        }
    }
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末焙蚓,一起剝皮案震驚了整個濱河市朝氓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌主届,老刑警劉巖赵哲,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異君丁,居然都是意外死亡枫夺,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進店門绘闷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來橡庞,“玉大人,你說我怎么就攤上這事印蔗“亲睿” “怎么了?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵华嘹,是天一觀的道長吧趣。 經(jīng)常有香客問我,道長耙厚,這世上最難降的妖魔是什么强挫? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮薛躬,結(jié)果婚禮上俯渤,老公的妹妹穿的比我還像新娘。我一直安慰自己型宝,他們只是感情好八匠,可當我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著趴酣,像睡著了一般梨树。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上价卤,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天劝萤,我揣著相機與錄音,去河邊找鬼慎璧。 笑死床嫌,一個胖子當著我的面吹牛跨释,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播厌处,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼鳖谈,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了阔涉?” 一聲冷哼從身側(cè)響起缆娃,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎瑰排,沒想到半個月后贯要,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡椭住,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年崇渗,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片京郑。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡宅广,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出些举,到底是詐尸還是另有隱情跟狱,我是刑警寧澤,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布户魏,位于F島的核電站驶臊,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏绪抛。R本人自食惡果不足惜资铡,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一电禀、第九天 我趴在偏房一處隱蔽的房頂上張望幢码。 院中可真熱鬧,春花似錦尖飞、人聲如沸症副。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽贞铣。三九已至,卻和暖如春沮明,著一層夾襖步出監(jiān)牢的瞬間辕坝,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工荐健, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留酱畅,地道東北人琳袄。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像纺酸,于是被迫代替她去往敵國和親窖逗。 傳聞我的和親對象是個殘疾皇子乐严,可洞房花燭夜當晚...
    茶點故事閱讀 45,055評論 2 355

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