目錄
概念
RunLoop與線程的關系
Runloop對外的接口
CFRunLoopSourceRef
Source0
Source1
CFRunLoopTimer
CFRunLoopObserver
RunLoop的Mode
應用場景
Runloop的內部邏輯
Runloop應用
tableView延遲加載圖片,保證流暢
Timer不被ScrollView的滑動影響
AFNetworking
?編輯
PerformSelecter
概念
一般來講線程一次只能執行一個任務,執行完后就會退出,我們現在想實現一個功能:線程一直在處理事件并且不會退出,這就是我們Runloop
的作用。其實很像runloop的名字所表示的,繞著一個圈圈一直跑。
要實現runloop這種模型一個關鍵點就是怎么樣去管理事件/消息,讓線程在沒有處理消息時休眠以避免資源占用、在有消息到來時立刻被喚醒
runloop就是一個可以管理需要處理的消息與事件的一個對象,CFRunLoopRef
可以理解為在 CoreFoundation
框架內的NSRunLoop,它提供了純 C 函數的 API,但是它的API都是線程安全的,而NSRunLoop卻不是線程安全的
RunLoop與線程的關系
基本上所有的線程操作的底層都是對pthread_t
的封裝
而關于RunLoop和線程,蘋果不允許直接創建Runloop
,它只提供了兩個自動獲取的函數:CFRunLoopGetMain
() 和 CFRunLoopGetCurrent
()
CFRunLoopGetMain的實現如下:
這當中用到了函數_CFRunLoopGet0,其實現如下:
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {//如果t不存在,則標記為主線程(即默認情況,默認是主線程)if (pthread_equal(t, kNilPthreadT)) {t = pthread_main_thread_np();}__CFLock(&loopsLock);if (!__CFRunLoops) {__CFUnlock(&loopsLock);//創建全局字典,標記為kCFAllocatorSystemDefaultCFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);//通過主線程 創建主運行循環CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());//利用dict,進行key-value綁定操作,即可以說明,線程和runloop是一一對應的// dict : key valueCFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {CFRelease(dict);}CFRelease(mainLoop);__CFLock(&loopsLock);}//通過其他線程獲取runloopCFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));__CFUnlock(&loopsLock);if (!loop) {//如果沒有獲取到,則新建一個運行循環CFRunLoopRef newLoop = __CFRunLoopCreate(t);__CFLock(&loopsLock);loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));if (!loop) {//將新建的runloop 與 線程進行key-value綁定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;
}
從上面的代碼可以看出,線程和 RunLoop 之間是一一對應的,其關系是保存在一個全局的 Dictionary 里 線程剛創建時并沒有runloop
,我們需要主動獲取,系統才會自動幫我們創建runloop
并加到字典中
Runloop對外的接口
通過前兩個函數我們可以分別獲得當前線程的CFRunLoop對象和主線程的CFRunLoop對象,但是只是獲得了,如果想要讓Runloop運行起來,就還需要一些別的操作。首先先看一下runloop的一些具體結構
在CoreFoundation里面關于RunLoop有5個類:
-
CFRunLoopRef
-
CFRunLoopModeRef
-
CFRunLoopSourceRef
-
CFRunLoopTimerRef
-
CFRunLoopObserverRef
一個Runloop可以包含多個Mode,CFRunLoopModeRef
類沒有對外暴露,只是通過CFRunLoopRef
的接口進行了封裝,他們的關系如下:
可以看到每個model中包含了Source/Timer/Observer的集合,每次調用RunLoop的主函數時,只能指定其中一個Mode,這個Mode被稱作CurrentMode,如果要切換Mode,必須退出Loop,再重新指定一個Mode進入,這樣做可以分隔開不同組的Source/TImer/Observer,避免互相影響
接下來我們分別來看看Source/Timer/Observer這三種結構,首先是Source
CFRunLoopSourceRef
CFRunLoopSourceRef 是事件產生的地方。Source有兩個版本:Source0 和 Source1。兩個的區別主要是RunLoop事件源的不同:
-
Source0:處理非基于端口的事件(如程序內部自定義的事件)
-
Source1:處理基于端口的事件(如來自內核的Mach端口信息)
Source0 需要手動標記(CFRunLoopSourceSignal
)并喚醒 RunLoop 才能觸發,而 Source1 會自動喚醒 RunLoop。
需要明確一個概念,RunLoop主要用來處理異步事件,如用戶輸入、定時器觸發、網絡響應等,這些事件通常被封裝成事件源,然后由RunLoop在適當的時機調度和處理。
Source0
Source0 只包含了一個回調(函數指針),它并不能主動觸發事件
使用時,先調用CFRunLoopSourceSignal(source0)
將 Source 標記為待處理,然后手動調用 CFRunLoopWakeUp(runloop)
來喚醒 RunLoop,讓其處理這個事件
// 假設有一個方法,用于處理按鈕點擊
- (void)buttonClicked {// 手動觸發RunLoop的Source0CFRunLoopSourceSignal(source0);CFRunLoopWakeUp(CFRunLoopGetCurrent()); // 喚醒RunLoop來處理事件
}
?
// 配置Source0
- (void)setupSource0 {CFRunLoopSourceContext context = {0, (__bridge void *)(self), NULL, NULL, NULL, NULL, NULL, NULL, NULL, &callout};source0 = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);CFRunLoopAddSource(CFRunLoopGetCurrent(), source0, kCFRunLoopDefaultMode);
}
?
// Source0的回調函數
void callout(void *info) {NSLog(@"Source0 event triggered.");
}
Source1
Source1
包含了一個 mach_port 和一個回調(函數指針),被用于通過內核和其他線程相互發送消息。這種 Source 能主動喚醒 RunLoop 的線程。
// 配置Source1
- (void)setupSource1 {CFRunLoopSourceContext1 context = {0, (__bridge void *)(self), NULL, NULL, NULL, NULL, NULL, &perform, NULL};CFMessagePortRef localPort = CFMessagePortCreateLocal(kCFAllocatorDefault, CFSTR("com.example.app.port"), &callback, &context, false);CFRunLoopSourceRef source1 = CFMessagePortCreateRunLoopSource(kCFAllocatorDefault, localPort, 0);CFRunLoopAddSource(CFRunLoopGetCurrent(), source1, kCFRunLoopCommonModes);
}
?
// Source1的回調函數
CFDataRef callback(CFMessagePortRef local, SInt32 msgid, CFDataRef data, void *info) {NSLog(@"Received message: %d", msgid);return NULL;
}
?
// Source1的事件執行
void perform(void *info) {NSLog(@"Performing work in response to external event.");
}
使用場景: ? 處理來自其他進程的數據或信號。 ? 監聽系統級事件或網絡事件。
CFRunLoopTimer
這是一個基于時間的觸發器,包含一個時間長度和一個回調,當其加入到 RunLoop
時,RunLoop
會注冊對應的時間點,當時間點到時,RunLoop
會被喚醒以執行那個回調。
CFRunLoopObserver
這是用來監視和相應RunLoop的特定活動的一種對象,通過 CFRunLoopObserver
,開發者可以在 RunLoop
的不同階段插入自定義的代碼來執行特定的任務
可以觀測的時間點有以下幾個:
我們在上面講的 Source/Timer/Observer
被統稱為 mode item,一個item被重復添加到同一個mode時不會多次執行,但是如果一個mode中一個item都沒有,runloop會自動退出,不會進出循環
RunLoop的Mode
剛才在上文講了mode item,modeitem是被加到mode中的,我們現在講一下Runloop的Mode
這里有個概念叫CommonModes,一個Mode可以把自己標記成”Common”屬性(通過將其 ModeName 添加到 RunLoop 的 “commonModes” 中),每當Runloop的內容發生變化時,RunLoop 都會自動將 _commonModeItems 里的 Source/Observer/Timer 同步到具有 Common標記的所有Mode里。
應用場景
比如說之前寫項目時經常遇到的那個滑動時計數器停止計時的問題,用這個點就可以回答:
主線程的Runloop
中有兩個預置的Mode
:
kCFRunLoopDefaultMode
和 UITrackingRunLoopMode
DefalutMode是App平時所處的狀態,TrackingMode是當滑動時所處的狀態,當我們創建NSTimer添加到DefalutMode中,Timer會得到重復回調,但是當我們滾動我們的TableView時,Runloop會切換Mode,由DefalutMode切換為TrackingMode,此時Timer會停止同時不會進行回調,也不會影響到滑動的操作
這時想讓滑動時NSTimer可以繼續運作的話,有兩個方法:
-
一種方法就是將Timer分別加入到兩個Mode
-
另一種方法就是將NSTimer加到最頂層的RunLoop 的 commonModeItems,加入后的ModeItems類型會被Runloop加到具有common屬性的Mode中去,也就是直接將Timer同時加到defaultMode與TrackMode中去
iOS中有5種Mode:
蘋果公開的三種有:
-
NSDefaultRunLoopMode(kCFRunloopDefaultMode):默認狀態,app通常在這個mode下運行
-
UITrackingRunLoopMode:界面跟蹤mode(例如滑動scrollview時不被其他mode影響)
-
NSRunLoopCommonModes(kCFRunLoopCommonModes):是 前兩個mode的集合,可以把自定義mode用CFRunLoopAddCommonMode函數加入到集合中
還有兩種只需了解:
-
GSEventReceiveRunLoopMode
:接收系統內部mode,通常用不到 -
UIInitializationRunLoopMode
:私有,只在app啟動時使用,使用完就不在集合中了
Runloop的內部邏輯
Runloop的邏輯有一張非常經典的圖:
Runloop應用
tableView延遲加載圖片,保證流暢
在快速滑動tableView時,滑動過的圖片會一直加載,但滑動過的圖片都不是我們想要呈現的圖片,如果加載就浪費CPU資源,用RunLoop就可以避免滑動時加載圖片
給ImgaeView的加載圖片的方法指定只有在DefalutMode下才能加載,滑動時不加載圖片
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"imgName.png"] afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
Timer不被ScrollView的滑動影響
上面其實已經講過這個問題了,除了剛才提到的兩個方法,還有一種方法:
用GCD創建定時器,它不會受到RunLoop影響
AFNetworking
在多線程中,線程執行完任務就會退出,那么如果需要反復執行任務的話,就會頻繁地創建與銷毀線程,這樣不僅效率低下,還增加了系統的開銷,因此如果有一個常駐線程來處理這些任務就可以避免這種情況。
一個RunLoop中如果沒有Observer/Timer/Source等items,Runloop會自動退出,因此我們創建一個空的port發送消息給Runloop,以至于Runloop不會退出而是一直常駐
PerformSelecter
當調用 NSObject 的 performSelecter:afterDelay:
后,其內部會自動創建一個Timer加到Runloop中,當時間到了執行回調,如果當前線程沒有Runloop,此方法也會失效