RunLoop 是什么
runloop
就是一個運行循環(huán)获三,目的是讓程序運行起來不會直接結(jié)束层坠,能在有任務的時候處理任務退盯,沒有任務的時候等待處理任務奄喂。
iOS 中有兩套API 可以訪問 runloop
- Foundation : NSRunLoop
- Core Foundation : CFRunLoopRef
參考
- Runloop 源碼 Objective-C 版本:OC_CFRunLoop
- Runloop 源碼最新的 Swift 版本:swift_CFRunLoop.c
- 官方源碼下載地址:https://opensource.apple.com/tarballs/CF/
- 官方文檔 Run Loops
RunLoop 與線程的關(guān)系
- 每條線程都有唯一的一個與之對應的RunLoop對象
- RunLoop保存在一個全局的Dictionary里婆赠,線程作為key绵脯,RunLoop作為value
- 線程剛創(chuàng)建時并沒有RunLoop對象,RunLoop會在第一次獲取它時創(chuàng)建
- RunLoop會在線程結(jié)束時銷毀
- 主線程的RunLoop已經(jīng)自動獲纫吃濉(創(chuàng)建)桨嫁,子線程默認沒有開啟RunLoop
獲取 RunLoop
對象
Foundation
[NSRunLoop currentRunLoop]; // 獲得當前線程的RunLoop對象
[NSRunLoop mainRunLoop]; // 獲得主線程的RunLoop對象
Core Foundation
CFRunLoopGetCurrent(); // 獲得當前線程的RunLoop對象
CFRunLoopGetMain(); // 獲得主線程的RunLoop對象
可以從 CFRunLoopGetCurrent() 源碼中看到相關(guān)獲取邏輯,代碼簡化如下
CFRunLoopRef CFRunLoopGetCurrent(void) {
// 返回根據(jù)當前線程取到的runloop
return _CFRunLoopGet0(pthread_self());
}
// 核心邏輯如下,已簡化代碼
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
// 1.判斷如果沒有runloop字典
if (!__CFRunLoops) {
// 2.創(chuàng)建一個字典
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 3.根據(jù)主線程創(chuàng)建runloop
CFRunLoopRef mainLoop = __CFRunLoopCreate(_CFMainPThread);
// 4.在dict中將主線程指針和當前創(chuàng)建的runloop以 key:value 形式保存
CFDictionarySetValue(dict, pthreadPointer(_CFMainPThread), mainLoop);
// 5.dict 賦值給 __CFRunLoops(代碼經(jīng)簡化份帐,源碼并非如此簡單)
__CFRunLoops = dict;
}
// 6. 從 __CFRunLoops 中根據(jù)參數(shù)t指針獲取runloop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
// 7. 如果沒有就創(chuàng)建一個新loop對象
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
// 8. __CFRunLoops 中添加參數(shù)t的指針和新loop對象保存
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
// 9. 新runloop 對象就是要返回的loop對象
loop = newLoop;
}
return loop;
}
runloop 內(nèi)部數(shù)據(jù)結(jié)構(gòu)
runloop 內(nèi)部核心數(shù)據(jù)
struct __CFRunLoop {
pthread_t _pthread; // 對應的線程
CFMutableSetRef _commonModes; // 通用Mode
CFMutableSetRef _commonModeItems; // 通用mode的items
CFRunLoopModeRef _currentMode; // 當前mode
CFMutableSetRef _modes; // 所有的mode
};
從數(shù)據(jù)結(jié)構(gòu)中可以看到璃吧,實際就是 runloop 中包含自己的 thread 和 mode, CFRunLoopModeRef 類型結(jié)構(gòu)如下
struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
};
可以看到 runloop 的內(nèi)部結(jié)構(gòu)基本如圖
runloop 的運行模式 CFRunLoopModeRef
- CFRunLoopModeRef代表RunLoop的運行模式
- 一個RunLoop包含若干個Mode废境,每個Mode又包含若干個Source0/Source1/Timer/Observer
- RunLoop啟動時只能選擇其中一個Mode畜挨,作為currentMode
- 如果需要切換Mode,只能退出當前Loop噩凹,再重新選擇一個Mode進入
- 不同組的Source0/Source1/Timer/Observer能分隔開來巴元,互不影響
- 如果Mode里沒有任何Source0/Source1/Timer/Observer,RunLoop會立馬退出
- 常見的2種Mode
- kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默認Mode驮宴,通常主線程是在這個Mode下運行
- UITrackingRunLoopMode:界面跟蹤 Mode逮刨,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響
Runloop 的運行邏輯
說白了 runloop 就是在整循環(huán)中一直檢查當前運行的 mode 內(nèi)部有沒有需要處理的事件堵泽,有就處理修己,沒有就休眠
Runloop 的 mode 內(nèi)部主要有下面4類事件要處理
-
Source0
- 觸摸事件處理
- performSelector:onThread:
-
Source1
- 基于Port的線程間通信
- 系統(tǒng)事件捕捉
-
Timers
- NSTimer
- performSelector:withObject:afterDelay:
-
Observers
- 用于監(jiān)聽RunLoop的狀態(tài)
- UI刷新(BeforeWaiting)
- Autorelease pool(BeforeWaiting)
一次循環(huán)處理事件的邏輯如下
一個簡單的線程保活
@interface PermenantThread()
@property (strong, nonatomic) NSThread *innerThread;
@end
@implementation PermenantThread
#pragma mark - public methods
- (instancetype)init
{
if (self = [super init]) {
self.innerThread = [[NSThread alloc] initWithBlock:^{
// 創(chuàng)建上下文(要初始化一下結(jié)構(gòu)體)
CFRunLoopSourceContext context = {0};
// 創(chuàng)建source
CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
// 往Runloop中添加source
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
// 銷毀source
CFRelease(source);
// 啟動
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
// while (weakSelf && !weakSelf.isStopped) {
// // 第3個參數(shù):returnAfterSourceHandled迎罗,設置為true睬愤,代表執(zhí)行完source后就會退出當前l(fā)oop
// CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
// }
NSLog(@"end----");
}];
[self.innerThread start];
}
return self;
}
- (void)executeTask:(PermenantThreadTask)task
{
if (!self.innerThread || !task) return;
[self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];
}
- (void)stop
{
if (!self.innerThread) return;
[self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self stop];
}
#pragma mark - private methods
- (void)__stop
{
CFRunLoopStop(CFRunLoopGetCurrent());
self.innerThread = nil;
}
- (void)__executeTask:(PermenantThreadTask)task
{
task();
}
@end
面試題
- 講講 Runloop,項目中有用到嗎纹安?
1. 用到過
2. 線程庇热瑁活砂豌,對于頻繁需要在子線程做的操作,使用 runloo 進行線程惫舛剑活 -- 適用于一個串行線程阳距,非并發(fā)的情況。如需要一個后臺線程持續(xù)做否些計算结借,網(wǎng)絡請求娄涩。
3. 如 AFNetworking 2.0版本的時候就是后臺有一個常駐線程
1. 實例
1. 控制線程聲明周期(線程保活)
2. 解決NSTimer在滑動時停止工作的問題
3. 監(jiān)控應用卡頓
4. 性能優(yōu)化
- runloop 內(nèi)部實現(xiàn)邏輯映跟?
1. runloop 的創(chuàng)建邏輯,從內(nèi)部的 dict 中獲取,沒有就新建扬虚,并保存努隙,目的是一條線程只對應一個runloop
2. runloop 的運行邏輯,本質(zhì)上就是一個 while() 循環(huán)辜昵。每次進入循環(huán)的時候處理如上圖事件荸镊。沒有事件的時候進入休眠(通過調(diào)用內(nèi)核接口實現(xiàn))。
- runloop 和線程的關(guān)系堪置?
是一一對應的關(guān)系躬存。
其實保存在 __CFRunLoops 全局 dict 中,以線程指針為key,線程對應的runloop地址為value
- timer 和 線程的關(guān)系舀锨?
- 程序中添加每3秒響應一次的NSTimer岭洲,當拖動 tableView 時候 timer 可能無法響應怎么解決?
將 timer 添加到 commonMode中坎匿,這樣就可以處理普通模式和tracking 模式下的事件了
- runloop 是怎么影響用戶操作的盾剩,具體流程是什么樣的?
由 source1 捕捉用戶的觸摸事件替蔬, 由 source0 處理用戶的觸摸事件告私。 V158T13
- 說說 runloop 的幾種狀態(tài)?
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
- runloop 的 mode 作用是什么承桥?
不同 mode 將各自的 source驻粟、timer、observer 隔離開凶异,這樣同一時間只能執(zhí)行一個 mode 下的 source蜀撑、timer、observer唠帝,這樣在某種模式下就會比較流暢屯掖。