前不久咱們咱們對RunLoop的底層有了簡單的瞭解,那咱們如今就要把咱們學到的這些東西,實際應用到咱們的項目中。數組
咱們在vc中建立一個定時器,而後在view上面添加一個滾動視圖,好比說scrollView,能夠發如今scrollView滾動的時候,timer定時器會卡住,中止滾動以後才從新生效。性能優化
這個問題比較簡單,也是咱們常常遇到的。ide
由於定時器默認是添加在了RunLoop的NSDefaultRunLoopMode模式下,scrollView在滾動的時候會進入UITrackingRunLoopMode,RunLoop在同一時間只能處理一種mode,因此在滾動的時候,天然定時器就無法處理,卡住。oop
解決方法就是咱們建立了timer以後,把他add到RunLoop的NSRunLoopCommonModes,NSRunLoopCommonModes其實並非一種真實的模式,他只是一個標誌,意味着timer在標記爲common的模式下都能使用 (標記爲common 也就是_commonModes數組)。性能
這個地方多說一句,這個標記爲common是啥意思。咱們得看回RunLoop結構體的源碼優化
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就等因而被標記爲了common。NSDefaultRunLoopMode和UITrackingRunLoopMode都在裏面。ui
下面是咱們建立timer的正確姿式 ~atom
//咱們平時可能都是用scheduledTimerWithTimeInterval這個方法建立,這個會默認把timer添加到runloop的defalut模式下,因此咱們使用timerWithTimeInterval建立 NSTimer * timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"%d",++ count); }]; //NSRunLoopCommonModes 並非一個真的模式 他只是一個標記,意味着timer在標記爲common的模式下都能使用 (標記爲common 也就是_commonModes數組) [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
線程保活並非全部的項目都用的到,他適應於那種一直有任務須要處理的場景,並且注意,必定要是串行的任務。這種狀況下保活一條線程,就能夠免去線程建立和銷燬的開銷,提升性能。線程
具體怎麼保活線程,我下面先直接把個人代碼貼出來,而後針對一些點在作一系列的說明。(模擬的項目場景是進入到一個vc中,開一條線程,而後用這條線程來執行任務,固然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]; } // 執行這個方法必需要在咱們本身建立的這個線程中 - (void)__stopThread { // 標識 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
方法,在內部有打印內容,方便我調試線程是否被銷燬。在咱們真是的項目中,能夠不須要這部分。
__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傳進線程內部,可能形成一些循環引用問題,最後影響vc和thread的銷燬,因此我是用了block的形式。
initWithBlock
的意思也就是線程初始化完畢會執行block內的代碼。一個子線程默認是沒有RunLoop的,RunLoop會在第一次獲取的時候建立,因此咱們先[NSRunLoop currentRunLoop]
獲取RunLoop,也就是建立了咱們當前線程的RunLoop。
在瞭解RunLoop底層的時候咱們瞭解到,若是一個RunLoop沒有timer、observer、source,就會退出。咱們新建立的RunLoop這些都是沒有的,若是咱們不手動的添加,那咱們的RunLoop一跑起來就這就會退出的。因此就等於說咱們必須手動給RunLoop添加點事情作。
在代碼中咱們使用了addPort:forMode
這個方法,向當前RunLoop添加一個端口讓RunLoop監聽。RunLoop有工做作了,天然就不會退出的。
咱們在開啓線程的時候,用了一個while循環,經過一個屬性stopped
來控制是否跳出循環,而後循環內部使用了- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
這個方法開啓RunLoop。有人有可能會問了,這裏的開啓RunLoop爲何不直接使用- (void)run;
這個方法。這裏我稍微解釋一下:
查閱一下蘋果的文檔能夠了解到,這個run方法,內部其實也是循環的調用了runMode這個方法的,可是這個循環是永遠不會中止的,也就是說咱們使用run方法開啓的RunLoop是永遠都不會停下來的,咱們調用了stop以後,也只會中止當前的這一次循環,他仍是會繼續run起來的。因此文檔中也提到,若是咱們要建立一個能夠停下來的RunLoop,用runMode這個方法。因此咱們用這個while循環模擬run的運行原理,可是呢,咱們經過stopped這個屬性能夠控制循環的中止。
while裏面的條件weakSelf && !weakSelf.isStopped
爲何不只僅使用stopped判斷,而是還要判斷weakSelf是否有值?咱們下面會提到的。
- (void)stopThread { if (!self.thread) return; // waitUntilDone YES [self performSelector:@selector(__stopThread) onThread:self.thread withObject:nil waitUntilDone:YES]; } // 執行這個方法必需要在咱們本身建立的這個線程中 - (void)__stopThread { // 標識置爲YES,跳出while循環 self.stopped = YES; // 中止runloop的方法 CFRunLoopStop(CFRunLoopGetCurrent()); // RunLoop退出以後,把線程置空釋放,由於RunLoop退出以後就無法從新開啓了 self.thread = nil; }
stopThread
是給咱們的中止button調用的,可是實際的中止RunLoop操做在__stopThread
裏面。在stopThread
中調用__stopThread
必定要使用performSelector:onThread:
這一類的方法,這樣就能夠保證在咱們指定的線程中執行這個方法。若是咱們直接調用__stopThread
,就說明是在主線程調用的,那就表明咱們把主線程的RunLoop停掉了,那咱們的程序就完了。
咱們在touchBegin
方法中,讓咱們self.thread執行-threadDoSomething
這個方法,表明每點擊一次,咱們的線程就要處理一次-threadDoSomething
中的打印事件。作這個操做是爲了檢測看咱們每次工做的線程是否是都是咱們最開始建立的這一個線程,沒有從新開新線程。
那咱們仔細觀察的話會發現一個問題,-threadDoSomething
和stopThread
這兩個方法中都是用下面這個方法來處理線程間通訊
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait
可是兩次調用傳入的wait參數是不同的。咱們要先知道這個waitUntilDone:(BOOL)wait
表明什麼意思。
若是wait傳的是YES,就表明咱們在主線程用self調用這個performSelector的時候,主線程會等待咱們的self.thread這個線程執行他須要執行的方法,等着self.thread執行完方法以後,主線程再繼續往下走。那若是是NO,確定就是主線程不會等了,主線程繼續往下走,而後咱們的self.thread去調用本身該調用的方法。
那爲何在stop方法中是用的YES?
有這麼一個情形,若是咱們push進這個vc,線程初始化,而後RunLoop開啓,可是咱們不想經過點擊中止button來中止,當咱們點擊導航的back的時候,我也須要銷燬線程。
因此咱們在vc的-dealloc
方法中也調用了stopThread方法。那若是stopThread中使用- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait
的時候wait不用YES,而是NO,會出現什麼狀況,那確定是crash了。
若是wait是NO,表明咱們的主線程不會等待self.thread執行__stopThread
方法。
#pragma mark - dealloc - (void)dealloc { NSLog(@"%s",__func__); [self stopThread]; }
可是dealloc中主線程調用完stopThread,以後整個dealloc方法就結束了,也就是咱們的控制器已經銷燬了。可是呢這個時候self.thread還在執行__stopThread
方法呢。__stopThread
中還要self變量,可是他其實已經銷燬了,因此這個地方就會crash了。因此在stopThread
中的wait必定要設置爲YES。
在當時寫代碼的時候,這樣確實處理了crash的問題,可是我直接返回值後,RunLoop並無結束,線程沒有銷燬。這就要講到上面說的while判斷條件是weakSelf && !weakSelf.isStopped
的緣由了。vc執行了dealloc以後,self被置爲nil了,weakSelf.isStopped也是nil,取非以後條件又成立了,while循環還要繼續的走,RunLoop又run起來了。因此這裏咱們加上weakSelf
這個判斷,也就是self必須不爲空。
上面就是我實現的線程保活這一功能的代碼和細節分析,固然咱們在實際的項目中可能有多個位置須要線程保活這一功能,因此咱們應該把這一部分作一下簡單的封裝,來方便咱們在不一樣的地方調用。你們有興趣的能夠本身封裝一下,我在寫RunLoop相關的代碼時,大多用的是OC層的代碼,有興趣的小夥伴能夠嘗試一下C語言的API。
RunLoop的應用當前不止這麼一點,還能夠監控應用卡頓,作性能優化,這些之後研究明白了再繼續更博客吧,一塊兒加油。