作者:Mitchell
Run loop 剖析:
Runloop 接收的輸入事件來自兩種不同的源:輸入源(intput source)和定時源(timer source)。輸入源傳遞異步事件。通常消息來自于其他線程或程序。定時源則傳遞同步事件,發(fā)生在特定時間或者重復的時間間隔。兩種源都使用程序的某一特定的處理歷程來處理到達的時間。
一、什么是RunLoop
- 基本作用
- 保持程序的持續(xù)運行(一個死循環(huán),使app不斷運行)
- 處理App中的各種事件(觸摸、定時器、Selector)
- 節(jié)省CPU資源、提高程序性能:該做事的時候做事,該休息的時候休息。
- 如果沒有RunLoop
int main(int argc,char * argv[]){
NSLog(@"execute main function");---->程序開始
return 0; ------------------------->程序結束
}
- 有 RunLoop
- 由于 main 函數(shù)里面啟動了個 RunLoop,所以程序并不會馬上退出,保持持續(xù)運行狀態(tài)
int main(int argc,char * argv[]){
BOOL running = YES; -------->程序開始
do {------------------------------
// 執(zhí)行各種任務,處理各種事件------持續(xù)運行
}while(running);---------------------
return 0;
}
二、main 函數(shù)中的 RunLoop
- UIApplicationMain函數(shù)內(nèi)部就啟動了一個RunLoop
- 所以UIApplicationMain 函數(shù)一直沒有返回,保持了程序的持續(xù)運行
- 這個默認啟動的 RunLoop 是跟主線程相關聯(lián)的
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
三、RunLoop 的輸入源
- 輸入源異步的發(fā)送消息給你的線程,時間的來源取決于輸入源的種類:基于端口的輸入源和自定義輸入源。基于端口的輸入源監(jiān)聽程序相應的端口。自定義輸入源則監(jiān)聽自定義的事件源。runloop,不關心輸入源是基于端口的還是自定義的。系統(tǒng)會實現(xiàn)兩種輸入源供你使用。兩類輸入源的區(qū)別在于如何顯示:基于端口的輸入源由內(nèi)核自動發(fā)送,而自定義的則需要人工從其他線程發(fā)送。
- 當你創(chuàng)建輸入源的時候,需要將其分配給 runloop 中的一個或多個模式。模式只會在特定事件影響監(jiān)聽的源。大多數(shù)情況下,runloop 運行在默認模式下,但是你也可以使其運行在自定義模式中。若某一源在當前模式下不被監(jiān)聽,那么任何生成的消息只在 runloop 運行在所關聯(lián)的模式下才會被傳遞。
- 基于端口的輸入源
- Cocoa 和 CoreFoundation 內(nèi)置支持使用端口相關的對象和函數(shù)來創(chuàng)建基于端口的源。在 Cocoa 里面你從來不需要直接創(chuàng)建輸入源。只要簡單的創(chuàng)建對象,并使用 NSPort 的方法將該端口添加到 ruhnloop 中。端口對象會自己處理創(chuàng)建和配置的輸入源。
- 配置基于端口的輸入源
配置 NSMachPort 對象
為了和 NSMachPort 對象建立穩(wěn)定的本地連接,你需要創(chuàng)建端口對象并將之加入相應的線程的 run loop。當運行輔助線程的時候,你傳遞端口對象到線程的主體入口點。輔助線程可以使用相同的端口對象將消息返回給原線程。- a) 實現(xiàn)主線程的代碼
- (void)launchThread
{
NSPort* myPort = [NSMachPort port];
if (myPort)
{
//這個類持有即將到來的端口消息
[myPort setDelegate:self];
//將端口作為輸入源安裝到當前的 runLoop
[[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
//當前線程去調(diào)起工作線程
[NSThread detachNewThreadSelector:@selector(LaunchThreadWithPort:)
toTarget:[MyWorkerClass class] withObject:myPort];
}
為了在線程間建立雙向的通信,你需要讓工作線程在簽到的消息中發(fā)送自己的本地端口到主線程。主線程接收到簽到消息后就可以知道輔助線程運行正常,并且??供了發(fā)送消息給輔助線程的方法。
以下代碼顯示了主線程的 handlePortMessage:方法。當由數(shù)據(jù)到達線程的本地端口時,該方法被調(diào)用。當簽到消息到達時,此方法可以直接從輔助線程里面檢索端口并保存下來以備后續(xù)使用。
#define kCheckinMessage 100
//處理從工作線程返回的響應
- (void)handlePortMessage:(NSPortMessage *)portMessage
{
//消息的 id
unsigned int message = [portMessage msgid];
//創(chuàng)建遠的端口
NSPort* distantPort = nil;
if (message == kCheckinMessage)
{
//獲取工作線程關聯(lián)的端口,并設置給遠程端口
distantPort = [portMessage sendPort];
//為了以后的使用保存工作端口
[self storeDistantPort:distantPort];
}
else
{
//處理其他的消息
}
- ***b) 輔助線程的實現(xiàn)代碼***
對于輔助工作線程,你必須配置線程使用特定的端口以發(fā)送消息返回給主要線程。
以下顯示了如何設置工作線程的代碼。創(chuàng)建了線程的自動釋放池后,緊接著創(chuàng)建工作對象驅(qū)動線程運行。工作對象的*** sendCheckinMessage: ***方法創(chuàng)建了工作線程的本地端口并發(fā)送簽到消息回主線程。
//根據(jù)端口信息啟動線程
+(void)LaunchThreadWithPort:(id)inData
{
//設置當前線程和主線程的通信端口
NSPort* distantPort = (NSPort*)inData;
//獲取當前的工作類
MyWorkerClass* workerObj = [[self alloc] init];
//發(fā)送簽到消息
[workerObj sendCheckinMessage:distantPort];
//釋放
[distantPort release];
//讓 runloop 處理事務
do
{
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:[NSDate distantFuture]];
}
while (![workerObj shouldExit]);
[workerObj release];
[pool release];
當使用 NSMachPort 的時候,本地和遠程線程可以使用相同的端口對象在線程間進行單邊通信。換句話說,一個線程創(chuàng)建的本地端口對象成為另一個線程的遠程端口對象。
以下代碼輔助線程的簽到例程,該方法為之后的通信設置自己的本地端口,然后發(fā)送簽到消息給主線程。它使用 LaunchThreadWithPort:方法中收到的端口對象做為目標消息。
//工作線程簽到方法
- (void)sendCheckinMessage:(NSPort*)outPort
{
//保留遠程端口,以便將來使用
[self setRemotePort:outPort];
//創(chuàng)建并且傳遞工作線程的端口
NSPort* myPort = [NSMachPort port];
[myPort setDelegate:self];
[[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
//創(chuàng)建簽到消息
NSPortMessage* messageObj = [[NSPortMessage alloc] initWithSendPort:outPort
receivePort:myPort components:nil];
if (messageObj)
{
//完成配置消息并立即將其發(fā)送
[messageObj setMsgId:setMsgid:kCheckinMessage];
[messageObj sendBeforeDate:[NSDate date]];
}
- ***配置 NSMessagePort 對象***
為了和 NSMeaasgePort 的建立穩(wěn)定的本地連接,你不能簡單的在線程間傳遞端口
對象。遠程消息端口必須通過名字來獲得。在 Cocoa 中這需要你給本地端口指定一個名字,并將名字傳遞到遠程線程以便遠程線程可以獲得合適的端口對象用于通信。以下代碼顯示端口創(chuàng)建,注冊到你想要使用消息端口的進程。
//創(chuàng)建本地消息端口
NSPort* localPort = [[NSMessagePort alloc] init];
//配置對象并且將其添加到當前 runloop 中
[localPort setDelegate:self];
[[NSRunLoop currentRunLoop] addPort:localPort forMode:NSDefaultRunLoopMode];
//使用一個特殊的名字注冊端口,名字必須是唯一的
NSString* localPortName = [NSString stringWithFormat:@"MyPortName"];
[[NSMessagePortNameServer sharedInstance] registerPort:localPort
name:localPortName];
- ***在 Core Foundation 中配置基于端口的源***
這部分介紹了在 Core Foundation 中如何在程序主線程和工作線程間建立雙通道通信。
以下代碼顯示了程序主線程加載工作線程的代碼。第一步是設置
CFMessagePortRef 不透明類型來監(jiān)聽工作線程的消息。工作線程需要端口的名稱來建立連接,以便使字符串傳遞給工作線程的主入口函數(shù)。在當前的用戶上下文中端口名必須是唯一的,否則可能在運行時造成沖突。
#define kThreadStackSize (8 *4096)
OSStatus MySpawnThread()
{
//創(chuàng)建一個本地的端口來接收響應
CFStringRef myPortName;
CFMessagePortRef myPort;
CFRunLoopSourceRef rlSource;
CFMessagePortContext context = {0, NULL, NULL, NULL, NULL};
Boolean shouldFreeInfo;
//創(chuàng)建端口的名稱
myPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR("com.myapp.MainThread"));
//創(chuàng)建端口
myPort = CFMessagePortCreateLocal(NULL,
myPortName,
&MainThreadResponseHandler,
&context,
&shouldFreeInfo);
if (myPort != NULL)
{
//端口被成功的創(chuàng)建了,現(xiàn)在為它創(chuàng)建一個 runloop 源
rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0);
if (rlSource) {
//將源添加到 runloop 中去
CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode);
//一旦被安裝,這些指針可以被釋放了。
CFRelease(myPort);
CFRelease(rlSource);
}}
//創(chuàng)建線程并且持續(xù)的運行
MPTaskID taskID;
return(MPCreateTask(&ServerThreadEntryPoint,
(void*)myPortName,kThreadStackSize,NULL,NULL,
NULL,0,&taskID));
}
端口建立而且線程啟動后,主線程在等待線程簽到時可以繼續(xù)執(zhí)行。當簽到消息到達后,主線程使用 MainThreadResponseHandler 來分發(fā)消息,如下面代碼 所示。這個函數(shù)??取工作線程的端口名,并創(chuàng)建用于未來通信的管道。
#define kCheckinMessage 100
//獲取主線程端口消息持有者
CFDataRef MainThreadResponseHandler(CFMessagePortRef local,
SInt32 msgid, CFDataRef data, void* info)
{
//如果消息是簽到的消息
if (msgid == kCheckinMessage)
{
//消息端口
CFMessagePortRef messagePort;
//線程名稱
CFStringRef threadPortName;
//數(shù)據(jù)長度
CFIndex bufferLength = CFDataGetLength(data);
//讀取數(shù)據(jù)流
UInt8* buffer = CFAllocatorAllocate(NULL, bufferLength, 0);
CFDataGetBytes(data, CFRangeMake(0, bufferLength), buffer);
//設置線程端口名稱
threadPortName = CFStringCreateWithBytes (NULL, buffer,bufferLength,kCFStringEncodingASCII, FALSE);
//你必須獲得一個遠程消息端口的名稱
messagePort = CFMessagePortCreateRemote(NULL,(CFStringRef)threadPortName);
//如果有消息端口
if (messagePort) {
//保留線程公共的端口為了將來的引用
AddPortToListOfActiveThreads(messagePort);
//因為端口已經(jīng)被之前的方法保留,所以這里將指針釋放
CFRelease(messagePort);
}
//清空,釋放線程端口名稱
CFAllocatorDeallocate(NULL, buffer);
}
else
{
//處理其他的信息
}
return NULL;
}
主線程配置好后,剩下的唯一事情是讓新創(chuàng)建的工作線程創(chuàng)建自己的端口然后簽到。以下代碼 顯示了工作線程的入口函數(shù)。函數(shù)獲取了主線程的端口名并使用它來創(chuàng)建和主線程的遠程連接。然后這個函數(shù)創(chuàng)建自己的本地端口號,安裝到線程的 runloop,最后連同本地端口名稱一起發(fā)回主線程簽到。
#工作線程的入口函數(shù)#
OSStatus ServerThreadEntryPoint(void* param)
{
//創(chuàng)建對主線程的遠程端口
CFMessagePortRef mainThreadPort;
//獲取主線程的名稱
CFStringRef portName = (CFStringRef)param;
mainThreadPort = CFMessagePortCreateRemote(NULL, portName);
//釋放作為參數(shù)傳遞過去的字符串的指針
CFRelease(portName);
//創(chuàng)建工作線程的端口
CFStringRef myPortName = CFStringCreateWithFormat(NULL, NULL,CFSTR("com.MyApp.Thread-%d"), MPCurrentTaskID());
//在線程的 context 信息中存儲端口的信息以備以后的使用
CFMessagePortContext context = {0, mainThreadPort, NULL, NULL, NULL};
Boolean shouldFreeInfo;
Boolean shouldAbort = TRUE;
CFMessagePortRef myPort = CFMessagePortCreateLocal(NULL, myPortName,
&ProcessClientRequest, &context, &shouldFreeInfo);
if (shouldFreeInfo)
{
//不能創(chuàng)建本地的端口,那么就殺死線程
MPExit(0);
}
CFRunLoopSourceRef rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0);
if (!rlSource){
//如果沒有創(chuàng)建本地的端口,那么就殺死線程
MPExit(0);
}
//將源添加到當前的 runloop 中去
CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode);
//一旦被安裝完畢,那么就可以被釋放了
CFRelease(myPort);
CFRelease(rlSource);
//將端口名稱和簽到信息打包,并且寫入流
CFDataRef returnData = nil;
CFDataRef outData;
CFIndex stringLength = CFStringGetLength(myPortName);
UInt8* buffer = CFAllocatorAllocate(NULL, stringLength, 0);
CFStringGetBytes(myPortName,
CFRangeMake(0,stringLength),
kCFStringEncodingASCII,
0,
FALSE,
buffer,
stringLength,
NULL);
outData = CFDataCreate(NULL, buffer, stringLength);
CFMessagePortSendRequest(mainThreadPort, kCheckinMessage, outData, 0.1, 0.0, NULL,NULL);
//清空線程數(shù)據(jù)
CFRelease(outData);
CFAllocatorDeallocate(NULL, buffer);
//進入 runLoop
CFRunLoopRun();
}
一旦線程進入了它的runloop,所有發(fā)送到線程端口的時間都會由 ProcessClientRequest 函數(shù)來處理。
- 自定義輸入源
- 為了自定義輸入源,必須使用 Core Foundation里面的 CGRunLoopSourceRef類型相關的函數(shù)來創(chuàng)建。你可以使用回調(diào)函數(shù)來配置自定義輸入源。Corefondation 會在配置源的不同地方調(diào)用回調(diào)函數(shù),處理輸入時間,在源從 runloop 移除的時候清理它。
- 除了定義在事件到達時自定義輸入源的行為,你也必須定義消息傳遞機制。源的這部分運行在單獨的線程里面,并負責在數(shù)據(jù)等待處理的時候傳遞數(shù)據(jù)給源并源并通知它處理數(shù)據(jù)。消息傳遞機制的定義取決于你,但是最好不要過于復雜。
- 創(chuàng)建自定義的輸入源包括定義以下內(nèi)容:
- 輸入源要處理的信息。
- 使感興趣的客戶端知道如何和輸入源交互的調(diào)度例程。
- 處理其他任何客戶端發(fā)送請求的例程。
- 使輸入源失效的取消例程。
- 由于創(chuàng)建輸入源來處理自定義消息,實際配置選是靈活配置的。調(diào)度例程,處理例程和取消例程都是創(chuàng)建自定義輸入源是最關鍵的例程。二輸入源其他的大部分行為都發(fā)生在這些例程的外部。比如,由于你決定數(shù)據(jù)傳輸?shù)捷斎朐吹臋C制,還有輸入源和其他線程的通信機制也是由你決定。
- 圖 3-2 中,程序的主線程維護了一個輸入源的引用,輸入源所需的自定義命令緩沖區(qū)和輸入源所在的 runloop。當主線程有任務需要分發(fā)給工作線程時候,主線程會給命令緩沖區(qū)發(fā)送命令和必須的信息來通知工作線程開始執(zhí)行任務。(因為主線程和輸入源所在工作線程都可以訪問命令緩沖區(qū),因此這些訪問必須是同步的)一旦命令傳送出去,主線程會通知輸入源并且喚醒工作線程的 runloop。而一收到喚醒命令,runloop 會調(diào)用輸入源的處理程序,由它來執(zhí)行命令緩沖區(qū)中響應的命令。
3-2.png - 圖3-2 中的輸入源使用了 Objective-C 的對象輔助 runloop來管理命令緩沖區(qū)。下面代碼給出了改對象的定義。RunLoopSource 對象管理著命令緩沖區(qū)并以此來接收其他線程的消息。例子同樣給出了 RunLoopContext 對象的定義,它是一個用于傳遞 RunLoopSource 對象和 runloop 引用給程序主線程的一個容器。
@interface RunLoopSource : NSObject
{
CFRunLoopSourceRef runLoopSource;
NSMutableArray* commands;
}
- (id)init;
- (void)addToCurrentRunLoop;
- (void)invalidate;
// Handler method
- (void)sourceFired;
// Client interface for registering commands to process
- (void)addCommand:(NSInteger)command withData:(id)data;
- (void)fireAllCommandsOnRunLoop:(CFRunLoopRef)runloop;
@end
//這些是 CFRunLoopSourceRef 的回調(diào)函數(shù)
void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);
void RunLoopSourcePerformRoutine (void *info);
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);
// RunLoopContext is a container object used during registration of the input source.
@interface RunLoopContext : NSObject
{
CFRunLoopRef runLoop;
RunLoopSource* source;
}
@property (readonly) CFRunLoopRef runLoop;
@property (readonly) RunLoopSource* source;
- (id)initWithSource:(RunLoopSource*)src andLoop:(CFRunLoopRef)loop;
盡管使用 Objective-C 代碼來管理輸入源的自定義數(shù)據(jù),但是將輸入源附加到 runloop 卻需要使用基于 C 的回調(diào)函數(shù)(RunLoopSourceScheduleRoutine)。因為這個輸入源只有一個客戶端(主線程),它使用調(diào)度函數(shù)發(fā)送注冊信息給應用程序的委托(delegate)。當委托需要和輸入源通信的時候,它會使用 RunLoopContext 對象來完成。
void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
{
//獲取輸入源
RunLoopSource* obj = (RunLoopSource*)info;
//獲取應用程序的委托
AppDelegate* del = [AppDelegate sharedAppDelegate];
//根據(jù) runloop輸入源 和 runloop 獲取 RunLoopContext,并將這個 RunLoopContext 注冊到主線程,用于委托和輸入源之間的通信。
RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];
//在主線程執(zhí)行注冊源
[del performSelectorOnMainThread:@selector(registerSource:)
withObject:theContext waitUntilDone:NO];
當輸入源被告知的時候回處理自定義數(shù)據(jù)的那個例程,以下代碼展示了這個和 RunLoopSource 對象相關回調(diào)的例程。這里只是簡單的讓 RunLoopSource 執(zhí)行 sourceFired 方法,然后繼續(xù)處理在命令緩存區(qū)出現(xiàn)的命令。
void RunLoopSourcePerformRoutine (void *info)
{
RunLoopSource* obj = (RunLoopSource*)info;
[obj sourceFired];
}
使用 CFRunLoopSourceInvalidate 函數(shù)將輸入源從 runloop 中移除,系統(tǒng)會調(diào)用輸入源的取消例程。可以使用該例程來通知其他客戶端該輸入源已經(jīng)失效,客戶端應該釋放輸入源的引用。
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
{
//獲取源
RunLoopSource* obj = (RunLoopSource*)info;
//獲取系統(tǒng)代理
AppDelegate* del = [AppDelegate sharedAppDelegate];
//根據(jù) 源和 runloop 獲取 RunLoopContext
RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];
//發(fā)送移除源的命令
[del performSelectorOnMainThread:@selector(removeSource:)
withObject:theContext waitUntilDone:YES];
}
- ***安裝輸入源到 RunLoop***
以下代碼顯示了 RunLoopSource 的 init 和 addToCurrentRunloop 的方法。Init 方法創(chuàng)建 CGFunLoopSourceRef 類型,該類型必須被附加到 runloop 里。它將 RunLoopSource 對象作為上下文引用參數(shù),以便回調(diào)例程持有該對象的一個引用指針。輸入源的安裝只在工作線程調(diào)用 addToCurrentRunLoop 方法才發(fā)生,此時 RunLoopSourceScheduledRoutine 被調(diào)用。一旦輸入源被添加到 runloop,線程就運行 runloop 并等待事件。
- (id)init
{
CFRunLoopSourceContext context = {0, self, NULL, NULL, NULL, NULL, NULL,
//CFRunLoopSource 輸入源
runLoopSource = CFRunLoopSourceCreate(NULL, 0, &context);
&RunLoopSourceScheduleRoutine,
RunLoopSourceCancelRoutine,
RunLoopSourcePerformRoutine};
//命令數(shù)組
commands = [[NSMutableArray alloc] init];
return self;
}
- (void)addToCurrentRunLoop
{
//獲取當前 runloop
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
//將源添加到 runloop
CFRunLoopAddSource(runLoop, runLoopSource, kCFRunLoopDefaultMode);
}
- ***協(xié)調(diào)輸入源的客戶端***
為了讓添加的輸入源有用,需要維護它并從其他線程給它發(fā)送信號。輸入源的主要工作就是將于輸入源相關的線程置于休眠狀態(tài)知道有事件發(fā)生。這就意味著程序中的要有其他線程知道該輸入源信息并且有辦法與之通信。
通知客戶端關于輸入源信息的方法之一就是當你的輸入源開始安裝到你的 runloop 上面后阿松注冊請求。將輸入源注冊到任意數(shù)量的客戶端,或者通過代理將輸入源注冊到感興趣的客戶端那。以下代碼顯示了應用委托定義的注冊方法以及它在 RunLoopSource 對象的調(diào)度函數(shù)被調(diào)用時如何運行。該方法接收 RunloopSource 提供的 RunLoopContext 對象,然后將其添加到他自己的源列表里面。
//注冊源
- (void)registerSource:(RunLoopContext*)sourceInfo;
{
[sourcesToPing addObject:sourceInfo];
}
//移除源
- (void)removeSource:(RunLoopContext*)sourceInfo
{
id objToRemove = nil;
for (RunLoopContext* context in sourcesToPing)
{
if ([context isEqual:sourceInfo])
{
objToRemove = context;
break;
}
}
if (objToRemove)
[sourcesToPing removeObject:objToRemove];
}
- ***通知輸入源***
當客戶端發(fā)送數(shù)據(jù)到輸入源之后,它必須發(fā)送信號通知源并且喚醒它的 runloop。發(fā)送信號給源可以讓 runloop 知道該源已經(jīng)做好處理消息的準備。而且因為信號發(fā)送時線程可能處于休眠,所以必須總是顯示的喚醒 runllop。如果不這樣做的話會導致延遲處理輸入源。
當客戶端準備好處理加入緩沖區(qū)的命令后會調(diào)用此方法。
- (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop
{
//給 runLoop 發(fā)送信號
CFRunLoopSourceSignal(runLoopSource);
//顯示的啟動 runLoop
CFRunLoopWakeUp(runloop);
注意:你不應該試圖通過自定義輸入源處理一個 SIGHUP 或其他進程級別類型的信號。CoreFoundation 喚醒 run loop 的函數(shù)不是信號安全的,不能在你的應用信號處理例程(signalhandler routines)里面使用。關于更多信號處理例程,參閱 sigaction 主頁。
- Cocoa 執(zhí)行 Selector 的源
- 除了基于端口的源,Cocoa 定義了自定義的輸入源,允許你在任何線程中執(zhí)行 seletor。和基于端口的源一樣,執(zhí)行 selector 請求會在目標線程上序列化,減緩許多咋線程上允許多個方法容易引起的同步問題。不像基于端口的源,一個 selector 執(zhí)行完后會自動從 runloop 里面移除。
- 當在其他線程上面執(zhí)行 selector 時候,目標線程需有一個活動的 runloop,對于你創(chuàng)建的線程,這意味著線程在你顯示的啟動 runloop 之前處于等待狀態(tài)。由于主線程自己啟動它的 runloop,那么在程序通過委托調(diào)用 applicationDidFinishlaunching:的時候你會遇到線程調(diào)用的問題。因為 RunLoop 通過每次循環(huán)來處理所有隊列的 selector 的調(diào)用,而不是通過 loop 的迭代來處理 selector。
- 定時源
- 定時源在預設的時間點同步方式傳遞消息。定時器是線程通知自己做某事的一種方法。
- 盡管定時器可以產(chǎn)生基于時間的通知,但它并不是實時機制。和輸入源一樣,定時器也和 runloop 的特定模式相關。如果定時器所在的模式當前未被 runloop 監(jiān)視,那么定時器將不會開始知道 runloop 運行在響應的模式下。類似的。如果定時器在 runloop 處理某一事件期間開始,定時器會一直等待直到下次 runloop 開始響應的處理程序。如果 runloop 不運行了,那么定時器也永遠不啟動。
- 配置定時源
Cocoa 中可以使用以下 NSTimer 類方法來創(chuàng)建并調(diào)配一個定時器:??
scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:??
scheduledTimerWithTimeInterval:invocation:repeats:
上述方法創(chuàng)建了定時器并以默認模式把它們添加到當前線程的 run loop。
Core Foundation 創(chuàng)建定時器
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopTimerContext context = {0, NULL, NULL, NULL, NULL};
CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.1, 0.3, 0, 0,
&myCFTimerCallback, &context);
四、RunLoop 對象
- iOS 中有2套 API 來訪問和使用 RunLoop
- Foundation
- NSRunLoop 的 currentRunLoop 類方法類檢索一個 NSRunLoop 對象。
- CoreFoundation
- CFRunLoopGetCurrent 函數(shù)
- Foundation
- NSRunLoop 和 CGRunLoopRef 都代表著 RunLoop 對象
- NSRunLoop 是基于 CFRunLoopRef 的一層 OC 包裝, 所以要了解 RunLoop內(nèi)部結構,需要研究 CFRunLoopRef 層面的 API (Core Foundation層面)
//獲取當前 runloop(NSRunLoop*)
+ (NSRunLoop *)currentRunLoop;
[NSRunLoop currentRunLoop];
//CFRunLoopRef
- (CFRunLoopRef)getCFRunLoop CF_RETURNS_NOT_RETAINED;
[[NSRunLoop currentRunLoop]getCFRunLoop];
- 在輔助線程運行 run loop 之前,必須至少添加已輸入源或定時器給它。如果 runloop 沒有任何源需要監(jiān)視的話,它會在你啟動的時候馬上退出。
- (void)threadMain {
// The application uses garbage collection, so no autorelease pool is needed.
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
// Create a run loop observer and attach it to the run loop.
CFRunLoopObserverContext context = {0, self, NULL, NULL, NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);
if (observer) {
CFRunLoopRef cfLoop = [myRunLoop getCFRunLoop];
CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
}
// Create and schedule the timer.
[NSTimer scheduledTimerWithTimeInterval:0.1 target:self
selector:@selector(doFireTimer:) userInfo:nil repeats:YES];
NSInteger loopCount = 10;
do{
// Run the run loop 10 times to let the timer fire.
[myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
loopCount--;
}
while (loopCount);
}
五、RunLoop 與線程
- 每條線程都有唯一的一個與之對應的 RunLoop 對象
- 主線程的 RunLoop 自動創(chuàng)建好了,子線程的 RunLoop 需要主動創(chuàng)建
- RunLoop 在第一次獲取時創(chuàng)建,在線程結束時銷毀
六、獲取RunLoop 對象
- Foundation
//獲得當前線程的 RunLoop 對象
[NSRunLoop currentRunLoop];
//獲得主線程的 RunLoop 對象
[NSRunLoop mainRunLoop];
- Core Foundation
//當前RunLoop
CFRunLoopGetCurrent();
//主線程 RunLoop
CFRunLoopGetMain();
七、NSRunLoop 相關類
- CoreFoundation 中關于 RunLoop 的5個類
- CFRunLoopRef(運行循環(huán)對象)
- CFRunLoopModeRef(1個runLoop可以有很多個Mode,1個Mode可以有很多個Source Observer Timer,
但是在同一時刻只能同時執(zhí)行一種Mode
,關于更多種類的Mode) - CFRunLoopSourceRef(處理事件)
- CFRunLoopTimerRef(處理定時器相關)
-
CFRunLoopObserverRef(觀察者,觀察是否有事件)
RunLoop.png
- CFRunLoopModeRef 代表 RunLoop 的運行模式
- 一個 RunLoop 包含若干個 Mode,每個Mode 又包含若干個 Source/Timer、Observer
- 每次 RunLoop 啟動時,只能制定其中一個 Mode,這個 Mode 被稱作 CurrentMode
- 如果需要切換 Mode,只能退出 Loop,再重新指定一個 Mode 進入
- 這樣做主要是為了分割開不同組的 Source/Timer/Observer,讓其互不影響
- 系統(tǒng)默認注冊了 5個Mode:
-
kCFRunLoopDefaultMode
:App的默認Mode,通常主線程是在這個 Mode 下運行的 -
UITrackingRunLoopMode
:界面跟蹤 Mode,用于ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他Mode 影響 - UIInitializationRunLoopMode:在剛啟動 App 時第進入的第一個Mode,啟動完成之后就不再使用。
- GSEventReceiveRunLoopMode:接收系統(tǒng)時間的內(nèi)部 Mode,通常用不到。
-
kCFRunLoopCommonModes(比較特殊)
:這時一個占位用的 Mode,不是一種真正的 Mode。
-
- CFRunLoopSourceRef 是事件源(輸入源)
- Source0:非基于Port的
- Custom Input Sources
- Cocoa Perform Selector Sources
- Source1:基于Port的
- Port- Based Sources
- 舉例:輸出點擊事件的調(diào)用棧,
我們可以清楚的看到runloop中做的是__CFRunLoopDoSource0
。
Source0調(diào)用棧.png
- Source0:非基于Port的
- CFRunLoopTimerRef 處理定時器
-
NSTimer 定時器調(diào)用棧:__CFRunLoopDoTimer
timer.png - 注意:使用不同種類的 Mode 會對定時器的效果有不同的展現(xiàn)
-
NSDefaultRunLoopMode
:將NSTimer添加到主線程NSRunLoop的默認模式下,只有主線程是默認模式下才能執(zhí)行NSTimer(滾動scrollView,RunLoop默認進入Tracking模式,所以NSTimer不會有效果)。 -
UITrackingRunLoopMode
:將NSTimer添加到主線程NSRunLoop的追蹤模式下,只有主線程是追蹤模式下才能執(zhí)行NSTimer。(例如滾動scrollView的時候就會監(jiān)聽到計時器) -
NSRunLoopCommonModes
:Common是一個表示,它是將NSDefaultRunLoopMode 和 UITrackingRunLoopMode標記為了Common
所以,只要將 timer 添加到 Common 占位模式下,timer就可以在Default和UITrackingRunLoopMode模式下都能運行
-
-
- 如果用GCD創(chuàng)建計時器:
- GCD 創(chuàng)建的好處,不受 RunLoopMode 的影響。
//1、創(chuàng)建timer
//dispatchQueue:定時器將來回調(diào)的方法在哪個線程中執(zhí)行
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
self.timer = timer;
//2.設置timer
/*
第一個參數(shù):需要設置哪個timer
第二個參數(shù):指定定時器開始的時間
第三個參數(shù):指定間隔時間
第四個參數(shù):定時器的精準度,如果傳0代表要求非常精準(系統(tǒng)會讓計時器執(zhí)行時間變得更加準確,性能消耗也會提高),如果傳入一個大于0的值,代表我們允許的誤差
//例如傳入60,就代表允許誤差有60秒
*/
//設置第一次執(zhí)行的時間
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC));
//DISPATCH_TIME_NOW
dispatch_source_set_timer(timer,start , 2 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
//3、設置timer的回調(diào)
dispatch_source_set_event_handler(timer, ^{
NSLog(@"%@",[NSRunLoop currentRunLoop]);
});
dispatch_resume(timer);
- 在 RunLoop 底層默認會調(diào)用這里
/// 9.1 如果一個 Timer 到時間了,觸發(fā)這個Timer的回調(diào)。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
/// 9.2 如果有dispatch到main_queue的block,執(zhí)行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
- CFRunLoopObserverRef觀察者,能夠監(jiān)聽RunLoop狀態(tài)改變
- 監(jiān)聽的時間點:
typedef CF_OPTIONS(CFOptionFlags,CFRunLoopActivity){
kCFRunLoopEntry = (1UL << 0), // 即將進入LOOP
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將進入處理Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 剛才休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出Loop
}
- 監(jiān)聽的代碼:
- (void)viewDidLoad{
[super viewDidLoad];
//1、創(chuàng)建監(jiān)聽對象
/*
第一個參數(shù):告訴系統(tǒng)如何給Observer對象分配存儲空間
第二個參數(shù):需要監(jiān)聽的類型
第三個參數(shù):是否需要重復監(jiān)聽
第四個參數(shù):優(yōu)先級
第五個參數(shù):監(jiān)聽到對應的狀態(tài)之后的回調(diào)
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
};
*/
CFRunLoopObserverRef oberver= CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"進入RunLoop");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"即將處理Timer");
break;
case kCFRunLoopBeforeSources:
NSLog(@"即將處理source");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"即將進入睡眠");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"即將醒來");
break;
case kCFRunLoopExit:
NSLog(@"退出");
break;
default:
break;
}
});
//2、給主線程的RunLoop添加監(jiān)聽
/*
第一個參數(shù):需要監(jiān)聽的 RunLoop 對象
第二個參數(shù):給指定的 RunLoop 對象添加的監(jiān)聽對象
第三個參數(shù):在哪種模式下監(jiān)聽
*/
CFRunLoopAddObserver(CFRunLoopGetMain(), oberver, kCFRunLoopCommonModes);
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(demo) userInfo:nil repeats:YES];
}
- (void)demo{
NSLog(@"%s",__func__);
}
我們會看到這幾行打印會重復執(zhí)行
2015-09-06 17:02:04.848 RunLoop觀察者[35817:418636] 即將醒來
2015-09-06 17:02:04.849 RunLoop觀察者[35817:418636] -[ViewController demo]
2015-09-06 17:02:04.849 RunLoop觀察者[35817:418636] 即將處理Timer
2015-09-06 17:02:04.849 RunLoop觀察者[35817:418636] 即將處理source
2015-09-06 17:02:04.849 RunLoop觀察者[35817:418636] 即將進入睡眠
2015-09-06 17:02:06.848 RunLoop觀察者[35817:418636] 即將醒來
2015-09-06 17:02:06.849 RunLoop觀察者[35817:418636] -[ViewController demo]
2015-09-06 17:02:06.849 RunLoop觀察者[35817:418636] 即將處理Timer
2015-09-06 17:02:06.849 RunLoop觀察者[35817:418636] 即將處理source
2015-09-06 17:02:06.849 RunLoop觀察者[35817:418636] 即將進入睡眠
八、關于 RunLoop 的理解
- 理解圖(
引用了一張網(wǎng)上很好的圖片
):
RunLoop理解圖.png - 一條線程對應一個 RunLoop,主線程的 RunLoop 只要程序已啟動就會默認創(chuàng)建并與主線程綁定好,
RunLoop 底層的實現(xiàn)是通過字典的形式來將 線程 和 RunLoop 來綁定的
,RunLoop 可以理解為懶加載,子線程的 RunLoop 可以調(diào)用 currentRunLoop,先從字典里面根據(jù)子線程取,如果沒有就會去創(chuàng)建并與子線程綁定,保存到字典當中。每個 RunLoop 里面有很多的 Mode,每個 Mode 里面又有很多的source、timer、observer。RunLoop 在同一時刻只能執(zhí)行一種 Mode,當執(zhí)行這種 Mode 的時候,只有這種 Mode 中的source、timer、observer 有效,別的 Mode 無效,這樣做是為了避免邏輯的混亂。 - 執(zhí)行流程:先進入 RunLoop,處理系統(tǒng)默認事件,觸發(fā)事件的時候,RunLoop 醒來處理 timer、source0、source1,處理完再睡覺。
- RunLoop 死掉的情況:
- RunLoop 有個默認的超時時間.
seconds = 9999999999.0
- 線程掛了。
九、RunLoop 應用場景
- NSTimer
- 就是CFRunLoopTimerRef,他們之間是 toll-free bridged 的。一個 NSTimer 注冊到 RunLoop 后,RunLoop 會為其重復的時間點注冊好事件。例如 10:00, 10:10, 10:20 這幾個時間點。RunLoop為了節(jié)省資源,并不會在非常準確的時間點回調(diào)這個Timer。Timer 有個屬性叫做 Tolerance (寬容度),標示了當時間點到后,容許有多少最大誤差。
- 如果某個時間點被錯過了,例如執(zhí)行了一個很長的任務,則那個時間點的回調(diào)也會跳過去,不會延后執(zhí)行。就比如等公交,如果 10:10 時我忙著玩手機錯過了那個點的公交,那我只能等 10:20 這一趟了。
- CADisplayLink 是一個和屏幕刷新率一致的定時器(但實際實現(xiàn)原理更復雜,和
NSTimer 并不一樣,其內(nèi)部實際是操作了一個 Source
)。如果在兩次屏幕刷新之間執(zhí)行了一個長任務,那其中就會有一幀被跳過去(和 NSTimer 相似),造成界面卡頓的感覺。在快速滑動TableView時,即使一幀的卡頓也會讓用戶有所察覺。
- ImageView顯示
- PerformSelector:
- 當調(diào)用 NSObject 的 performSelector:afterDelay:后,世紀上期內(nèi)部會創(chuàng)建一個 Timer 并添加到當前線程的 RunLoop 中,所以如果當前線程沒有 RunLoop,則這個方法會失效。
- 當調(diào)用 performSelector:onThread: 時,實際上其會創(chuàng)建一個 Timer 加到對應的線程去,同樣的,如果對應線程沒有 RunLoop 該方法也會失效。
-
常駐線程
-
創(chuàng)建一個線程來處理耗時且頻繁的操作,例如即時聊天音頻的壓縮,或者經(jīng)常下載,避免頻繁開啟線程以便提高性能
, AFNetWorking就是如此。
-
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
-
自動釋放池
- 系統(tǒng)在主線程 RunLoop 里注冊了
兩個 Observer
,其回調(diào)都是_wrapRunLoopWithAutoreleasePoolHandler()
-
第一個 Observer 監(jiān)視的事件是 Entry(即將進入Loop)
,其回調(diào)內(nèi)會調(diào)用_objc_autoreleasePoolPush()
創(chuàng)建自動釋放池。其 order 是-2147483647,優(yōu)先級最高,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前。 -
第二個 Observer 監(jiān)視了兩個事件
:BeforeWaiting(準備進入休眠) 時
調(diào)用_objc_autoreleasePoolPop()
和_objc_autoreleasePoolPush()
釋放舊的池并創(chuàng)建新池;Exit(即將退出Loop)
時調(diào)用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優(yōu)先級最低,保證其釋放池子發(fā)生在其他所有回調(diào)之后。 - 打印 currentRunLoop 來獲取autoreleasePool 的狀態(tài)
- 系統(tǒng)在主線程 RunLoop 里注冊了
NSLog(@"%@",[NSRunLoop currentRunLoop]);
- 只有兩種狀態(tài)
_wrapRunLoopWithAutoreleasePoolHandler:activities = 0x1 = 1
_wrapRunLoopWithAutoreleasePoolHandler:activities = 0xa0 = 160
- 對比 RunLoop 的活動狀態(tài):
對比runLoop
typedef CF_OPTIONS(CFOptionFlags,CFRunLoopActivity){
kCFRunLoopEntry = (1UL << 0), // 即將進入LOOP =1
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理Timer = 2
kCFRunLoopBeforeSources = (1UL << 2), // 即將進入處理Source = 4
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠 = 32
kCFRunLoopAfterWaiting = (1UL << 6), // 剛才休眠中喚醒 = 64
kCFRunLoopExit = (1UL << 7), // 即將退出Loop = 128
}
- 得出結論:
+ _wrapRunLoopWithAutoreleasePoolHandler:activities = 0x1 = 1 = 即將進入RunLoop創(chuàng)建一個自動釋放池
+ _wrapRunLoopWithAutoreleasePoolHandler:activities = 0xa0 = 160 = 128+32
+ 32: 即將進入休眠 1、銷毀一個自動釋放池 2、創(chuàng)建一個新的自動釋放池
+ 128:即將退出RunLoop 銷毀一個自動釋放池
- 參考文獻:這是一篇很好的文章
這是蘋果的各種 RunLoopMode
***多線程編程指南 ***