iOS-RunLoop詳解(三):使用RunLoop線程?;罘桨?/h1>
如果經(jīng)常要在子線程中做事情,不使用?;?,就會一直創(chuàng)建、銷毀子線程,這樣很耗性能,所以經(jīng)常在子線程做事情最好使用線程?;睢?/p>
實現(xiàn)線程保活
創(chuàng)建線程類,表示需要經(jīng)常執(zhí)行的任務
***********************?? MJThread.h ??**************************
@interface MJThread : NSThread
@end
#import "MJThread.h"
@implementation MJThread
***********************?? MJThread.m ??**************************
- (void)dealloc
{
NSLog(@"%s", __func__);
}
@end
***********************?? ViewController.m ??**************************
@implementation ViewController
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// NSThread 頻繁創(chuàng)建線程
MJThread *thread = [[MJThread alloc]initWithTarget:self selector:@selector(test) object:nil];
[thread start];
// [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
// 子線程需要執(zhí)行的任務
- (void)test
{
NSLog(@"%s %@", __func__, [NSThread currentThread]);
}
@end
RUN> ??????
我們每次點擊,都會去執(zhí)行任務:
NSThread 頻繁創(chuàng)建線程2021-05-14 14:55:19.054958+0800 Interview03-線程保活[4068:166884] -[ViewController test] <MJThread: 0x600000634900>{number = 7, name = (null)} 2021-05-14 14:55:19.056474+0800 Interview03-線程?;頪4068:166884] -[MJThread dealloc] 2021-05-14 14:55:21.627640+0800 Interview03-線程?;頪4068:166923] -[ViewController test] <MJThread: 0x60000062ddc0>{number = 8, name = (null)} 2021-05-14 14:55:21.627815+0800 Interview03-線程保活[4068:166923] -[MJThread dealloc]可以看到,任務一執(zhí)行完,線程就釋放了.并且這樣頻繁的創(chuàng)建線程,很消耗資源
我們可以用RunLoop來延長線程的生命周期,不讓線程掛掉
// 子線程需要執(zhí)行的任務
- (void)test
{
NSLog(@"%s %@", __func__, [NSThread currentThread]);
[[NSRunLoop currentRunLoop] run];
NSLog(@"%s ----end----", __func__);
}
RUN> ??????
2021-05-14 15:01:54.818875+0800 Interview03-線程?;頪4128:171606] -[ViewController test] <MJThread: 0x60000147d100>{number = 7, name = (null)} 2021-05-14 15:01:54.819344+0800 Interview03-線程?;頪4128:171606] -[ViewController test] ----end---- 2021-05-14 15:01:54.821043+0800 Interview03-線程保活[4128:171606] -[MJThread dealloc]運行一下,發(fā)現(xiàn)線程執(zhí)行完任務還是會結(jié)束線程(
[MJThread dealloc])這是因為,我們雖然獲取了當前的
RunLoop,并且調(diào)用run方法讓RunLoop跑起來了,而run方法底層調(diào)用的是- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;方法,這個方法會把RunLoop添加到NSDefaultRunLoopMode模式下.而Model中如果沒有任何Source0 , Source1 , Timer , Observer,RunLoop會立馬退出. 所以我們需要往RunLoop中添加任務,任何任務都可以.
我們需要往RunLoop中添加任務,任何任務都可以.
// 子線程需要執(zhí)行的任務
- (void)test
{
NSLog(@"%s %@", __func__, [NSThread currentThread]);
NSLog(@"??????????????????????????");
// 往RunLoop里面添加Source\Timer\Observer
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
NSLog(@"%s ----end----", __func__);
}
RUN> ??????
2021-05-14 15:05:54.882701+0800 Interview03-線程?;頪4172:174678] -[ViewController test] <MJThread: 0x600003c90f00>{number = 7, name = (null)} 2021-05-14 15:05:54.883756+0800 Interview03-線程保活[4172:174678] ??????????????????????????在運行線程就不會執(zhí)行完任務就掛掉了,而是執(zhí)行完任務就休眠:
每一次點擊屏幕,都是創(chuàng)建了[[MJThread alloc]initWithTarget:self selector:@selector(test) object:nil];經(jīng)過[[NSRunLoop currentRunLoop] addPort 和 [[NSRunLoop currentRunLoop] run]; 線程進入休眠了,但是我們現(xiàn)在沒辦法喚醒線程和執(zhí)行線程任務,程序繼續(xù)修改。
上面的代碼thread是一個局部變量,每次執(zhí)行任務都會重新創(chuàng)建,所以我們把線程設(shè)置成成員屬性。
***********************?? ViewController.m ??**************************
@interface ViewController ()
@property (strong, nonatomic) MJThread *thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[MJThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[self.thread start];
}
//觸碰屏幕事件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
// 子線程需要執(zhí)行的任務
- (void)test
{
NSLog(@"%s %@", __func__, [NSThread currentThread]);
NSLog(@"??????????????????????????");
}
// 這個方法的目的:線程保活
- (void)run {
NSLog(@"%s %@", __func__, [NSThread currentThread]);
NSLog(@"---------- start -----------");
// 往RunLoop里面添加Source\Timer\Observer
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
NSLog(@"%s ----end----", __func__);
}
@end
RUN> ??????
2021-05-14 15:14:16.185593+0800 Interview03-線程?;頪4221:180238] -[ViewController test] <MJThread: 0x6000005ecb40>{number = 7, name = (null)} 2021-05-14 15:14:16.185768+0800 Interview03-線程保活[4221:180238] ?????????????????????????? 2021-05-14 15:14:17.821116+0800 Interview03-線程?;頪4221:180238] -[ViewController test] <MJThread: 0x6000005ecb40>{number = 7, name = (null)} 2021-05-14 15:14:17.821240+0800 Interview03-線程?;頪4221:180238] ?????????????????????????? 2021-05-14 15:14:20.824925+0800 Interview03-線程保活[4221:180238] -[ViewController test] <MJThread: 0x6000005ecb40>{number = 7, name = (null)} 2021-05-14 15:14:20.825090+0800 Interview03-線程?;頪4221:180238] ??????????????????????????
上面的代碼有兩個問題:
- self和thread會造成循環(huán)引用,都不會釋放
- thread一直不會死
首先解決循環(huán)引用:
#import "ViewController.h"
#import "MJThread.h"
@interface ViewController ()
@property (strong, nonatomic) MJThread *thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//如果使用如下方式創(chuàng)建thread,self會引用thread,thread會引用self,會造成循環(huán)引用。
//[[MJThread alloc] initWithTarget:self selector:@selector(run) object:nil];
self.thread = [[MJThread alloc] initWithBlock:^{
NSLog(@"%@----begin----", [NSThread currentThread]);
// 往RunLoop里面添加Source\Timer\Observer
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
//線程會一直阻塞這這一行,永遠不會銷毀
[[NSRunLoop currentRunLoop] run];
//當把NSRunLoop停掉之后,代碼就會從下一行往下走,這時候任務執(zhí)行完成,線程該死的時候就會死了。
NSLog(@"%@----end----", [NSThread currentThread]);
}];
[self.thread start];
}
- (void)dealloc
{
NSLog(@"%s", __func__);
//就算把thread清空,thread也不會銷毀,因為任務還沒結(jié)束,線程就不會死。
//self.thread = nil;
}
運行后,在當前界面返回
RUN> ??????
2021-05-14 15:21:44.228798+0800 Interview03-線程?;頪4480:196517] <MJThread: 0x600002a04080>{number = 9, name = (null)}----begin---- 2021-05-14 15:21:47.517069+0800 Interview03-線程?;頪4480:195322] -[ViewController dealloc]可以發(fā)現(xiàn)ViewController銷毀了,但是thread還是沒被銷毀
很奇怪,控制器都釋放了,按理說控制器內(nèi)部的所有東西都應該釋放了呀,我們在
ViewController的dealloc方法中把thread置為nil:2021-05-14 15:25:09.585499+0800 Interview03-線程?;頪4520:199781] <MJThread: 0x600003b4ab40>{number = 8, name = (null)}----begin---- 2021-05-14 15:25:13.501110+0800 Interview03-線程?;頪4520:199781] -[ViewController test] <MJThread: 0x600003b4ab40>{number = 8, name = (null)} 2021-05-14 15:25:13.501288+0800 Interview03-線程?;頪4520:199781] ?????????????????????????? 2021-05-14 15:25:16.475845+0800 Interview03-線程?;頪4520:199617] -[ViewController dealloc]把
thread強制置為nil,thread還是沒有釋放.self.thread = [[MJThread alloc] initWithBlock:^{ NSLog(@"%@----begin----", [NSThread currentThread]); // 往RunLoop里面添加Source\Timer\Observer [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode]; //線程會一直阻塞這這一行,永遠不會銷毀 [[NSRunLoop currentRunLoop] run]; //當把NSRunLoop停掉之后,代碼就會從下一行往下走,這時候任務執(zhí)行完成,線程該死的時候就會死了。 NSLog(@"%@----end----", [NSThread currentThread]); }];這是因為RunLoop在 [[NSRunLoop currentRunLoop] run]這一行一直阻塞,一直不會打印----end----,這時候任務一直在進行,任務還沒有完成線程就不會死,就算在ViewController的dealloc方法里面把thread清空,thread也不會死。
如果我們想要精準的控制線程的生命周期,比如說控制器銷毀的時候,線程也銷毀,那應該怎么做呢?我們可以像下面這樣手動停止RunLoop:
- (IBAction)stop {
// 在子線程調(diào)用stop
[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];
}
// 用于停止子線程的RunLoop
- (void)stopThread
{
// 停止RunLoop
CFRunLoopStop(CFRunLoopGetCurrent());
NSLog(@"%s %@", __func__, [NSThread currentThread]);
}
run> ??????
2021-05-14 15:34:46.499408+0800 Interview03-線程保活[4566:205814] <MJThread: 0x600001f1b900>{number = 9, name = (null)}----begin---- 2021-05-14 15:34:55.230839+0800 Interview03-線程?;頪4566:205814] -[ViewController test] <MJThread: 0x600001f1b900>{number = 9, name = (null)} 2021-05-14 15:34:55.230956+0800 Interview03-線程保活[4566:205814] ?????????????????????????? 2021-05-14 15:34:57.846728+0800 Interview03-線程?;頪4566:205814] -[ViewController stopThread] <MJThread: 0x600001f1b900>{number = 9, name = (null)} 2021-05-14 15:35:14.203913+0800 Interview03-線程?;頪4566:205687] -[ViewController dealloc]
stopRunLoop雖然執(zhí)行了,并且ViewController也已經(jīng)銷毀了,但是thread仍然沒有銷毀,這是為什么呢?
線程不會死的原因就是有個RunLoop一直在運行,線程一直有任務做,所以想讓線程死掉,就把RunLoop停掉,當把RunLoop停掉之后,代碼就會從 [[NSRunLoop currentRunLoop] run]往下走,當線程執(zhí)行完任務后,線程該死的時候(當前控制器銷毀后)就會死了。
我們看run方法的解釋:
it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:.
In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers.
翻譯過來就是:
它通過反復調(diào)用runMode:beforeDate:在NSDefaultRunLoopMode中運行接收器。換句話說,這個方法有效地開始了一個無限循環(huán),處理來自運行循環(huán)的輸入源和計時器的數(shù)據(jù)。
可以看出,通過run方法運行的RunLoop是無法停止的,它專門用于開啟一個永不銷毀的線程(NSRunLoop)。
既然這樣,那我們可以模仿run方法,寫個while循環(huán),內(nèi)部也調(diào)用runMode:beforeDate:方法,如下:
while (!weakSelf.isStoped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
//while的條件判斷中要使用weakSelf,不然self強引用thread,thread強引用block,block強引用self,產(chǎn)生循環(huán)引用
不使用run方法,我們就能停掉RunLoop了,停掉RunLoop系統(tǒng)有提供API是CFRunLoopStop(CFRunLoopGetCurrent()),但是這個API不能在ViewController的dealloc方法里面寫,因為ViewController的dealloc方法是在主線程調(diào)用的,我們要保證在子線程調(diào)用CFRunLoopStop(CFRunLoopGetCurrent())。
#import "ViewController.h"
#import "MJThread.h"
@interface ViewController ()
@property (strong, nonatomic) MJThread *thread;
@property (assign, nonatomic, getter=isStoped) BOOL stopped;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.stopped = NO;
self.thread = [[MJThread alloc] initWithBlock:^{
NSLog(@"%@----begin----", [NSThread currentThread]);
// 往RunLoop里面添加Source\Timer\Observer
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
while (!weakSelf.isStoped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
NSLog(@"%@----end----", [NSThread currentThread]);
// NSRunLoop的run方法是無法停止的,它專門用于開啟一個永不銷毀的線程(NSRunLoop)
// [[NSRunLoop currentRunLoop] run];
/*
it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:.
In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers
*/
}];
[self.thread start];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
// 子線程需要執(zhí)行的任務
- (void)test
{
NSLog(@"%s %@", __func__, [NSThread currentThread]);
NSLog(@"??????????????????????????");
}
- (IBAction)stop {
// 在子線程調(diào)用stop
[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];
}
// 用于停止子線程的RunLoop
- (void)stopThread
{
// 設(shè)置標記為NO
self.stopped = YES;
// 停止RunLoop
CFRunLoopStop(CFRunLoopGetCurrent());
NSLog(@"%s %@", __func__, [NSThread currentThread]);
}
- (void)dealloc
{
NSLog(@"%s", __func__);
//就算把thread清空,thread也不會銷毀,因為任務還沒結(jié)束,線程就不會死。
self.thread = nil;
// [self stop];
}
@end
RUN> ??????
2021-05-14 15:39:47.441947+0800 Interview03-線程保活[4606:209299] <MJThread: 0x600000f47580>{number = 8, name = (null)}----begin---- 2021-05-14 15:39:48.914864+0800 Interview03-線程?;頪4606:209299] -[ViewController test] <MJThread: 0x600000f47580>{number = 8, name = (null)} 2021-05-14 15:39:48.915021+0800 Interview03-線程保活[4606:209299] ?????????????????????????? 2021-05-14 15:39:50.900749+0800 Interview03-線程?;頪4606:209299] -[ViewController stopThread] <MJThread: 0x600000f47580>{number = 8, name = (null)} 2021-05-14 15:39:50.901004+0800 Interview03-線程保活[4606:209299] <MJThread: 0x600000f47580>{number = 8, name = (null)}----end---- 2021-05-14 15:39:59.322333+0800 Interview03-線程?;頪4606:209161] -[ViewController dealloc] 2021-05-14 15:39:59.322455+0800 Interview03-線程?;頪4606:209161] -[MJThread dealloc]上面要使用weakself,不然self強引用thread,thread強引用block,block強引用self,產(chǎn)生循環(huán)引用(使用weakself之后,就是self強引用thread,thread強引用block,block弱引用self,不會產(chǎn)生循環(huán)引用)。
運行代碼,進入界面,打?。?/p>
2021-05-14 15:39:47.441947+0800 Interview03-線程?;頪4606:209299] <MJThread: 0x600000f47580>{number = 8, name = (null)}----begin----說明線程開始工作了。
點擊空白,打?。?/p>
2021-05-14 15:42:22.608171+0800 Interview03-線程?;頪4606:211084] -[ViewController test] <MJThread: 0x600000f38540>{number = 9, name = (null)} 2021-05-14 15:42:22.610916+0800 Interview03-線程保活[4606:211084] ??????????????????????????說明RunLoop接收到事件,開始處理事件。
點擊stop打?。?/p>
2021-05-14 15:42:35.860716+0800 Interview03-線程?;頪4606:211084] -[ViewController stopThread] <MJThread: 0x600000f38540>{number = 9, name = (null)} 2021-05-14 15:42:35.861011+0800 Interview03-線程?;頪4606:211084] <MJThread: 0x600000f38540>{number = 9, name = (null)}----end----可以看出,執(zhí)行了CFRunLoopStop,并且線程任務完成,打印了----end----。
點擊stop之后再退出當前VC,打印:
2021-05-14 15:44:08.504631+0800 Interview03-線程?;頪4606:209161] -[ViewController dealloc] 2021-05-14 15:44:08.504806+0800 Interview03-線程保活[4606:209161] -[MJThread dealloc]可以發(fā)現(xiàn),當前VC和thread都被銷毀了。
上面代碼還有一個問題,就是我們每次都要先點擊停止再返回當前VC,這樣很麻煩,可能你會說可以把[self stop]方法寫在ViewController的dealloc方法里面,試了下,發(fā)現(xiàn)報錯壞內(nèi)存訪問:

這是為什么呢?這就是我們上面講的waitUntilDone造成的.我們在[self performSelector:@selector(stopRunLoop) onThread:self.thread withObject:nil waitUntilDone:NO];中把waitUntilDone設(shè)置為NO,就表示在子線程執(zhí)行的stopRunLoop函數(shù)和在主線程執(zhí)行的- (IBAction)stop函數(shù)是同時執(zhí)行的.一旦- (IBAction)stop函數(shù)先執(zhí)行完,那么ViewController的dealloc函數(shù)也會立馬執(zhí)行完畢,ViewController就會釋放.這時候再去執(zhí)行stopRunLoop就會報壞內(nèi)存訪問,因為ViewController已經(jīng)釋放了.為什么會崩潰到[[NSRunLoop currentRunLoop]runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];這一行呢?
現(xiàn)在你應該明白為什么會在RunLoop那行代碼報壞內(nèi)存訪問錯誤了吧!
解決辦法也很簡單,dealloc方法里面調(diào)用[self stop],并且將上面NO改成YES。
- (IBAction)stop {
// 在子線程調(diào)用stop
[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self stop];
}
運行代碼,直接返回當前VC,打印:
2021-05-14 15:59:38.055083+0800 Interview03-線程?;頪4787:225673] -[ViewController dealloc]
2021-05-14 15:59:38.055307+0800 Interview03-線程?;頪4787:225794] -[ViewController stopThread] <MJThread: 0x600000a8dd00>{number = 8, name = (null)}
我們點擊返回退出控制器后ViewController釋放了,但是沒有看到線程釋放
其實那個RunLoop的確停掉了,但是停掉之后,他會再次來到while循環(huán)判斷條件:
我們在while循環(huán)中打一個斷點:

這時候當前控制器已經(jīng)被銷毀,weakSelf指針已經(jīng)被清空,這時候!nil獲取的就是YES,所以會再次進入循環(huán)體啟動RunLoop,RunLoop又跑起來了,線程又有事情干了,所以線程不會銷毀。
解決辦法:
while (weakSelf && !weakSelf.isStoped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
再次運行項目,返回當前VC
2021-05-14 16:03:29.471773+0800 Interview03-線程?;頪4863:229852] <MJThread: 0x600000452e40>{number = 9, name = (null)}----end----
2021-05-14 16:03:29.472196+0800 Interview03-線程保活[4863:229852] -[MJThread dealloc]
再次運行項目,點擊暫停,返回當前VC,這時候又崩了

點擊暫停之后RunLoop肯定停掉了,RunLoop停掉后,這時候的線程就不能用了,runloop停止掉后它的任務就執(zhí)行完了,線程的生命周期已經(jīng)結(jié)束了,這時候它已經(jīng)不能再執(zhí)行任務了.但是這時候thread還沒銷毀(還沒調(diào)用dealloc),因為thread還被self引用著,我們點擊返回按鈕,又讓子線程去執(zhí)行stopRunLoop任務就會報錯,這時候訪問一個不能用的thread就會報壞內(nèi)存訪問錯誤。
解決辦法也很簡單,暫停RunLoop后把thread指針置為nil,并且如果發(fā)現(xiàn)子線程為nil就不在子線程執(zhí)行任務了。

最后的完整代碼
#import "ViewController.h"
#import "MJThread.h"
@interface ViewController ()
@property (strong, nonatomic) MJThread *thread;
@property (assign, nonatomic, getter=isStoped) BOOL stopped;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.stopped = NO;
self.thread = [[MJThread alloc] initWithBlock:^{
NSLog(@"%@----begin----", [NSThread currentThread]);
// 往RunLoop里面添加Source\Timer\Observer
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
while (weakSelf && !weakSelf.isStoped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
NSLog(@"%@----end----", [NSThread currentThread]);
// NSRunLoop的run方法是無法停止的,它專門用于開啟一個永不銷毀的線程(NSRunLoop)
// [[NSRunLoop currentRunLoop] run];
/*
it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:.
In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers
*/
}];
[self.thread start];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
// 子線程需要執(zhí)行的任務
- (void)test
{
NSLog(@"%s %@", __func__, [NSThread currentThread]);
NSLog(@"??????????????????????????");
}
- (IBAction)stop {
// 在子線程調(diào)用stop
if (!self.thread) return;
[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}
// 用于停止子線程的RunLoop
- (void)stopThread
{
// 設(shè)置標記為NO
self.stopped = YES;
// 停止RunLoop
CFRunLoopStop(CFRunLoopGetCurrent());
self.thread = nil;
NSLog(@"%s %@", __func__, [NSThread currentThread]);
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self stop];
}
@end
特別備注
本系列文章總結(jié)自MJ老師在騰訊課堂iOS底層原理班(下)/OC對象/關(guān)聯(lián)對象/多線程/內(nèi)存管理/性能優(yōu)化,相關(guān)圖片素材均取自課程中的課件。如有侵權(quán),請聯(lián)系我刪除,謝謝!