OC底層原理三十七:內(nèi)存管理(autorelease & runloop)

OC底層原理 學(xué)習(xí)大綱

  • ?? 上一節(jié) ,詳細(xì)介紹了weakstrong、強(qiáng)引用解決方案。本節(jié),我們將介紹:
  1. autorelease自動(dòng)釋放池
  2. runloop

準(zhǔn)備工作:


1.autorelease自動(dòng)釋放池

  • autorelease自動(dòng)釋放池: 自動(dòng)管理作用域內(nèi)對(duì)象引用計(jì)數(shù)池子。

面試題1:臨時(shí)變量什么時(shí)候釋放?
面試題2:簡(jiǎn)述自動(dòng)釋放池原理
面試題3:自動(dòng)釋放池能否嵌套使用?

1.1 初探autorelease

  • APP的入口函數(shù)main,包含了@autoreleasepool:
int main(int argc, const char * argv[]) {
    @autoreleasepool {
    }
    return 0;
}
  • 使用clangmain.m編譯后輸出main.cpp,在cpp文件中,可以看到:
    image.png
image.png
  1. @autoreleasepool編譯期轉(zhuǎn)化為了__AtAutoreleasePool結(jié)構(gòu)體。
  2. __AtAutoreleasePool構(gòu)造函數(shù)創(chuàng)建了自動(dòng)釋放池對(duì)象
  3. __AtAutoreleasePool析構(gòu)函數(shù)釋放了自動(dòng)釋放池對(duì)象
  • 仿__AtAutoreleasePool實(shí)現(xiàn)一個(gè)構(gòu)造析構(gòu)函數(shù),觀察生命周期:
    image.png

利用結(jié)構(gòu)體構(gòu)造析構(gòu)函數(shù),有效的匹配作用域。

1.2 源碼分析

  • 定位源碼: (libobjc庫(kù))
  • main.m文件的@autoreleasepool處加上斷點(diǎn),打開(kāi)匯編模式,運(yùn)行代碼:

    image.png

  • 加入objc_autoreleasePoolPush符號(hào)斷點(diǎn),運(yùn)行代碼,發(fā)現(xiàn)源碼libobjc庫(kù)

    image.png

  • 打開(kāi)objc4源碼,搜索objc_autoreleasePoolPush
    image.png
1.2.1 自動(dòng)釋放池結(jié)構(gòu)
image.png
image.png
1.2.2 push自動(dòng)釋放池
image.png
1.2.3 pop自動(dòng)釋放池
image.png

1.3 代碼驗(yàn)證:

必須在MRC環(huán)境下,才可以使用autorelease

image.png

#import <objc/runtime.h>
#import <malloc/malloc.h>

// 聲明外部實(shí)現(xiàn)
extern void _objc_autoreleasePoolPrint(void);

int main(int argc, char * argv[]) {
    @autoreleasepool {
        for (int i = 0 ; i<505; i++) {
            NSObject * objc = [[NSObject alloc] autorelease];
        }
        // 打印自動(dòng)釋放池的結(jié)構(gòu)信息
        _objc_autoreleasePoolPrint();
    }
    return 0;
}
  • 打印結(jié)果:


    image.png

    image.png
  • 數(shù)據(jù)較多,只截取了第一頁(yè)開(kāi)頭第二頁(yè)數(shù)據(jù)

1.4 autorelease的嵌套

#import <objc/runtime.h>
#import <malloc/malloc.h>

// 聲明外部實(shí)現(xiàn)
extern void _objc_autoreleasePoolPrint(void);

int main(int argc, char * argv[]) {
    @autoreleasepool {
        
        for (int i = 0 ; i<5; i++) {
            NSObject * objc = [[NSObject alloc] autorelease];
        }
        
        @autoreleasepool {
            for (int i = 0 ; i<3; i++) {
                NSObject * objc = [[NSObject alloc] autorelease];
            }
            // 打印自動(dòng)釋放池的結(jié)構(gòu)信息
            _objc_autoreleasePoolPrint();
            printf("\n-----------------------------\n");
        }
        // 打印自動(dòng)釋放池的結(jié)構(gòu)信息
        _objc_autoreleasePoolPrint();
    }
    return 0;
}
image.png
  • autoRelease嵌套,并沒(méi)有結(jié)構(gòu)上進(jìn)行嵌套。而是利用哨兵作用,直接多插入一個(gè)哨兵。
  • 因?yàn)槊看?code>遇到哨兵(pop出棧時(shí)),都表示一個(gè)autorelease釋放

2. runloop

【前言】
關(guān)于runloop,我看了一些資料,越看越把我看暈。 停下來(lái)稍微理一下。
(ps:runloop版本號(hào)是我虛構(gòu)的,輔助理解

  • 【使命】runloop官方文檔是在thread線程板塊中,他只是線程的一個(gè)輔助方式

    很簡(jiǎn)單的理解:一個(gè)線程,我們創(chuàng)建后,執(zhí)行任務(wù),它就釋放了。 那每次使用,我都這樣創(chuàng)建->執(zhí)行->釋放,豈不是很麻煩?

    我也不知道我啥時(shí)候會(huì)用到,我就希望有一個(gè)線程,在我需要在這個(gè)線程執(zhí)行任務(wù)時(shí)候,直接把任務(wù)丟過(guò)去就可以了。很開(kāi)心,runloop滿足你。

  • 【實(shí)現(xiàn)】runloop,顧名思義,就是一個(gè)(run)運(yùn)行(loop)循環(huán)。它的作用上面說(shuō)了,就是讓線程一直保持可用狀態(tài)(?;?/code>),如何保持一直在線?

    第一想法是,給個(gè)do-while(1)循環(huán),循環(huán)內(nèi)可以接受外部函數(shù),我們每次要執(zhí)行任務(wù)時(shí),給他一個(gè)函數(shù)就可以了。聰明!runloop1.0版本已經(jīng)開(kāi)發(fā)了??。

  • 【小結(jié)】runloop,本身就是一個(gè)函數(shù),函數(shù)內(nèi)創(chuàng)建do-while循環(huán),一直持有當(dāng)前線程(在哪個(gè)線程調(diào)用它,它就一直持有著哪個(gè)線程)。

  • 【優(yōu)化】按照上面使用do-while(1)循環(huán),我們會(huì)發(fā)現(xiàn)cpu激增,因?yàn)樗?code>一直運(yùn)行。那有人就有想法了,能不能我需要時(shí)候它就運(yùn)行,我不用時(shí)候,它就休息,不要占用我的cpu資源。我真不想要這個(gè)線程的時(shí)候,線程循環(huán)都給我銷毀。可以不可以呀?

    要求挺多呀,但挺合理的。?? 于是runloop2.0版本滿足你??。

    【第一個(gè)要求:支持銷毀
    runloopdo-while加上條件,你不要了就把這條件設(shè)置不滿足就OK啦。

    【第二個(gè)要求:支持休眠喚醒

    首先,runloop得知道它到底還有沒(méi)有沒(méi)干完的活。怎么界定呢? 簡(jiǎn)單,runloop列個(gè)業(yè)務(wù)清單,打今兒起,我就只接幾種業(yè)務(wù),做完了會(huì)回調(diào)你。(像不像去抽血,1,2,3號(hào)抽血,請(qǐng)?jiān)?,5,6窗口等結(jié)果。血液檢測(cè)完了,你就可以拿到結(jié)果了 ??)。 如果所有業(yè)務(wù)都處理完了,就進(jìn)入休眠狀態(tài)(沒(méi)活了可以玩會(huì)手機(jī),休息下)

    啥時(shí)候喚醒?怎么喚醒呢?

    runloop直接使用系統(tǒng)內(nèi)核mach port的消息機(jī)制mach_msg(),當(dāng)接收業(yè)務(wù)時(shí),系統(tǒng)會(huì)(通過(guò)source1)直接喚醒runloop,去執(zhí)行現(xiàn)在當(dāng)前接收到的任務(wù)。
    每一次循環(huán),都會(huì)查詢活有沒(méi)有干完,有沒(méi)有其他活在排隊(duì)沒(méi)有了就休息。收到系統(tǒng)消息就接著干活。

總之,目前我就是這么理解runloop的,總結(jié)一下:

  • 作用:為線程?;?/code>(所以一個(gè)線程一個(gè)runloop,一一對(duì)應(yīng))
  • 實(shí)現(xiàn):線程內(nèi)的一個(gè)函數(shù),弄個(gè)do-while循環(huán)讓這個(gè)線程一直在線??梢?code>處理幾類事務(wù)回調(diào)處理結(jié)果。支持休眠被喚醒,也支持銷毀。

相關(guān)鏈接:
?? RunLoop 官方文檔
?? 邏輯教育kody老師的公開(kāi)課
?? ibireme大神的Runloop分析
?? RunLoop 源碼閱讀

  • 至此,我想你內(nèi)心對(duì)runloop已經(jīng)有了一個(gè)大體認(rèn)知
    (一開(kāi)始就一頭扎進(jìn)源碼的我,可沒(méi)這么幸運(yùn)??)

  • 現(xiàn)在,我們來(lái)正式了解runloop

2.1 runloop是什么

runloop:

    1. 使用一個(gè)循環(huán),保持程序持續(xù)運(yùn)行;
    1. 一個(gè)線程對(duì)應(yīng)一個(gè)runloop,負(fù)責(zé)處理APP各種事件(觸摸、定時(shí)器、performSelector)
    1. 節(jié)省cpu資源。(無(wú)任務(wù)時(shí)自動(dòng)休眠,被喚醒繼續(xù)工作)
  • 經(jīng)典runloop流程圖:


    image.png

2.1.1 runloop的循環(huán)

  • 簡(jiǎn)單循環(huán)案例,會(huì)占用cpu。

    image.png

  • runloop循環(huán),閑時(shí)不會(huì)占用cpu
    (app啟動(dòng)就會(huì)啟動(dòng)主線程,主線程內(nèi)就維持一個(gè)runloop,一直給程序?;?/code>。)

    image.png

  • 我們下載runloop源碼,將CFRunLoop.cCFRunLoop.h文件拖入demo文件夾,搜索void CFRunloopRun:

    image.png

  • 發(fā)現(xiàn)runloop[do-while]循環(huán)在stopfinished時(shí),會(huì)結(jié)束。

ps: 驗(yàn)證runloop1.02.0過(guò)渡??

2.2 runloop線程保活

上面我們說(shuō)了,一個(gè)runloop對(duì)應(yīng)一個(gè)線程,作用就是線程?;?/code>:

2.2.1 原始線程(用完即銷毀):
//MARK: - HTThread
// 繼承NSThread,為了打印dealloc - 線程釋放
@interface HTThread : NSThread
@end
@implementation HTThread
-(void)dealloc{ NSLog(@"%@ %s",[HTThread currentThread].name,__func__); }
@end

//MARK: - ViewController
@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 1. 調(diào)用完,直接銷毀
    HTThread * thread = [[HTThread alloc]initWithTarget:self selector:@selector(threadTest) object:nil];
    thread.name = @"ht_Thread";
    [thread start];
}

- (void)threadTest {
    NSLog(@"%@ %s",[NSThread currentThread].name, __func__);
}
@end
image.png
  • 可以看到,名為ht_thread的線程,執(zhí)行完任務(wù)(threadTest函數(shù))后,就被銷毀了。
2.2.2 runloop線程?;睿?/h5>
@interface HTThread : NSThread
@end
@implementation HTThread
-(void)dealloc{ NSLog(@"%@ %s",[HTThread currentThread].name,__func__); }
@end

//MARK: -ViewController
@interface ViewController ()
@property(nonatomic, strong) HTThread * thread;
@property(nonatomic, strong) NSRunLoop * runloop; // 常駐線程

@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self createThread];
}

- (void)createThread {
    _thread = [[HTThread alloc]initWithTarget:self selector:@selector(threadTest) object:nil];
    _thread.name = @"ht_Thread";
    [_thread start];
}

- (void)threadTest {
    
    NSLog(@"%@ %s",[NSThread currentThread].name, __func__);

    // @autoreleasepool 對(duì)子線程中的臨時(shí)變量做優(yōu)化管理。更高效利用空間
    @autoreleasepool {
        // 使用runloop對(duì)當(dāng)前線程保活(當(dāng)前`threadTest`函數(shù)是在`ht_Thread`線程內(nèi)執(zhí)行)
        _runloop = [NSRunLoop currentRunLoop];
        [_runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        // 處理主線程,其他線程都需要手動(dòng)開(kāi)啟runloop (run內(nèi)綁定了線程與runloop的關(guān)系)
        [_runloop run];
    }
    
    // runloop沒(méi)被釋放,就到不來(lái)這一行。threadTest函數(shù)也一直不會(huì)結(jié)束
    NSLog(@"runloop釋放了 %@ %s",[NSThread currentThread].name, __func__);
    
}

- (void)threadTask {
    NSLog(@"%@ %s",[NSThread currentThread].name, __func__);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 點(diǎn)擊,讓_thread線程執(zhí)行任務(wù)(`threadTask`函數(shù))
    // 如果疑惑_thread是被self強(qiáng)持有,本身就可執(zhí)行的話。 可手動(dòng)注釋`@autoreleasepool`內(nèi)部代碼。再點(diǎn)擊檢驗(yàn)。會(huì)發(fā)現(xiàn)崩潰了。
    // 因?yàn)殡m然_thread在,但是沒(méi)法讓它為你工作,runloop是可以幫你管理_thread并讓它為你工作的。
    [self performSelector:@selector(threadTask) onThread:_thread withObject:nil waitUntilDone:YES];
}

@end
  • runloop成功的實(shí)現(xiàn)線程?;?/code>。(我想用的時(shí)候,都可以用)
    image.png

2.3 runloop的讀取

  • runloop的讀取,支持2種方式:
    主線程獲取(CFRunLoopGetMain) 和 當(dāng)前線程獲取(CFRunLoopGetCurre)。內(nèi)部調(diào)用_CFRunLoopGet0函數(shù)。
    image.png

總結(jié):

  1. 主線程是static全局唯一的,第一次獲取時(shí)創(chuàng)建。
  2. 線程不存在,默認(rèn)使用主線程,并返回主線程的runloop
  3. 首次訪問(wèn),會(huì)創(chuàng)建全局唯一__CFRunLoops字典key線程,valuerunloop。
    (線程runloop一一對(duì)應(yīng))
  4. 每次優(yōu)先__CFRunLoops字典中,通過(guò)key(線程),獲取value(runloop)。
  5. 如果runloop 不存在,就創(chuàng)建線程對(duì)應(yīng)的runloop,并更新__CFRunLoops字典對(duì)應(yīng)值
  6. 更新TSD(線程私有存儲(chǔ)),記錄runloop。
  7. 返回runloop

面試題: runLoop與線程的關(guān)系
一一對(duì)應(yīng)關(guān)系。由全局Runloop字典進(jìn)行記錄,其中key線程,valuerunloop。

2.4 runloop的創(chuàng)建

image.png

總結(jié):

  1. __CFRunLoop為模板,創(chuàng)建Runloop結(jié)構(gòu)體對(duì)象
  2. 屬性初始化賦值
  3. Mode的獲?。?
    • 如果通過(guò)__kCFRunLoopModeTypeID讀取到Modes,并且 Modes中存在kCFRunLoopDefaultMode,就直接返回找到的Mode。
    • 否則,創(chuàng)建一個(gè)Mode,加入modes中。返回Mode。

拓展:

  1. runLoop本質(zhì)是__CFRunLoop格式的結(jié)構(gòu)體。

    記錄線程鎖,port喚醒端口,所在線程、所有標(biāo)記為Common的Mode加入CommonMode的item事務(wù)、當(dāng)前Mode所有Mode

image.png
  1. 理解commonModesmodes:
    image.png

2.5 runloop的運(yùn)行原理

image.png
  • 看完源碼后,runloop運(yùn)行周期,喚醒方式十分清晰了?,F(xiàn)在奉上經(jīng)典Runloop流程圖
    image.png

補(bǔ)充說(shuō)明:

  1. 【最外層流程】
    kCFRunLoopEntry進(jìn)入循環(huán) (發(fā)通知)
    -> __CFRunLoopRun 運(yùn)行循環(huán)
    -> kCFRunLoopExit退出循環(huán)(發(fā)通知)

  2. 【循環(huán)內(nèi)部】
    kCFRunLoopBeforeTimers即將處理Timer (發(fā)通知)
    -> kCFRunLoopBeforeSources 即將處理Sources0 (發(fā)通知)
    __CFRunLoopDoBlocks 處理Blocks)
    -> __CFRunLoopDoSources0 處理Sources0
    __CFRunLoopDoBlocks 處理Blocks)
    -> __CFRunLoopServiceMachPort : 監(jiān)聽(tīng)Port端口消息(source1),有消息就跳轉(zhuǎn)handle_msg
    -> kCFRunLoopBeforeWaiting: 將進(jìn)入休眠 (發(fā)通知)
    進(jìn)入休眠,等待喚醒 (內(nèi)部的Timer到期、gcd都可喚醒)
    -> 線程被喚醒, (發(fā)通知)

3.【handle_msg】處理消息:

  • 被Timers喚醒(CFRUNLOOP_WAKEUP_FOR_TIMER): __CFRunLoopDoTimers (發(fā)通知)
  • 被gcd喚醒(CFRUNLOOP_WAKEUP_FOR_DISPATCH):__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ (發(fā)通知)
  • 被source喚醒(CFRUNLOOP_WAKEUP_FOR_SOURCE): __CFRunLoopDoSource1
    __CFRunLoopDoBlocks 處理Blocks)
  • 檢查stopfinish

Timer、dispatch、source等回調(diào)函數(shù):

// main  dispatch queue
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__

// __CFRunLoopDoObservers
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__

// __CFRunLoopDoBlocks
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__

// __CFRunLoopDoSources0
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__

// __CFRunLoopDoSource1
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__

// __CFRunLoopDoTimers
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__

在執(zhí)行回調(diào)Block前,我們可以在堆棧中看到上述回調(diào)函數(shù)。

  • 回調(diào)函數(shù)檢驗(yàn):
    (每次觸發(fā)TouchBegin時(shí),所在線程runloop都會(huì)調(diào)用__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__函數(shù))
    image.png

至此,完成了runloop基礎(chǔ)探索。

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

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

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