線程虑鼎、自動釋放池辱匿、RunLoop的愛恨情仇

原文地址

線程

在多線程OS中键痛,線程是能獨立運行的基本單位,因而也是獨立調(diào)度和分派的基本單位匾七。由于線程很“輕”絮短,故線程的切換非常迅速且開銷小(在同一進程中的)
在一個進程中的多個線程之間昨忆,可以并發(fā)執(zhí)行丁频,甚至允許在一個進程中所有線程都能并發(fā)執(zhí)行;同樣邑贴,不同進程中的線程也能并發(fā)執(zhí)行席里,充分利用和發(fā)揮了處理機與外圍設(shè)備并行工作的能力。
在同一進程中的各個線程拢驾,都可以共享該進程所擁有的資源奖磁,這首先表現(xiàn)在:所有線程都具有相同的地址空間(進程的地址空間),這意味著繁疤,線程可以訪問該地址空間的每一個虛地址咖为;此外,還可以訪問進程所擁有的已打開文件稠腊、定時器躁染、信號量機構(gòu)等。由于同一個進程內(nèi)的線程共享內(nèi)存文件架忌,所以線程之間互相通信不必調(diào)用內(nèi)核褐啡。

自動釋放池

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(準(zhǔn)備進入休眠) 時調(diào)用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池并創(chuàng)建新池莉恼;Exit(即將退出Loop) 時調(diào)用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 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 了汽久。

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

  • 一個線程的自動釋放池是一個指針堆棧
  • 每一個指針或者指向被釋放的對象,或者是自動釋放池的POOL_BOUNDARY踊餐,POOL_BOUNDARY 是自動釋放池的邊界景醇。
  • 一個池子的 token 是指向池子 POOL_BOUNDARY 的指針。當(dāng)池子被出棧的時候吝岭,每一個高于標(biāo)準(zhǔn)的對象都會被釋放掉三痰。
  • 堆棧被分成一個頁面的雙向鏈表。頁面按照需要添加或者刪除窜管。
  • 本地線程存放著指向當(dāng)前頁的指針酒觅,在這里存放著新創(chuàng)建的自動釋放的對象。

自動釋放池是由 AutoreleasePoolPage 以雙向鏈表的方式實現(xiàn)的微峰,當(dāng)對象調(diào)用 autorelease 方法時舷丹,會將對象加入 AutoreleasePoolPage 的棧中,調(diào)用 AutoreleasePoolPage::pop 方法會向棧中的對象發(fā)送 release 消息.


雙向鏈表.png

AutoreleasePoolPage 的結(jié)構(gòu)

AutoreleasePoolPage 是一個 C++ 中的類:


AutoreleasePoolPage.png

它在 NSObject.mm 中的定義是這樣的:

class AutoreleasePoolPage {
...
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
...
};
  • magic 用于對當(dāng)前 AutoreleasePoolPage 完整性的校驗
  • thread 保存了當(dāng)前頁所在的線程
    每一個自動釋放池都是由一系列的 AutoreleasePoolPage 組成的蜓肆,并且每一個 AutoreleasePoolPage 的大小都是 4096 字節(jié)(16 進制 0x1000):
#define I386_PGBYTES 4096
#define PAGE_SIZE I386_PGBYTES

AutoreleasepoolPage 通過壓棧的方式來存儲每個需要自動釋放的對象:

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

RunLoop概念

蘋果對run loop 的解釋:

A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.

RunLoop 實際上是一個對象颜凯,這個對象管理了其需要處理的事件和消息,并提供了一個入口函數(shù)來執(zhí)行上面 Event Loop 的邏輯仗扬。線程執(zhí)行了這個函數(shù)后症概,函數(shù)會一直處于 "接受消息->等待->處理" 的循環(huán)中,直到這個循環(huán)結(jié)束(比如傳入 quit 的消息)早芭。
CFRunLoopRef 是在 CoreFoundation 框架內(nèi)的彼城,它提供了純 C 函數(shù)的 API,所有這些 API 都是線程安全的退个。CFRunLoopRef代碼是開源的募壕,https://opensource.apple.com/tarballs/CF/可以下載最新的源碼。
NSRunLoop 是基于 CFRunLoopRef 的封裝语盈,提供了面向?qū)ο蟮?API舱馅,但是這些 API 不是線程安全的。

RunLoop與線程的關(guān)系

獲取 main thread 的方法: pthread_main_thread_np() 或 [NSThread mainThread]刀荒,獲取當(dāng)前線程的方法:pthread_self() 或 [NSThread currentThread] 代嗤。
通過這兩個方法CFRunLoopGetMain() 和 CFRunLoopGetCurrent()來獲取RunLoop:

CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK();
    static CFRunLoopRef __main = NULL; // no retain needed
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
    return __main;
}

CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
    t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
        CFRelease(dict);
    }
    CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    if (!loop) {
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    if (!loop) {
        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
        loop = newLoop;
    }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFUnlock(&loopsLock);
    CFRelease(newLoop);
    }
    if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}

從上面的代碼可以看出,獲取RunLoop時需要傳pthread_t t作為參數(shù)缠借,線程和 RunLoop 之間是一一對應(yīng)的干毅,其關(guān)系是保存在一個全局的 Dictionary 里。線程剛創(chuàng)建時并沒有 RunLoop泼返,如果你不主動獲取硝逢,那它一直都不會有。RunLoop 的創(chuàng)建是發(fā)生在第一次獲取時,RunLoop 的銷毀是發(fā)生在線程結(jié)束時趴捅。你只能在一個線程的內(nèi)部獲取其 RunLoop(主線程除外)垫毙。

總結(jié)

  • 線程是獨立調(diào)度和分派的基本單位霹疫,RunLoop和自動釋放池為線程服務(wù)拱绑;
  • RunLoop是一個事件循環(huán),讓線程休眠和線程崩鲂活成為了可能猎拨,線程休眠可以節(jié)省CPU資源;
  • 自動釋放池一定存在于線程之中屠阻,解決了資源的釋放問題红省。

原文地址

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市国觉,隨后出現(xiàn)的幾起案子吧恃,更是在濱河造成了極大的恐慌,老刑警劉巖麻诀,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件痕寓,死亡現(xiàn)場離奇詭異,居然都是意外死亡蝇闭,警方通過查閱死者的電腦和手機呻率,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來呻引,“玉大人礼仗,你說我怎么就攤上這事÷哂疲” “怎么了元践?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長童谒。 經(jīng)常有香客問我卢厂,道長,這世上最難降的妖魔是什么惠啄? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任慎恒,我火速辦了婚禮,結(jié)果婚禮上撵渡,老公的妹妹穿的比我還像新娘融柬。我一直安慰自己,他們只是感情好趋距,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布粒氧。 她就那樣靜靜地躺著,像睡著了一般节腐。 火紅的嫁衣襯著肌膚如雪外盯。 梳的紋絲不亂的頭發(fā)上摘盆,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機與錄音饱苟,去河邊找鬼孩擂。 笑死,一個胖子當(dāng)著我的面吹牛箱熬,可吹牛的內(nèi)容都是我干的类垦。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼城须,長吁一口氣:“原來是場噩夢啊……” “哼蚤认!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起糕伐,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤砰琢,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后良瞧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體陪汽,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年莺褒,在試婚紗的時候發(fā)現(xiàn)自己被綠了掩缓。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡遵岩,死狀恐怖你辣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情尘执,我是刑警寧澤舍哄,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站誊锭,受9級特大地震影響表悬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜丧靡,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一蟆沫、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧温治,春花似錦饭庞、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春累盗,著一層夾襖步出監(jiān)牢的瞬間寒矿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工若债, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留符相,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓拆座,卻偏偏與公主長得像主巍,于是被迫代替她去往敵國和親冠息。 傳聞我的和親對象是個殘疾皇子挪凑,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

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