前不久我們我們對RunLoop的底層有了簡單的了解,那我們現(xiàn)在就要把我們學(xué)到的這些東西,實際應(yīng)用到我們的項目中。
Timer定時器問題
我們在vc中創(chuàng)建一個定時器,然后在view上面添加一個滾動視圖,比如說scrollView,可以發(fā)現(xiàn)在scrollView滾動的時候,timer定時器會卡住,停止?jié)L動之后才重新生效。
這個問題比較簡單,也是我們經(jīng)常遇到的。
因為定時器默認(rèn)是添加在了RunLoop的NSDefaultRunLoopMode模式下,scrollView在滾動的時候會進(jìn)入UITrackingRunLoopMode,RunLoop在同一時間只能處理一種mode,所以在滾動的時候,自然定時器就沒法處理,卡住。
解決方法就是我們創(chuàng)建了timer之后,把他add到RunLoop的NSRunLoopCommonModes,NSRunLoopCommonModes其實并不是一種真實的模式,他只是一個標(biāo)志,意味著timer在標(biāo)記為common的模式下都能使用 (標(biāo)記為common 也就是_commonModes數(shù)組)。
這個地方多說一句,這個標(biāo)記為common是啥意思。我們得看回RunLoop結(jié)構(gòu)體的源碼
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
可以看到里面有一個set類型的變量,CFMutableSetRef _commonModes;,被放到這個set中的mode就等于是被標(biāo)記為了common。NSDefaultRunLoopMode和UITrackingRunLoopMode都在里面。
下面是我們創(chuàng)建timer的正確姿勢 ~
//我們平時可能都是用scheduledTimerWithTimeInterval這個方法創(chuàng)建,這個會默認(rèn)把timer添加到runloop的defalut模式下,所以我們使用timerWithTimeInterval創(chuàng)建
NSTimer * timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%d",++ count);
}];
//NSRunLoopCommonModes 并不是一個真的模式 他只是一個標(biāo)記,意味著timer在標(biāo)記為common的模式下都能使用 (標(biāo)記為common 也就是_commonModes數(shù)組)
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
線程保活
線程?;畈⒉皇撬械捻椖慷加玫牡?,他適應(yīng)于那種一直有任務(wù)需要處理的場景,而且注意,一定要是串行的任務(wù)。這種情況下?;钜粭l線程,就可以免去線程創(chuàng)建和銷毀的開銷,提高性能。
具體怎么?;罹€程,我下面先直接把我的代碼貼出來,然后針對一些點在做一系列的說明。(模擬的項目場景是進(jìn)入到一個vc中,開一條線程,然后用這條線程來執(zhí)行任務(wù),當(dāng)然vc銷毀時,線程也要銷毀。)
下面是全部代碼,大家可以先跳過代碼看下面的一些解析。
#import "SecondViewController.h"
@interface MyThread : NSThread
@end
@implementation MyThread
- (void)dealloc {
NSLog(@"%s",__func__);
}
@end
@interface SecondViewController ()
@property (nonatomic, strong) MyThread * thread;
@property (nonatomic, assign, getter=isStopped) BOOL stopped;
@end
@implementation SecondViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
self.stopped = NO;
UIButton * btn = [UIButton buttonWithType:UIButtonTypeCustom];
btn.frame = CGRectMake(40, 100, 100, 40);
btn.backgroundColor = [UIColor blackColor];
[btn setTitle:@"停止" forState:UIControlStateNormal];
[btn addTarget:self action:@selector(stopThread) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];
__weak typeof(self) weakSelf = self;
// 初始化thread
self.thread = [[MyThread alloc] initWithBlock:^{
NSLog(@"--begin--");
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
while (weakSelf && !weakSelf.isStopped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
NSLog(@"--end--");
}];
[self.thread start];
}
- (void)stopThread {
if (!self.thread) return;
// waitUntilDone YES
[self performSelector:@selector(__stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}
// 執(zhí)行這個方法必須要在我們自己創(chuàng)建的這個線程中
- (void)__stopThread {
// 標(biāo)識
self.stopped = YES;
// 停止runloop
CFRunLoopStop(CFRunLoopGetCurrent());
//
self.thread = nil;
}
#pragma mark - 添加touch事件 (每點擊一次 讓線程處理一次事件)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
if (!self.thread) return;
[self performSelector:@selector(threadDoSomething) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)threadDoSomething {
NSLog(@"work--%@",[NSThread currentThread]);
}
#pragma mark - dealloc
- (void)dealloc {
NSLog(@"%s",__func__);
[self stopThread];
}
@end
最頂部新建了一個繼承自NSThread的MyThread類,目的就是為了重寫-dealloc方法,在內(nèi)部有打印內(nèi)容,方便我調(diào)試線程是否被銷毀。在我們真是的項目中,可以不需要這部分。
初始化線程,開啟RunLoop
__weak typeof(self) weakSelf = self;
// 初始化thread
self.thread = [[MyThread alloc] initWithBlock:^{
NSLog(@"--begin--");
//往runloop里面添加source/timer/observer
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
while (weakSelf && !weakSelf.isStopped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
NSLog(@"--end--");
}];
[self.thread start];
這部分是初始化我們的線程,線程的初始化我們一般用的多的是self.thread = [[MyThread alloc] initWithTarget:self selector:@selector(runThread) object:nil];這樣的方法,我是覺得這樣把self傳進(jìn)線程內(nèi)部,可能造成一些循環(huán)引用問題,最后影響vc和thread的銷毀,所以我是用了block的形式。
initWithBlock的意思也就是線程初始化完畢會執(zhí)行block內(nèi)的代碼。一個子線程默認(rèn)是沒有RunLoop的,RunLoop會在第一次獲取的時候創(chuàng)建,所以我們先[NSRunLoop currentRunLoop]獲取RunLoop,也就是創(chuàng)建了我們當(dāng)前線程的RunLoop。
在了解RunLoop底層的時候我們了解到,如果一個RunLoop沒有timer、observer、source,就會退出。我們新創(chuàng)建的RunLoop這些都是沒有的,如果我們不手動的添加,那我們的RunLoop一跑起來就這就會退出的。所以就等于說我們必須手動給RunLoop添加點事情做。
在代碼中我們使用了addPort:forMode這個方法,向當(dāng)前RunLoop添加一個端口讓RunLoop監(jiān)聽。RunLoop有工作做了,自然就不會退出的。
我們在開啟線程的時候,用了一個while循環(huán),通過一個屬性stopped來控制是否跳出循環(huán),然后循環(huán)內(nèi)部使用了- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;這個方法開啟RunLoop。有人有可能會問了,這里的開啟RunLoop為什么不直接使用- (void)run;這個方法。這里我稍微解釋一下:
查閱一下蘋果的文檔可以了解到,這個run方法,內(nèi)部其實也是循環(huán)的調(diào)用了runMode這個方法的,但是這個循環(huán)是永遠(yuǎn)不會停止的,也就是說我們使用run方法開啟的RunLoop是永遠(yuǎn)都不會停下來的,我們調(diào)用了stop之后,也只會停止當(dāng)前的這一次循環(huán),他還是會繼續(xù)run起來的。所以文檔中也提到,如果我們要創(chuàng)建一個可以停下來的RunLoop,用runMode這個方法。所以我們用這個while循環(huán)模擬run的運行原理,但是呢,我們通過stopped這個屬性可以控制循環(huán)的停止。
while里面的條件weakSelf && !weakSelf.isStopped為什么不僅僅使用stopped判斷,而是還要判斷weakSelf是否有值?我們下面會提到的。
兩個stopThread方法
- (void)stopThread {
if (!self.thread) return;
// waitUntilDone YES
[self performSelector:@selector(__stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}
// 執(zhí)行這個方法必須要在我們自己創(chuàng)建的這個線程中
- (void)__stopThread {
// 標(biāo)識置為YES,跳出while循環(huán)
self.stopped = YES;
// 停止runloop的方法
CFRunLoopStop(CFRunLoopGetCurrent());
// RunLoop退出之后,把線程置空釋放,因為RunLoop退出之后就沒法重新開啟了
self.thread = nil;
}
stopThread是給我們的停止button調(diào)用的,但是實際的停止RunLoop操作在__stopThread里面。在stopThread中調(diào)用__stopThread一定要使用performSelector:onThread:這一類的方法,這樣就可以保證在我們指定的線程中執(zhí)行這個方法。如果我們直接調(diào)用__stopThread,就說明是在主線程調(diào)用的,那就代表我們把主線程的RunLoop停掉了,那我們的程序就完了。
touch模擬事件處理
我們在touchBegin方法中,讓我們self.thread執(zhí)行-threadDoSomething這個方法,代表每點擊一次,我們的線程就要處理一次-threadDoSomething中的打印事件。做這個操作是為了檢測看我們每次工作的線程是不是都是我們最開始創(chuàng)建的這一個線程,沒有重新開新線程。
其他細(xì)節(jié)
那我們仔細(xì)觀察的話會發(fā)現(xiàn)一個問題,-threadDoSomething和stopThread這兩個方法中都是用下面這個方法來處理線程間通信
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait
但是兩次調(diào)用傳入的wait參數(shù)是不一樣的。我們要先知道這個waitUntilDone:(BOOL)wait代表什么意思。
如果wait傳的是YES,就代表我們在主線程用self調(diào)用這個performSelector的時候,主線程會等待我們的self.thread這個線程執(zhí)行他需要執(zhí)行的方法,等著self.thread執(zhí)行完方法之后,主線程再繼續(xù)往下走。那如果是NO,肯定就是主線程不會等了,主線程繼續(xù)往下走,然后我們的self.thread去調(diào)用自己該調(diào)用的方法。
那為什么在stop方法中是用的YES?
有這么一個情形,如果我們push進(jìn)這個vc,線程初始化,然后RunLoop開啟,但是我們不想通過點擊停止button來停止,當(dāng)我們點擊導(dǎo)航的back的時候,我也需要銷毀線程。
所以我們在vc的-dealloc方法中也調(diào)用了stopThread方法。那如果stopThread中使用
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait
的時候wait不用YES,而是NO,會出現(xiàn)什么情況,那肯定是crash了。
如果wait是NO,代表我們的主線程不會等待self.thread執(zhí)行__stopThread方法。
#pragma mark - dealloc
- (void)dealloc {
NSLog(@"%s",__func__);
[self stopThread];
}
但是dealloc中主線程調(diào)用完stopThread,之后整個dealloc方法就結(jié)束了,也就是我們的控制器已經(jīng)銷毀了。但是呢這個時候self.thread還在執(zhí)行__stopThread方法呢。__stopThread中還要self變量,但是他其實已經(jīng)銷毀了,所以這個地方就會crash了。所以在stopThread中的wait一定要設(shè)置為YES。
在當(dāng)時寫代碼的時候,這樣確實處理了crash的問題,但是我直接返回值后,RunLoop并沒有結(jié)束,線程沒有銷毀。這就要講到上面說的while判斷條件是weakSelf && !weakSelf.isStopped的原因了。vc執(zhí)行了dealloc之后,self被置為nil了,weakSelf.isStopped也是nil,取非之后條件又成立了,while循環(huán)還要繼續(xù)的走,RunLoop又run起來了。所以這里我們加上weakSelf這個判斷,也就是self必須不為空。
總結(jié)
上面就是我實現(xiàn)的線程?;钸@一功能的代碼和細(xì)節(jié)分析,當(dāng)然我們在實際的項目中可能有多個位置需要線程?;钸@一功能,所以我們應(yīng)該把這一部分做一下簡單的封裝,來方便我們在不同的地方調(diào)用。大家有興趣的可以自己封裝一下,我在寫RunLoop相關(guān)的代碼時,大多用的是OC層的代碼,有興趣的小伙伴可以嘗試一下C語言的API。
RunLoop的應(yīng)用當(dāng)前不止這么一點,還可以監(jiān)控應(yīng)用卡頓,做性能優(yōu)化,這些以后研究明白了再繼續(xù)更博客吧,一起加油。
相關(guān)的功能代碼和封裝已經(jīng)放到github上面了
https://github.com/Sunxb/RunLoopDemo