iOS 性能優(yōu)化-自動釋放池

AutoreleasePool是OC中的一種自動回收機制诗宣,在ARC的模式下已經(jīng)很少能看到autorelease了,它可以延遲變量release的時機耗跛。在OC的main.m中就有一個autoreleasepool杖刷,本篇結合runtime研究一下autoreleasepool的底層是如何實現(xiàn)的竹勉。

問答模式

問:什么時候需要使用自動釋放池裹虫?

官方解釋:基本分為如下三點
1肿嘲、當我們需要創(chuàng)建大量的臨時變量的時候,可以通過@autoreleasepool 來減少內(nèi)存峰值筑公。
2雳窟、創(chuàng)建了新的線程執(zhí)行Cocoa調(diào)用。
3匣屡、如果您的應用程序或線程是長期存在的封救,并且可能會生成大量自動釋放的對象,那么您應該定期清空并創(chuàng)建自動釋放池(就像UIKit在主線程上所做的那樣)捣作;否則誉结,自動釋放的對象會累積,內(nèi)存占用也會增加券躁。但是惩坑,如果創(chuàng)建的線程不進行Cocoa調(diào)用,則不需要創(chuàng)建自動釋放池也拜。

問:為什么會減少內(nèi)存峰值旭贬?
答:借用YYImage的代碼打個比方。
比如業(yè)務需要在一個代碼塊中需要創(chuàng)建大量臨時變量搪泳,或臨時變量足夠大,占用了很多內(nèi)存扼脐,可以在臨時變量使用完以后就立即釋放掉岸军,在ARC的環(huán)境下只能通過自動釋放池實現(xiàn)奋刽。

if ([UIDevice currentDevice].isSimulator) {
        @autoreleasepool {
            NSString *outPath = [NSString stringWithFormat:@"%@ermilio.gif.png",IMAGE_OUTPUT_DIR];
            NSData *outData = UIImagePNGRepresentation([UIImage imageWithData:gif]);
            [outData writeToFile:outPath atomically:YES];
            [gif writeToFile:[NSString stringWithFormat:@"%@ermilio.gif",IMAGE_OUTPUT_DIR] atomically:YES];
        }
        @autoreleasepool {
            NSString *outPath = [NSString stringWithFormat:@"%@ermilio.apng.png",IMAGE_OUTPUT_DIR];
            NSData *outData = UIImagePNGRepresentation([UIImage imageWithData:apng]);
            [outData writeToFile:outPath atomically:YES];
            [apng writeToFile:[NSString stringWithFormat:@"%@ermilio.png",IMAGE_OUTPUT_DIR] atomically:YES];
        }
        @autoreleasepool {
            NSString *outPath = [NSString stringWithFormat:@"%@ermilio_q85.webp.png",IMAGE_OUTPUT_DIR];
            NSData *outData = UIImagePNGRepresentation([YYImageDecoder decodeImage:webp_q85 scale:1]);
            [outData writeToFile:outPath atomically:YES];
            [webp_q85 writeToFile:[NSString stringWithFormat:@"%@ermilio_q85.webp",IMAGE_OUTPUT_DIR] atomically:YES];
        }
}

再比如在循環(huán)的場景下,如果創(chuàng)建大量的臨時變量艰赞,會使內(nèi)存峰值持續(xù)增加佣谐,加入自動釋放池以后,在每次循環(huán)結束時方妖,超出自動釋放池的作用域狭魂,使得內(nèi)部的大量臨時變量被釋放,從而大大降低了內(nèi)存的使用党觅。

for (int i = 0; i < count; i++) {
        @autoreleasepool {
            id imageSrc = _images[i];
            NSDictionary *frameProperty = NULL;
            if (_type == YYImageTypeGIF && count > 1) {
                frameProperty = @{(NSString *)kCGImagePropertyGIFDictionary : @{(NSString *) kCGImagePropertyGIFDelayTime:_durations[i]}};
            } else {
                frameProperty = @{(id)kCGImageDestinationLossyCompressionQuality : @(_quality)};
            }
}

上述這幾種情況如果沒必要就別這么寫雌澄,畢竟創(chuàng)建自動釋放池也需要耗費內(nèi)存。

自動釋放池的實現(xiàn)原理

在開始之前先看一下自動釋放池的大致結構圖

自動釋放池結構圖.png

上圖就是自動釋放池的結構圖杯瞻,可能現(xiàn)在看不懂镐牺,這里先有個概況繼續(xù)往下看就明白了,不太會畫圖魁莉,反正意思表達出來了睬涧。

查看main.cpp

我們先在終端clang一下main.m,變成C++實現(xiàn)
clang -rewrite-objc main.m -o main.cpp
我們會得到一個main.cpp文件旗唁,打開這個文件翻到最底部會看到這個代碼

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_vg_bngxds5x5q90wwst5gl1jq140000gn_T_main_1b100d_mi_0);
    }
    return 0;
}

發(fā)現(xiàn)原來autoreleasepool也是一個對象畦浓,我們在這個cpp文件中查找__AtAutoreleasePool,找到如下的結構體

extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);

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

結構體只提供了一個構造函數(shù)和一個析構函數(shù)检疫,里面分別調(diào)用了objc_autoreleasePoolPushobjc_autoreleasePoolPop讶请,這個objc前綴告訴我們,是不是能到runtime里面搜索一下电谣,在rumtime源碼中全局搜索objc_autoreleasePoolPush秽梅,找到這個函數(shù)

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

我們發(fā)現(xiàn)了正主,是一個類AutoreleasePoolPage

AutoreleasePoolPage

class AutoreleasePoolPage 
{
   ···
   //當自動釋放池為空時的一個占位符
#   define EMPTY_POOL_PLACEHOLDER ((id*)1)
    //邊界符剿牺,用來區(qū)別每個AutoreleasePoolPage的邊界
#   define POOL_BOUNDARY nil
    //線程的key企垦,通過key值尋找線程下的AutoreleasePoolPage
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    //4096個字節(jié),表示每個page的大小晒来,因為虛擬內(nèi)存每個扇區(qū)4096個字節(jié)
    PAGE_MAX_SIZE;  
    //一個page里面的對象數(shù)量
    static size_t const COUNT = SIZE / sizeof(id);
    //共需要占用56個字節(jié)
    magic_t const magic;                   // 16字節(jié)钞诡,校驗完整性的變量
    id *next;                              // 8字節(jié),指向下一個對象的指針
    pthread_t const thread;                // 8字節(jié)湃崩,所屬線程荧降,page和thread是一一對應關系
    AutoreleasePoolPage * const parent;    // 8字節(jié),父節(jié)點攒读,指向上一個page
    AutoreleasePoolPage *child;            // 8字節(jié)朵诫,子節(jié)點,指向下一個page
    uint32_t const depth;                  // 4字節(jié)薄扁,表示鏈表一共有多少個節(jié)點
    uint32_t hiwat;                        // 4字節(jié)剪返,high water marks表示自動釋放池中最多能存放的對象個數(shù)
    ···
}

從這個類中我們得到了以下內(nèi)容

  • EMPTY_POOL_PLACEHOLDER:
    當自動釋放池為空時的一個占位符废累,就是在第一次push時,先用這個字段把AutoreleasePoolPage的位置占上脱盲。
// EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is 
// pushed and it has never contained any objects. This saves memory 
// when the top level (i.e. libdispatch) pushes and pops pools but 
// never uses them.
  • POOL_BOUNDARY :
    邊界符邑滨,用來區(qū)別每個AutoreleasePoolPage的邊界,我們從創(chuàng)建page的時候可以得知
// We are pushing an object or a non-placeholder'd pool.
// 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);
   }
  • key:
    線程的key钱反,通過key值尋找線程下的AutoreleasePoolPage
  • PAGE_MAX_SIZE:
    4096個字節(jié)掖看,表示每個page的大小,因為虛擬內(nèi)存每個扇區(qū)4096個字節(jié)
  • COUNT:
    一個page里面的對象數(shù)量
  • magic:
    校驗完整性的變量面哥,占用16字節(jié)
  • next:
    指向下一個對象的指針哎壳,占用8字節(jié)
  • thread:
    所屬線程,page和thread是一一對應關系幢竹,占用8字節(jié)
  • parent:
    父節(jié)點耳峦,指向上一個page,占用8字節(jié)焕毫,看到這里我們發(fā)現(xiàn)這個自動釋放池其實是個雙向鏈表蹲坷,不過是以棧的形式存取的
  • child:
    子節(jié)點,指向下一個page邑飒,占用8字節(jié)
  • depth:
    表示鏈表一共有多少個節(jié)點循签,占用4字節(jié)
  • hiwat:
    high water marks表示自動釋放池中最多能存放的對象個數(shù),占用4字節(jié)

從上面的分析我們可以得知疙咸,page本身占用了56個字節(jié)县匠,而一個AutoreleasePoolPage一共4096個字節(jié),也就是說我們還剩下4040個字節(jié)可以用來放對象撒轮。接下來看看它的push和pop的過程乞旦。

1、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;
    }

這里我們不看Debug题山,直接找autoreleaseFast函數(shù)

static inline id *autoreleaseFast(id obj)
    {
        //獲取當前活躍的page
        AutoreleasePoolPage *page = hotPage();
        if (page && !page->full()) {
            return page->add(obj);
        } else if (page) {
            return autoreleaseFullPage(obj, page);
        } else {
            return autoreleaseNoPage(obj);
        }
    }

從代碼中我們得知纹烹,先調(diào)用hotPage()函數(shù)獲取page涮帘,當page不滿時谒兄,我們調(diào)用add()函數(shù)赶促;當對象滿了時,調(diào)用了autoreleaseFullPage()函數(shù)慨菱;當沒獲取到page時焰络,調(diào)用autoreleaseNoPage()函數(shù)。接下來我們看看這幾個函數(shù)都做了什么

1.1符喝、hotPage()
static inline AutoreleasePoolPage *hotPage() 
    {
        //從一個鍵值對中獲取當前page
        AutoreleasePoolPage *result = (AutoreleasePoolPage *)
            tls_get_direct(key);
        if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
        if (result) result->fastcheck();
        return result;
    }

從代碼中我們得知hotPage函數(shù)是從一個鍵值對中獲取當前活躍的page闪彼,而這個key就是上面我們看到的

static pthread_key_t const key = AUTORELEASE_POOL_KEY;
1.2、page->add(obj)
id *add(id obj)
    {
        assert(!full());
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
        protect();
        return ret;
    }

從源碼中我們得知协饲,add()是向鏈表中增加一個對象备蚓,簡單的改變了指針的指向课蔬,這不必細說。

1.3郊尝、autoreleaseFullPage(obj, page)
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
    {
        // The hot page is full. 
        // Step to the next non-full page, adding a new page if necessary.
        // Then add the object to that 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滿了以后战惊,循環(huán)遍歷自動釋放池中的page流昏,直到找到一個page不滿時,我們把對象添加進去吞获。

1.4况凉、autoreleaseNoPage(obj)
id *autoreleaseNoPage(id obj)
    {
        bool pushExtraBoundary = false;
        //判斷是否有空池占位符
        if (haveEmptyPoolPlaceholder()) {
            pushExtraBoundary = true;
        } else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
            //沒有可用pool
            objc_autoreleaseNoPool(obj);
            return nil;
        }
        else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
            //當前page還沒有空池占位符,先加上占位符
            return setEmptyPoolPlaceholder();
        }
       //如果執(zhí)行到這里各拷,表示目前沒有可有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);
        }
         //把autorelease對象添加進來
        return page->add(obj);
    }
1.5烤黍、Push總結
AutoreleasePoolPage-push.png

流程基本如上圖所示

2知市、Pop

在Pop時,會傳入當前的token速蕊,token就是

static inline void pop(void *token) 
    {
        //token就是邊界符
        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
            if (hotPage()) {
                pop(coldPage()->begin());
            } else {
                setHotPage(nil);
            }
            return;
        }
        //找到最上邊的page嫂丙,即當前的page
        page = pageForPointer(token);
        stop = (id *)token;
        if (*stop != POOL_BOUNDARY) {
            if (stop == page->begin()  &&  !page->parent) {
                //講道理,如果token不等于POOL_BOUNDARY规哲,pageForPointer()計算過后跟啤,理論上是一定會進入這里的
            } else {
                //走這就出問題了
                // Error. For bincompat purposes this is not 
                // fatal in executables built with old SDKs.
                return badPop(token);
            }
        }
        
        //更新當前自動釋放池最大存儲數(shù)
        if (PrintPoolHiwat) printHiwat();
        //清空token之前的autorelease對象
        page->releaseUntil(stop);
        //清空操作
        if (page->lessThanHalfFull()) {
            page->child->kill();
        } else if (page->child->child) {
            page->child->child->kill();
       } 
    }

從源碼中我們看到了Pop的過程分成了三步,
1唉锌、判斷token是否等于EMPTY_POOL_PLACEHOLDER
首先我們要知道token實際上是個邊界符隅肥,通常情況下等于POOL_BOUNDARY,其次我們要記得上面說過自動釋放池其實是個雙向鏈表袄简,不過是以棧的形式存取的腥放,所以當執(zhí)行這個判斷條件時,實際上就是Pop到了最后一步了痘番。
2捉片、當token不等于POOL_BOUNDARY時
這一步一般是不會進來的,只有在沒有自動釋放池且調(diào)用了autorelease時才會出現(xiàn)汞舱。但生活還是要繼續(xù)的...
在做接下來的操作前伍纫,先獲取最新的page,即當前page

page = pageForPointer(token);
static AutoreleasePoolPage *pageForPointer(uintptr_t p) 
    {
        AutoreleasePoolPage *result;
        uintptr_t offset = p % SIZE;//size就是4096昂芜,每個page最大size

        result = (AutoreleasePoolPage *)(p - offset);
        result->fastcheck();

        return result;
    }

下面這個操作知識為了確保這個token拿到的page沒問題莹规。

stop = (id *)token;
        if (*stop != POOL_BOUNDARY) {
            if (stop == page->begin()  &&  !page->parent) {
                //講道理,如果token不等于POOL_BOUNDARY泌神,pageForPointer()計算過后良漱,理論上是一定會進入這里的
                // 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);
            }
        }

3舞虱、最后一步釋放page里面的對象。
在釋放操作之前母市,更新當前自動釋放池最大存儲數(shù)矾兜。

if (PrintPoolHiwat) printHiwat();

static void printHiwat()
    {
        AutoreleasePoolPage *p = hotPage();
        uint32_t mark = p->depth*COUNT + (uint32_t)(p->next - p->begin());
        if (mark > p->hiwat  &&  mark > 256) {
            for( ; p; p = p->parent) {
                p->unprotect();
                p->hiwat = mark;
                p->protect();
            }

這一步操作釋放token之前的autorelease對象。

//釋放token之前的autorelease對象
page->releaseUntil(stop);

void releaseUntil(id *stop) 
    {
        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
        
        while (this->next != stop) {
            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);
            }
        }

kill操作患久,如果當前page小于當前page的一半時椅寺,則把當前頁的所有子節(jié)點都kill掉,否則從子節(jié)點的子節(jié)點開始kill蒋失。

//清空操作
        if (page->lessThanHalfFull()) {
            page->child->kill();
        } else if (page->child->child) {
            page->child->child->kill();
       } 

到目前為止返帕,我們明白了autorelease對象的釋放是在autoreleasePool釋放之前。

參考資料

autorelease和autoreleasePoolPage--你真的了解么?
OC源碼 —— autoreleasepool
官方runtime源碼

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末篙挽,一起剝皮案震驚了整個濱河市荆萤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌铣卡,老刑警劉巖链韭,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異算行,居然都是意外死亡梧油,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進店門州邢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來儡陨,“玉大人,你說我怎么就攤上這事量淌∑澹” “怎么了?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵呀枢,是天一觀的道長胚股。 經(jīng)常有香客問我,道長裙秋,這世上最難降的妖魔是什么琅拌? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮摘刑,結果婚禮上进宝,老公的妹妹穿的比我還像新娘。我一直安慰自己枷恕,他們只是感情好党晋,可當我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般未玻。 火紅的嫁衣襯著肌膚如雪灾而。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天扳剿,我揣著相機與錄音旁趟,去河邊找鬼。 笑死舞终,一個胖子當著我的面吹牛轻庆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播敛劝,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼纷宇!你這毒婦竟也來了夸盟?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤像捶,失蹤者是張志新(化名)和其女友劉穎上陕,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拓春,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡释簿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了硼莽。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片庶溶。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖懂鸵,靈堂內(nèi)的尸體忽然破棺而出偏螺,到底是詐尸還是另有隱情,我是刑警寧澤匆光,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布套像,位于F島的核電站,受9級特大地震影響终息,放射性物質(zhì)發(fā)生泄漏夺巩。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一周崭、第九天 我趴在偏房一處隱蔽的房頂上張望柳譬。 院中可真熱鬧,春花似錦休傍、人聲如沸征绎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽人柿。三九已至柴墩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間凫岖,已是汗流浹背江咳。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留哥放,地道東北人歼指。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像甥雕,于是被迫代替她去往敵國和親踩身。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,871評論 2 354

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