iOS底層原理總結(jié) - RunLoop
面試題
- 講講 RunLoop,項目中有用到嗎睦柴?
- RunLoop內(nèi)部實現(xiàn)邏輯徙融?
- Runloop和線程的關(guān)系毁菱?
- timer 與 Runloop 的關(guān)系窄瘟?
- 程序中添加每3秒響應(yīng)一次的NSTimer,當拖動tableview時timer可能無法響應(yīng)要怎么解決趟卸?
- Runloop 是怎么響應(yīng)用戶操作的蹄葱, 具體流程是什么樣的?
- 說說RunLoop的幾種狀態(tài)锄列?
- Runloop的mode作用是什么图云?
一. RunLoop簡介
運行循環(huán),在程序運行過程中循環(huán)做一些事情邻邮,如果沒有Runloop程序執(zhí)行完畢就會立即退出竣况,如果有Runloop程序會一直運行,并且時時刻刻在等待用戶的輸入操作筒严。RunLoop可以在需要的時候自己跑起來運行丹泉,在沒有操作的時候就停下來休息。充分節(jié)省CPU資源鸭蛙,提高程序性能摹恨。
二. RunLoop基本作用:
- 保持程序持續(xù)運行,程序一啟動就會開一個主線程娶视,主線程一開起來就會跑一個主線程對應(yīng)的RunLoop,RunLoop保證主線程不會被銷毀晒哄,也就保證了程序的持續(xù)運行
- 處理App中的各種事件(比如:觸摸事件睁宰,定時器事件,Selector事件等)
-
節(jié)省CPU資源寝凌,提高程序性能柒傻,程序運行起來時,當什么操作都沒有做的時候较木,RunLoop就告訴CPU红符,現(xiàn)在沒有事情做,我要去休息劫映,這時CPU就會將其資源釋放出來去做其他的事情违孝,當有事情做的時候RunLoop就會立馬起來去做事情 我們先通過API內(nèi)一張圖片來簡單看一下RunLoop內(nèi)部運行原理
通過圖片可以看出,RunLoop在跑圈過程中泳赋,當接收到Input sources 或者 Timer sources時就會交給對應(yīng)的處理方去處理雌桑。當沒有事件消息傳入的時候,RunLoop就休息了祖今。這里只是簡單的理解一下這張圖校坑,接下來我們來了解RunLoop對象和其一些相關(guān)類,來更深入的理解RunLoop運行流程千诬。
三. RunLoop在哪里開啟
UIApplicationMain函數(shù)內(nèi)啟動了Runloop耍目,程序不會馬上退出,而是保持運行狀態(tài)徐绑。因此每一個應(yīng)用必須要有一個runloop邪驮, 我們知道主線程一開起來,就會跑一個和主線程對應(yīng)的RunLoop傲茄,那么RunLoop一定是在程序的入口main函數(shù)中開啟毅访。
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
復(fù)制代碼
進入UIApplicationMain
UIKIT_EXTERN int UIApplicationMain(int argc, char *argv[], NSString * __nullable principalClassName, NSString * __nullable delegateClassName);
復(fù)制代碼
我們發(fā)現(xiàn)它返回的是一個int數(shù),那么我們對main函數(shù)做一些修改
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"開始");
int re = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
NSLog(@"結(jié)束");
return re;
}
}
復(fù)制代碼
運行程序盘榨,我們發(fā)現(xiàn)只會打印開始喻粹,并不會打印結(jié)束,這說明在UIApplicationMain函數(shù)中草巡,開啟了一個和主線程相關(guān)的RunLoop守呜,導(dǎo)致UIApplicationMain不會返回,一直在運行中山憨,也就保證了程序的持續(xù)運行查乒。 我們來看到RunLoop的源碼
// 用DefaultMode啟動
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
復(fù)制代碼
我們發(fā)現(xiàn)RunLoop確實是do while通過判斷result的值實現(xiàn)的。因此萍歉,我們可以把RunLoop看成一個死循環(huán)侣颂。如果沒有RunLoop,UIApplicationMain函數(shù)執(zhí)行完畢之后將直接返回枪孩,也就沒有程序持續(xù)運行一說了憔晒。
四. RunLoop對象
Fundation框架 (基于CFRunLoopRef的封裝) NSRunLoop對象
CoreFoundation CFRunLoopRef對象
因為Fundation框架是基于CFRunLoopRef的一層OC封裝藻肄,這里我們主要研究CFRunLoopRef源碼
如何獲得RunLoop對象
Foundation
[NSRunLoop currentRunLoop]; // 獲得當前線程的RunLoop對象
[NSRunLoop mainRunLoop]; // 獲得主線程的RunLoop對象
Core Foundation
CFRunLoopGetCurrent(); // 獲得當前線程的RunLoop對象
CFRunLoopGetMain(); // 獲得主線程的RunLoop對象
復(fù)制代碼
五. RunLoop和線程間的關(guān)系
- 每條線程都有唯一的一個與之對應(yīng)的RunLoop對象
- RunLoop保存在一個全局的Dictionary里,線程作為key,RunLoop作為value
- 主線程的RunLoop已經(jīng)自動創(chuàng)建好了拒担,子線程的RunLoop需要主動創(chuàng)建
- RunLoop在第一次獲取時創(chuàng)建嘹屯,在線程結(jié)束時銷毀
通過源碼查看上述對應(yīng)
// 拿到當前Runloop 調(diào)用_CFRunLoopGet0
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
// 查看_CFRunLoopGet0方法內(nèi)部
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 根據(jù)傳入的主線程獲取主線程對應(yīng)的RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 保存主線程 將主線程-key和RunLoop-Value保存到字典中
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
// 從字典里面拿,將線程作為key從字典里獲取一個loop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
// 如果loop為空从撼,則創(chuàng)建一個新的loop州弟,所以runloop會在第一次獲取的時候創(chuàng)建
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
// 創(chuàng)建好之后,以線程為key runloop為value低零,一對一存儲在字典中婆翔,下次獲取的時候,則直接返回字典內(nèi)的runloop
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFUnlock(&loopsLock);
CFRelease(newLoop);
}
if (pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
復(fù)制代碼
從上面的代碼可以看出掏婶,線程和 RunLoop 之間是一一對應(yīng)的啃奴,其關(guān)系是保存在一個 Dictionary 里。所以我們創(chuàng)建子線程RunLoop時雄妥,只需在子線程中獲取當前線程的RunLoop對象即可[NSRunLoop currentRunLoop];
如果不獲取最蕾,那子線程就不會創(chuàng)建與之相關(guān)聯(lián)的RunLoop,并且只能在一個線程的內(nèi)部獲取其 RunLoop [NSRunLoop currentRunLoop];
方法調(diào)用時老厌,會先看一下字典里有沒有存子線程相對用的RunLoop瘟则,如果有則直接返回RunLoop,如果沒有則會創(chuàng)建一個枝秤,并將與之對應(yīng)的子線程存入字典中醋拧。當線程結(jié)束時,RunLoop會被銷毀淀弹。
六. RunLoop結(jié)構(gòu)體
通過源碼我們找到__CFRunLoop結(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;
};
復(fù)制代碼
除一些記錄性屬性外趁仙,主要來看一下以下兩個成員變量
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
復(fù)制代碼
CFRunLoopModeRef 其實是指向__CFRunLoopMode結(jié)構(gòu)體的指針,__CFRunLoopMode結(jié)構(gòu)體源碼如下
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
char _padding[3];
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
CFMutableDictionaryRef _portToV1SourceMap;
__CFPortSet _portSet;
CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
dispatch_source_t _timerSource;
dispatch_queue_t _queue;
Boolean _timerFired; // set to true by the source when a timer has fired
Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
mach_port_t _timerPort;
Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
DWORD _msgQMask;
void (*_msgPump)(void);
#endif
uint64_t _timerSoftDeadline; /* TSR */
uint64_t _timerHardDeadline; /* TSR */
};
復(fù)制代碼
主要查看以下成員變量
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
復(fù)制代碼
通過上面分析我們知道垦页,CFRunLoopModeRef代表RunLoop的運行模式,一個RunLoop包含若干個Mode干奢,每個Mode又包含若干個Source0/Source1/Timer/Observer痊焊,而RunLoop啟動時只能選擇其中一個Mode作為currentMode。
Source1/Source0/Timers/Observer分別代表什么
1. Source1 : 基于Port的線程間通信
2. Source0 : 觸摸事件忿峻,PerformSelectors
我們通過代碼驗證一下
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"點擊了屏幕");
}
復(fù)制代碼
打斷點之后打印堆棧信息薄啥,當xcode工具區(qū)打印的堆棧信息不全時,可以在控制臺通過“bt”指令打印完整的堆棧信息逛尚,由堆棧信息中可以發(fā)現(xiàn)垄惧,觸摸事件確實是會觸發(fā)Source0事件。
同樣的方式驗證performSelector堆棧信息
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self performSelectorOnMainThread:@selector(test) withObject:nil waitUntilDone:YES];
});
復(fù)制代碼
可以發(fā)現(xiàn)PerformSelectors同樣是觸發(fā)Source0事件
其實绰寞,當我們觸發(fā)了事件(觸摸/鎖屏/搖晃等)后到逊,由IOKit.framework生成一個 IOHIDEvent事件铣口,而IOKit是蘋果的硬件驅(qū)動框架,由它進行底層接口的抽象封裝與系統(tǒng)進行交互傳遞硬件感應(yīng)的事件觉壶,并專門處理用戶交互設(shè)備脑题,由IOHIDServices和IOHIDDisplays兩部分組成,其中IOHIDServices是專門處理用戶交互的铜靶,它會將事件封裝成IOHIDEvents對象叔遂,接著用mach port轉(zhuǎn)發(fā)給需要的App進程,隨后 Source1就會接收IOHIDEvent争剿,之后再回調(diào)__IOHIDEventSystemClientQueueCallback()已艰,__IOHIDEventSystemClientQueueCallback()內(nèi)觸發(fā)Source0贩挣,Source0 再觸發(fā) _UIApplicationHandleEventQueue()手幢。所以觸摸事件看到是在 Source0 內(nèi)的。
3. Timers : 定時器升敲,NSTimer
通過代碼驗證
[NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
NSLog(@"NSTimer ---- timer調(diào)用了");
}];
復(fù)制代碼
打印完整堆棧信息
4. Observer : 監(jiān)聽器捆蜀,用于監(jiān)聽RunLoop的狀態(tài)
七. 詳解RunLoop相關(guān)類及作用
通過上面的分析疮丛,我們對RunLoop內(nèi)部結(jié)構(gòu)有了大致的了解,接下來來詳細分析RunLoop的相關(guān)類辆它。以下為Core Foundation中關(guān)于RunLoop的5個類
CFRunLoopRef - 獲得當前RunLoop和主RunLoop
CFRunLoopModeRef - RunLoop 運行模式誊薄,只能選擇一種,在不同模式中做不同的操作
CFRunLoopSourceRef - 事件源锰茉,輸入源
CFRunLoopTimerRef - 定時器時間
CFRunLoopObserverRef - 觀察者
1. CFRunLoopModeRef
CFRunLoopModeRef代表RunLoop的運行模式 一個 RunLoop 包含若干個 Mode呢蔫,每個Mode又包含若干個Source、Timer飒筑、Observer 每次RunLoop啟動時片吊,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode 如果需要切換Mode协屡,只能退出RunLoop俏脊,再重新指定一個Mode進入,這樣做主要是為了分隔開不同組的Source肤晓、Timer爷贫、Observer,讓其互不影響补憾。如果Mode里沒有任何Source0/Source1/Timer/Observer漫萄,RunLoop會立馬退出 如圖所示:
注意:一種Mode中可以有多個Source(事件源,輸入源盈匾,基于端口事件源例鍵盤觸摸等) Observer(觀察者腾务,觀察當前RunLoop運行狀態(tài)) 和Timer(定時器事件源)。但是必須至少有一個Source或者Timer削饵,因為如果Mode為空岩瘦,RunLoop運行到空模式不會進行空轉(zhuǎn)未巫,就會立刻退出。
系統(tǒng)默認注冊的5個Mode:
RunLoop 有五種運行模式担钮,其中常見的有1.2兩種
1\. kCFRunLoopDefaultMode:App的默認Mode橱赠,通常主線程是在這個Mode下運行
2\. UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動箫津,保證界面滑動時不受其他 Mode 影響
3\. UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode狭姨,啟動完成后就不再使用,會切換到kCFRunLoopDefaultMode
4\. GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode苏遥,通常用不到
5\. kCFRunLoopCommonModes: 這是一個占位用的Mode饼拍,作為標記kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一種真正的Mode
復(fù)制代碼
Mode間的切換
我們平時在開發(fā)中一定遇到過田炭,當我們使用NSTimer每一段時間執(zhí)行一些事情時滑動UIScrollView师抄,NSTimer就會暫停,當我們停止滑動以后教硫,NSTimer又會重新恢復(fù)的情況叨吮,我們通過一段代碼來看一下
代碼中的注釋也很重要,展示了我們探索的過程
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
// 加入到RunLoop中才可以運行
// 1\. 把定時器添加到RunLoop中瞬矩,并且選擇默認運行模式NSDefaultRunLoopMode = kCFRunLoopDefaultMode
// [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 當textFiled滑動的時候茶鉴,timer失效,停止滑動時景用,timer恢復(fù)
// 原因:當textFiled滑動的時候涵叮,RunLoop的Mode會自動切換成UITrackingRunLoopMode模式,因此timer失效伞插,當停止滑動割粮,RunLoop又會切換回NSDefaultRunLoopMode模式,因此timer又會重新啟動了
// 2\. 當我們將timer添加到UITrackingRunLoopMode模式中媚污,此時只有我們在滑動textField時timer才會運行
// [[NSRunLoop mainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
// 3\. 那個如何讓timer在兩個模式下都可以運行呢舀瓢?
// 3.1 在兩個模式下都添加timer 是可以的,但是timer添加了兩次耗美,并不是同一個timer
// 3.2 使用站位的運行模式 NSRunLoopCommonModes標記氢伟,凡是被打上NSRunLoopCommonModes標記的都可以運行,下面兩種模式被打上標簽
//0 : <CFString 0x10b7fe210 [0x10a8c7a40]>{contents = "UITrackingRunLoopMode"}
//2 : <CFString 0x10a8e85e0 [0x10a8c7a40]>{contents = "kCFRunLoopDefaultMode"}
// 因此也就是說如果我們使用NSRunLoopCommonModes幽歼,timer可以在UITrackingRunLoopMode,kCFRunLoopDefaultMode兩種模式下運行
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
NSLog(@"%@",[NSRunLoop mainRunLoop]);
}
-(void)show
{
NSLog(@"-------");
}
復(fù)制代碼
由上述代碼可以看出谬盐,NSTimer不管用是因為Mode的切換甸私,因為如果我們在主線程使用定時器,此時RunLoop的Mode為kCFRunLoopDefaultMode飞傀,即定時器屬于kCFRunLoopDefaultMode皇型,那么此時我們滑動ScrollView時诬烹,RunLoop的Mode會切換到UITrackingRunLoopMode,因此在主線程的定時器就不在管用了弃鸦,調(diào)用的方法也就不再執(zhí)行了绞吁,當我們停止滑動時,RunLoop的Mode切換回kCFRunLoopDefaultMode唬格,所以NSTimer就又管用了家破。
同樣道理的還有ImageView的顯示,我們直接來看代碼购岗,不再贅述了
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%s",__func__);
// performSelector默認是在default模式下運行汰聋,因此在滑動ScrollView時,圖片不會加載
// [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"abc"] afterDelay:2.0 ];
// inModes: 傳入Mode數(shù)組
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"abc"] afterDelay:2.0 inModes:@[NSDefaultRunLoopMode,UITrackingRunLoopMode]];
復(fù)制代碼
使用GCD也可是創(chuàng)建計時器喊积,而且更為精確我們來看一下代碼
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//創(chuàng)建隊列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//1.創(chuàng)建一個GCD定時器
/*
第一個參數(shù):表明創(chuàng)建的是一個定時器
第四個參數(shù):隊列
*/
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 需要對timer進行強引用烹困,保證其不會被釋放掉,才會按時調(diào)用block塊
// 局部變量乾吻,讓指針強引用
self.timer = timer;
//2.設(shè)置定時器的開始時間,間隔時間,精準度
/*
第1個參數(shù):要給哪個定時器設(shè)置
第2個參數(shù):開始時間
第3個參數(shù):間隔時間
第4個參數(shù):精準度 一般為0 在允許范圍內(nèi)增加誤差可提高程序的性能
GCD的單位是納秒 所以要*NSEC_PER_SEC
*/
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
//3.設(shè)置定時器要執(zhí)行的事情
dispatch_source_set_event_handler(timer, ^{
NSLog(@"---%@--",[NSThread currentThread]);
});
// 啟動
dispatch_resume(timer);
}
復(fù)制代碼
2. CFRunLoopSourceRef事件源(輸入源)
Source分為兩種
Source0:非基于Port的 用于用戶主動觸發(fā)的事件(點擊button 或點擊屏幕)
Source1:基于Port的 通過內(nèi)核和其他線程相互發(fā)送消息(與內(nèi)核相關(guān))
觸摸事件及PerformSelectors會觸發(fā)Source0事件源在前文已經(jīng)驗證過髓梅,這里不在贅述
3. CFRunLoopObserverRef
CFRunLoopObserverRef是觀察者,能夠監(jiān)聽RunLoop的狀態(tài)改變
我們直接來看代碼绎签,給RunLoop添加監(jiān)聽者枯饿,監(jiān)聽其運行狀態(tài)
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//創(chuàng)建監(jiān)聽者
/*
第一個參數(shù) CFAllocatorRef allocator:分配存儲空間 CFAllocatorGetDefault()默認分配
第二個參數(shù) CFOptionFlags activities:要監(jiān)聽的狀態(tài) kCFRunLoopAllActivities 監(jiān)聽所有狀態(tài)
第三個參數(shù) Boolean repeats:YES:持續(xù)監(jiān)聽 NO:不持續(xù)
第四個參數(shù) CFIndex order:優(yōu)先級,一般填0即可
第五個參數(shù) :回調(diào) 兩個參數(shù)observer:監(jiān)聽者 activity:監(jiān)聽的事件
*/
/*
所有事件
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理Source
kCFRunLoopBeforeWaiting = (1UL << 5), //即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6),// 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7),// 即將退出RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
*/
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"RunLoop進入");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"RunLoop要處理Timers了");
break;
case kCFRunLoopBeforeSources:
NSLog(@"RunLoop要處理Sources了");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop要休息了");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"RunLoop醒來了");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop退出了");
break;
default:
break;
}
});
// 給RunLoop添加監(jiān)聽者
/*
第一個參數(shù) CFRunLoopRef rl:要監(jiān)聽哪個RunLoop,這里監(jiān)聽的是主線程的RunLoop
第二個參數(shù) CFRunLoopObserverRef observer 監(jiān)聽者
第三個參數(shù) CFStringRef mode 要監(jiān)聽RunLoop在哪種運行模式下的狀態(tài)
*/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
/*
CF的內(nèi)存管理(Core Foundation)
凡是帶有Create辜御、Copy鸭你、Retain等字眼的函數(shù),創(chuàng)建出來的對象擒权,都需要在最后做一次release
GCD本來在iOS6.0之前也是需要我們釋放的袱巨,6.0之后GCD已經(jīng)納入到了ARC中,所以我們不需要管了
*/
CFRelease(observer);
}
復(fù)制代碼
我們來看一下輸出
以上可以看出碳抄,Observer確實用來監(jiān)聽RunLoop的狀態(tài)愉老,包括喚醒,休息剖效,以及處理各種事件嫉入。
八. RunLoop處理邏輯
這時我們再來分析RunLoop的處理邏輯,就會簡單明了很多璧尸,現(xiàn)在回頭看官方文檔RunLoop的處理邏輯咒林,對RunLoop的處理邏輯有新的認識。
源碼解析
下面源碼僅保留了主流程代碼
// 共外部調(diào)用的公開的CFRunLoopRun方法爷光,其內(nèi)部會調(diào)用CFRunLoopRunSpecific
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
// 經(jīng)過精簡的 CFRunLoopRunSpecific 函數(shù)代碼垫竞,其內(nèi)部會調(diào)用__CFRunLoopRun函數(shù)
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
// 通知Observers : 進入Loop
// __CFRunLoopDoObservers內(nèi)部會調(diào)用 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
函數(shù)
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// 核心的Loop邏輯
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 通知Observers : 退出Loop
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
// 精簡后的 __CFRunLoopRun函數(shù),保留了主要代碼
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
int32_t retVal = 0;
do {
// 通知Observers:即將處理Timers
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 通知Observers:即將處理Sources
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 處理Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 處理Sources0
if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
// 處理Blocks
__CFRunLoopDoBlocks(rl, rlm);
}
// 如果有Sources1,就跳轉(zhuǎn)到handle_msg標記處
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
goto handle_msg;
}
// 通知Observers:即將休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
// 進入休眠欢瞪,等待其他消息喚醒
__CFRunLoopSetSleeping(rl);
__CFPortSetInsert(dispatchPort, waitSet);
do {
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
} while (1);
// 醒來
__CFPortSetRemove(dispatchPort, waitSet);
__CFRunLoopUnsetSleeping(rl);
// 通知Observers:已經(jīng)喚醒
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
handle_msg: // 看看是誰喚醒了RunLoop活烙,進行相應(yīng)的處理
if (被Timer喚醒的) {
// 處理Timer
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
}
else if (被GCD喚醒的) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else { // 被Sources1喚醒的
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply);
}
// 執(zhí)行Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 根據(jù)之前的執(zhí)行結(jié)果,來決定怎么做遣鼓,為retVal賦相應(yīng)的值
if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;
}
} while (0 == retVal);
return retVal;
}
復(fù)制代碼
上述源代碼中啸盏,相應(yīng)處理事件函數(shù)內(nèi)部還會調(diào)用更底層的函數(shù),內(nèi)部調(diào)用才是真正處理事件的函數(shù)骑祟,通過上面bt打印全部堆棧信息也可以得到驗證回懦。
__CFRunLoopDoObservers 內(nèi)部調(diào)用 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
__CFRunLoopDoBlocks 內(nèi)部調(diào)用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
__CFRunLoopDoSources0 內(nèi)部調(diào)用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
__CFRunLoopDoTimers 內(nèi)部調(diào)用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
GCD 調(diào)用 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
__CFRunLoopDoSource1 內(nèi)部調(diào)用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
RunLoop處理邏輯流程圖
此時我們按照源碼重新整理一下RunLoop處理邏輯就會很清晰
九. RunLoop退出
- 主線程銷毀RunLoop退出
- Mode中有一些Timer 、Source曾我、 Observer粉怕,這些保證Mode不為空時保證RunLoop沒有空轉(zhuǎn)并且是在運行的,當Mode中為空的時候抒巢,RunLoop會立刻退出
- 我們在啟動RunLoop的時候可以設(shè)置什么時候停止
[NSRunLoop currentRunLoop]runUntilDate:<#(nonnull NSDate *)#>
[NSRunLoop currentRunLoop]runMode:<#(nonnull NSString *)#> beforeDate:<#(nonnull NSDate *)#>
復(fù)制代碼
十. RunLoop應(yīng)用
1. 常駐線程
常駐線程的作用:我們知道贫贝,當子線程中的任務(wù)執(zhí)行完畢之后就被銷毀了,那么如果我們需要開啟一個子線程蛉谜,在程序運行過程中永遠都存在稚晚,那么我們就會面臨一個問題,如何讓子線程永遠活著型诚,這時就要用到常駐線程:給子線程開啟一個RunLoop 注意:子線程執(zhí)行完操作之后就會立即釋放客燕,即使我們使用強引用引用子線程使子線程不被釋放,也不能給子線程再次添加操作狰贯,或者再次開啟也搓。 子線程開啟RunLoop的代碼,先點擊屏幕開啟子線程并開啟子線程RunLoop涵紊,然后點擊button傍妒。
#import "ViewController.h"
@interface ViewController ()
@property(nonatomic,strong)NSThread *thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 創(chuàng)建子線程并開啟
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(show) object:nil];
self.thread = thread;
[thread start];
}
-(void)show
{
// 注意:打印方法一定要在RunLoop創(chuàng)建開始運行之前,如果在RunLoop跑起來之后打印摸柄,RunLoop先運行起來颤练,已經(jīng)在跑圈了就出不來了,進入死循環(huán)也就無法執(zhí)行后面的操作了驱负。
// 但是此時點擊Button還是有操作的嗦玖,因為Button是在RunLoop跑起來之后加入到子線程的,當Button加入到子線程RunLoop就會跑起來
NSLog(@"%s",__func__);
// 1.創(chuàng)建子線程相關(guān)的RunLoop跃脊,在子線程中創(chuàng)建即可宇挫,并且RunLoop中要至少有一個Timer 或 一個Source 保證RunLoop不會因為空轉(zhuǎn)而退出,因此在創(chuàng)建的時候直接加入
// 添加Source [NSMachPort port] 添加一個端口
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
// 添加一個Timer
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//創(chuàng)建監(jiān)聽者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"RunLoop進入");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"RunLoop要處理Timers了");
break;
case kCFRunLoopBeforeSources:
NSLog(@"RunLoop要處理Sources了");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop要休息了");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"RunLoop醒來了");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop退出了");
break;
default:
break;
}
});
// 給RunLoop添加監(jiān)聽者
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 2.子線程需要開啟RunLoop
[[NSRunLoop currentRunLoop]run];
CFRelease(observer);
}
- (IBAction)btnClick:(id)sender {
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
-(void)test
{
NSLog(@"%@",[NSThread currentThread]);
}
@end
復(fù)制代碼
注意:創(chuàng)建子線程相關(guān)的RunLoop酪术,在子線程中創(chuàng)建即可器瘪,并且RunLoop中要至少有一個Timer 或 一個Source 保證RunLoop不會因為空轉(zhuǎn)而退出,因此在創(chuàng)建的時候直接加入,如果沒有加入Timer或者Source娱局,或者只加入一個監(jiān)聽者,運行程序會崩潰
2. 自動釋放池
Timer和Source也是一些變量咧七,需要占用一部分存儲空間衰齐,所以要釋放掉,如果不釋放掉继阻,就會一直積累耻涛,占用的內(nèi)存也就越來越大,這顯然不是我們想要的瘟檩。 那么什么時候釋放抹缕,怎么釋放呢? RunLoop內(nèi)部有一個自動釋放池墨辛,當RunLoop開啟時卓研,就會自動創(chuàng)建一個自動釋放池,當RunLoop在休息之前會釋放掉自動釋放池的東西睹簇,然后重新創(chuàng)建一個新的空的自動釋放池奏赘,當RunLoop被喚醒重新開始跑圈時,Timer,Source等新的事件就會放到新的自動釋放池中太惠,當RunLoop退出的時候也會被釋放磨淌。 注意:只有主線程的RunLoop會默認啟動。也就意味著會自動創(chuàng)建自動釋放池凿渊,子線程需要在線程調(diào)度方法中手動添加自動釋放池梁只。
@autorelease{
// 執(zhí)行代碼
}
復(fù)制代碼
NSTimer、ImageView顯示埃脏、PerformSelector等在上面已經(jīng)有過例子搪锣,這里不再贅述。