Runloop:運行循環(huán)-死循環(huán)
主要目的:提高性能,有事情就干,沒事情休眠。
參考https://blog.csdn.net/callauxiliary/article/details/107419854
主要應(yīng)用
1,保證線程一直運行,處理事件,比如觸摸事件,時鐘事件,都是由runloop完成。
2,優(yōu)化卡頓:將一次runloop執(zhí)行完的任務(wù),放到多次runloop中執(zhí)行。
3,UI滑動時計時不準(zhǔn)確的問題,設(shè)置定時器的Mode為:NSRunLoopCommonModes。
4,需要在線程上使用performSelector*****方法(運行時方法)。例如
讓UITableView、UICollectionView等延遲加載圖片
[imageView performSelector:@selector(setImage:) withObject:image afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
這算一個優(yōu)化點,這里用的defaultMode,就是滾動的時候不加載圖片,停止?jié)L動加載圖片
Runloop運行原理
當(dāng)你調(diào)用 CFRunLoopRun() 時,線程就會一直停留在這個循環(huán)里;直到超時或被手動停止,該函數(shù)才會返回。每次線程運行RunLoop都會自動處理之前未處理的消息,并且將消息發(fā)送給觀察者,讓事件得到執(zhí)行。
Runloop的生命周期:在第一次獲取時創(chuàng)建,在線程結(jié)束時銷毀。
Runloop要想跑起來,它的內(nèi)部必須要有一個mode,這個mode里面必須有source\observer\timer,至少要有其中的一個。
系統(tǒng)默認(rèn)注冊了5個mode
a.kCFRunLoopDefaultMode:App的默認(rèn)Mode,通常主線程是在這個Mode下運行
b.UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響
c.UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用
d.GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到
e.kCFRunLoopCommonModes: 這是一個占位用的Mode,不是一種真正的Mode
RunLoop運行時首先根據(jù)modeName找到對應(yīng)mode,如果mode里沒有source/timer/observer,直接返回。
主要分為三大步驟:
1、首先根據(jù)modeName找到對應(yīng)的Mode
2、如果model里沒有Source/Timer/Observer,直接返回
3、如果model有Source/Timer/Observer,就會即將進入runloop

詳細步驟
簡單來講就是一個do-while循環(huán),有輸入源就喚醒,沒有就處于休眠狀態(tài),官方解釋鏈接。具體過程如下:
- 通知觀察者開始進入 Runloop
- 通知觀察者開始處理 Timer 事件
- 通知觀察者將要處理非基于 port 的事件
- 啟動準(zhǔn)備好的事件
- 如果基于 port 的事件已經(jīng)準(zhǔn)備好,立即啟動。并進入步驟 9
- 通知觀察者進入休眠狀態(tài)
- 線程進入休眠直到以下事件出現(xiàn)
- 基于 port 的事件源出現(xiàn)
- 定時器啟動
- 設(shè)置的時間已經(jīng)超時
- RunLoop 被喚醒
- 通知觀察者線程將被喚醒
- 處理已經(jīng)進入的事件
- 如果用戶自定義的計時器啟動,處理事件并重啟 Runloop。轉(zhuǎn)到第二步
- 輸入源啟動,傳遞消息
- 如果 Runloop 被顯示喚醒且沒有超時,重啟 Runloop。轉(zhuǎn)到第二步
- 通知觀察者 Runloop 已經(jīng)退出
Runloop組成
兩種對象:NSRunLoop 和 CFRunLoopRef。
CFRunLoopRef 是在 CoreFoundation 框架內(nèi)的,它提供了純 C 函數(shù)的 API,所有這些 API 都是線程安全的。
NSRunLoop 是基于 CFRunLoopRef 的封裝,提供了面向?qū)ο蟮?API,但是這些 API 不是線程安全的。
RunLoop共包含5個類,但公開的只有Source、Timer、Observer相關(guān)的三個類。
1,Timder,時鐘
2,source,事件源:一切事件的來源,按照函數(shù)的調(diào)用棧分為source0:非系統(tǒng)內(nèi)核事件,source1:系統(tǒng)內(nèi)核事件。
3,observer,觀察者,觀察的runloop的循環(huán)周期。
蘋果不允許直接創(chuàng)建 RunLoop,它只提供了兩個自動獲取的函數(shù):CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。
APP啟動過程
1,當(dāng)點擊APP時,操作系統(tǒng)開啟一條線程執(zhí)行程序的main函數(shù)。這個線程就是這個程序的主線程。這個線程是一個常駐線程,因為這個線程的Runloop被開啟了,不會被線程池釋放。
線程和runloop 的關(guān)系
線程和 RunLoop 之間是一一對應(yīng)的,其關(guān)系是保存在一個全局的 Dictionary 里。線程剛創(chuàng)建時并沒有 RunLoop,如果你不主動獲取,那它一直都不會有。RunLoop 的創(chuàng)建是發(fā)生在第一次獲取時,RunLoop 的銷毀是發(fā)生在線程結(jié)束時。你只能在一個線程的內(nèi)部獲取其 RunLoop(主線程除外)
Runloop的作用
1,保證線程不退出。
2,監(jiān)聽所有的事件,比如觸摸事件,時鐘事件,網(wǎng)絡(luò)事件等。
子線程要監(jiān)聽事件,就必須開啟子線程的runloop。
驗證將時鐘添加到runloop才可以實現(xiàn)監(jiān)聽
1,scheduledTimerWithTimeInterval初始化方法默認(rèn)是添加到當(dāng)前線程的runloop的,所以不用顯示的添加。
這種方法添加是NSDefaultRunLoopMode模式,UI事件會中斷響應(yīng)。
-(void)test{
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeerFunC) userInfo:nil repeats:YES];
}
-(void)timeerFunC{
static int num;
NSLog(@"%d",num);
num++;
}
3,子線程結(jié)束runloop
-(void)test{
//要放在子線程,設(shè)置runloop時間,時間過了沒有執(zhí)行的任務(wù),子線程就會結(jié)束,并釋放
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timeerFunC) userInfo:nil repeats:YES];
//將時鐘手動添加到當(dāng)前的runloop,否者時鐘方法不會執(zhí)行
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
//當(dāng)為yes時,取消RunLoop循環(huán)
while (!_finish) {
NSLog(@"來了11");
[[NSRunLoop currentRunLoop]runUntilDate:[NSDate dateWithTimeIntervalSinceReferenceDate:1]];
}
NSLog(@"來了");
});
}
-(void)timeerFunC{
static int num;
NSLog(@"%d",num);
num++;
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"22222");
_finish = YES;
}
2,timerWithTimeInterval初始化方法需要手動將時鐘添加到當(dāng)前線程的runloop中,否則時鐘不能被監(jiān)聽。
-(void)test{
NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timeerFunC) userInfo:nil repeats:YES];
//將時鐘手動添加到當(dāng)前的runloop,否者時鐘方法不會執(zhí)行
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
}
-(void)timeerFunC{
static int num;
NSLog(@"%d",num);
num++;
}
runloop的三種Mode
1,當(dāng)對象使用NSDefaultRunLoopMode(默認(rèn)模式,一般處理網(wǎng)絡(luò)事件和Timer事件),在runloop執(zhí)行UI事件的時候,會暫停響應(yīng)該對象的事件。
2,當(dāng)對象使用UITrackingRunLoopMode(UI模式,一般處理UI事件),只有在runloop執(zhí)行UI事件的時候,才會會暫停響應(yīng)該對象的事件。
3,當(dāng)對象使用NSRunLoopCommonModes(占位模式,這個不是一個真的模式,只是同時添加了上面兩種模式),在runloop執(zhí)行UI事件的時候,也會響應(yīng)該對象的事件(最理想的結(jié)果)。
開啟當(dāng)前線程的runloop
子線程處理需要監(jiān)聽的事件(需要子線程活著一直監(jiān)聽的情況),可以啟動子線程的runloop,讓子線程成為常駐線程,避免被釋放,從而可以響應(yīng)監(jiān)聽的事件。唯一讓線程不被釋放的方法就去啟動他的runloop。
解決NSTimer在滑動時停止工作的問題的辦法
1,設(shè)置定時器的Mode為:NSRunLoopCommonModes
2,將NSTimder放到子線程并且開啟runloop,name就是算是NSDefaultRunLoopMode,滑動UI也不會影響Timer。
NSTimder處理的事件一般放在子線程,并開啟子線程的runloop,避免影響主線程處理事件。
-(void)test{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timeerFunC) userInfo:nil repeats:YES];
//將時鐘手動添加到當(dāng)前的runloop,否者時鐘方法不會執(zhí)行
[[NSRunLoop currentRunLoop]addTimer:timer forMode: NSDefaultRunLoopMode ];
//手動開啟當(dāng)前線程的runloop,這是一個死循環(huán)
[[NSRunLoop currentRunLoop]run];
NSLog(@"這里不會走了");
});
}
-(void)timeerFunC{
static int num;
NSLog(@"%d",num);
num++;
}
GCD實現(xiàn)定時器
Dispatch Source創(chuàng)建定時器timer,優(yōu)于NSTimer
必須要聲明timer屬性強應(yīng)用,不然會被釋放
@property (nonatomic, strong) dispatch_source_t timer;
-(void)test2{
NSLog(@"啟動");
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(_timer, ^{
NSLog(@"間隔一秒執(zhí)行%@",[NSThread currentThread]);
});
dispatch_resume(_timer);
NSLog(@"啟動");
}
優(yōu)化練習(xí)
tableview在快速滑動時,高清圖片造成卡頓優(yōu)化。
原因:tableview在快速滑動時,runloop必須在一次循環(huán)內(nèi),渲染所有的圖片。
思路:runloop每次循環(huán)只加載一張圖片,例如一個屏幕總的可以顯示15張圖片,那么就分15次加入到runloop中加載。相當(dāng)于把一次做完的事情,分成了15次。例如監(jiān)聽kCFRunLoopBeforeWaiting事件,回調(diào)函數(shù)加載圖片,加載完成后刪除這個任務(wù)。每次加載圖片后都會再次到達kCFRunLoopBeforeWaiting,還有圖片任務(wù)就會繼續(xù)執(zhí)行。
通過CFRunLoopObserverRef監(jiān)聽runloop 的狀態(tài),函數(shù)指針回調(diào),Ref是引用,指針的意思。
步驟
1,添加runloop觀察者,觀察runloop 的狀態(tài)變化。
2,將加載圖片的代碼塊放到數(shù)組中。
3,在觀察者回調(diào)中,拿出數(shù)組中加載圖片的代碼執(zhí)行。