Phone應用開發中關於NSRunLoop的概述是本文要介紹的內容,NSRunLoop是一種更加高明的訊息處理模式,他就高明在對訊息處理過程進行了更好的抽象和封裝,這樣才能是的你不用處理一些很瑣碎很低層次的具體訊息的處理,在NSRunLoop中每一個訊息就被打包在input source或者是timer source中了,來看詳細內容。
1.什麼是NSRunLoop
我們會經常看到這樣的代碼:
- - (IBAction)start:(id)sender
- {
- pageStillLoading = YES;
- [NSThread detachNewThreadSelector:@selector(loadPageInBackground:)toTarget:self withObject:nil];
- [progress setHidden:NO];
- while (pageStillLoading) {
- [NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
- }
- [progress setHidden:YES];
- }
複製代碼
這段代碼很神奇的,因為他會“暫停”代碼運行,而且程式運行不會因為這裡有一個while迴圈而受到影響。在[progress setHidden:NO]執行之後,整個函數想暫停了一樣停在迴圈裡面,等loadPageInBackground裡面的操作都完成了以後才讓[progress setHidden:YES]運行。這樣做就顯得簡介,而且邏輯很清晰。如果你不這樣做,你就需要在loadPageInBackground裡面表示load完成的地方調用[progress setHidden:YES],顯得代碼不緊湊而且容易出錯。
[iGoogle有話說:應用程式架構主線程已經封裝了對NSRunLoop runMode:beforeDate:的調用;它和while迴圈構成了一個訊息泵,不斷擷取和處理訊息;可能大家會比較奇怪,既然主線程中已經封裝好了對NSRunLoop的調用,為什麼這裡還可以再次調用,這個就是它與Windows訊息迴圈的區別,它可以嵌套調用.當再次調用while+NSRunLoop時候程式並沒有停止執行,它還在不停提取訊息/處理訊息.這一點與Symbian中Active Scheduler的嵌套調用達到同步作用原理是一樣的.]
那麼具體什麼是NSRunLoop呢?其實NSRunLoop的本質是一個訊息機制的處理模式。如果你對vc++編程有一定瞭解,在windows中,有一系列很重要的函數SendMessage,PostMessage,GetMessage,這些都是有關訊息傳遞處理的API。
但是在你進入到Cocoa的編程世界裡面,我不知道你是不是走的太快太匆忙而忽視了這個很重要的問題,Cocoa裡面就沒有提及到任何關於訊息處理的API,開發人員從來也沒有自己去關心過訊息的傳遞過程,好像一切都是那麼自然,像大自然一樣自然?在Cocoa裡面你再也不用去自己定義WM_COMMAD_XXX這樣的宏來標識某個訊息,也不用在switch-case裡面去對特定的訊息做特別的處理。難道是Cocoa裡面就沒有了訊息機制?答案是否定的,只是Apple在設計訊息處理的時候採用了一個更加高明的模式,那就是RunLoop。
2. NSRunLoop工作原理
接下來看一下NSRunLoop具體的工作原理,首先是官方文檔提供的說法,看圖:
通過所有的“訊息”都被添加到了NSRunLoop中去,而在這裡這些訊息並分為“input source”和“Timer source” 並在迴圈中檢查是不是有事件需要發生,如果需要那麼就調用相應的函數處理。為了更清晰的解釋,我們來對比VC++和iOS訊息處理過程。
VC++中在一切初始化都完成之後程式就開始這樣一個迴圈了(代碼是從戶sir mfc程式設計課程的slides中截取):
- int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow){
- ...
- while (GetMessage(&msg, NULL, 0, 0)){
- if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)){
- TranslateMessage(&msg);
- DispatchMessage(&msg);
- }
- }
- }
可以看到在GetMessage之後就去分發處理訊息了,而iOS中main函數中只是調用了UIApplicationMain,那麼我們可以介意猜出UIApplicationMain在初始化完成之後就會進入這樣一個情形:
- int UIApplicationMain(...){
- ...
- while(running){
- [NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
- }
- ...
- }
所以在UIApplicationMain中也是同樣在不斷處理runloop才是的程式沒有退出。剛才的我說了NSRunLoop是一種更加高明的訊息處理模式,他就高明在對訊息處理過程進行了更好的抽象和封裝,這樣才能是的你不用處理一些很瑣碎很低層次的具體訊息的處理,在NSRunLoop中每一個訊息就被打包在input source或者是timer source中了,當需要處理的時候就直接調用其中包含的相應對象的處理函數了。
所以對外部的開發人員來講,你感受到的就是,把source/timer加入到runloop中,然後在適當的時候類似於[receiver action]這樣的事情發生了。甚至很多時候,你都沒有感受到整個過程前半部分,你只是感覺到了你的某個對象的某個函數調用了。
比如在UIView被觸摸時會用touchesBegan/touchesMoved等等函數被調用,也許你會想,“該死的,我都不知道在那裡被告知有觸摸訊息,這些處理函數就被調用了!?”所以,訊息是有的,只是runloop已經幫你做了!為了證明我的觀點,我截取了一張debug touchesBegan的call stack,
利用NSRunLoop阻塞NSOperation線程
在使用NSOperationQueue簡化多線程開發中介紹了多線程的開發,我這裡主要介紹一下使用NSRunLoop阻塞線程。
主要使用在NStimer定時啟用的任務或者非同步擷取資料的情況如socket擷取網路資料,要阻塞線程,直到擷取資料之後在釋放線程。
下面是線程中沒有使用NSRunLoop阻塞線程的代碼和執行效果:
線程類:
#import <Foundation/Foundation.h>
@interface MyTask : NSOperation {
}
@end
#import "MyTask.h"
@implementation MyTask
-(void)main
{
NSLog(@"開始線程=%@",self);
[NSTimer timerWithTimeInterval:2 target:self selector:@selector(hiandeTime:) userInfo:nil repeats:NO];
}
-(void)hiandeTime:(id)sender
{
NSLog(@"執行了NSTimer");
}
-(void)dealloc
{
NSLog(@"delloc mytask=%@",self);
[super dealloc];
}
@end
線程添加到隊列中:
- (void)viewDidLoad
{
[super viewDidLoad];
NSOperationQueue *queue=[[NSOperationQueue alloc] init];
MyTask *myTask=[[[MyTask alloc] init] autorelease];
[queue addOperation:myTask];
MyTask *myTask1=[[[MyTask alloc] init] autorelease];
[queue addOperation:myTask1];
MyTask *myTask2=[[[MyTask alloc] init] autorelease];
[queue addOperation:myTask2];
[queue release];
}
執行結果是:
2011-07-25 09:44:45.393 OperationDemo[20676:1803] 開始線程=<MyTask: 0x4b4dea0>
2011-07-25 09:44:45.393 OperationDemo[20676:5d03] 開始線程=<MyTask: 0x4b50db0>
2011-07-25 09:44:45.396 OperationDemo[20676:1803] 開始線程=<MyTask: 0x4b51070>
2011-07-25 09:44:45.404 OperationDemo[20676:6303] delloc mytask=<MyTask: 0x4b4dea0>
2011-07-25 09:44:45.404 OperationDemo[20676:5d03] delloc mytask=<MyTask: 0x4b50db0>
2011-07-25 09:44:45.405 OperationDemo[20676:6303] delloc mytask=<MyTask: 0x4b51070>
可以看到,根本沒有執行NSTimer中的方法,線程就釋放掉了,我們要執行
NSTimer中的方法,就要利用NSRunLoop阻塞線程。下面是修改後的代碼:
-(void)main
{
NSLog(@"開始線程=%@",self);
NSTimer *timer=[NSTimer timerWithTimeInterval:2 target:self selector:@selector(hiandeTime) userInfo:nil repeats:NO];
[timer fire];
while (!didDisconnect) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}
執行結果如下:
2011-07-25 10:07:00.543 OperationDemo[21270:1803] 開始線程=<MyTask: 0x4d16380>
2011-07-25 10:07:00.543 OperationDemo[21270:5d03] 開始線程=<MyTask: 0x4d17790>
2011-07-25 10:07:00.550 OperationDemo[21270:6303] 開始線程=<MyTask: 0x4d17a50>
2011-07-25 10:07:00.550 OperationDemo[21270:1803] 執行了NSTimer
2011-07-25 10:07:00.551 OperationDemo[21270:5d03] 執行了NSTimer
2011-07-25 10:07:00.552 OperationDemo[21270:6303] 執行了NSTimer
2011-07-25 10:07:00.556 OperationDemo[21270:6503] delloc mytask=<MyTask: 0x4d16380>
2011-07-25 10:07:00.557 OperationDemo[21270:6303] delloc mytask=<MyTask: 0x4d17790>
2011-07-25 10:07:00.557 OperationDemo[21270:5d03] delloc mytask=<MyTask: 0x4d17a50>
我們可以使用NSRunLoop進行線程阻塞。
使用runloop阻塞線程的正確寫法
Runloop可以阻塞線程,等待其他線程執行後再執行。
比如:
@implementation ViewController{
BOOL end;
}
…
– (void)viewDidLoad
{
[super viewDidLoad];
NSLog(@”start new thread …”);
[NSThread detachNewThreadSelector:@selector(runOnNewThread) toTarget:self withObject:nil];
while (!end) {
NSLog(@”runloop…”);
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
NSLog(@”runloop end.”);
}
NSLog(@”ok.”);
}
-(void)runOnNewThread{
NSLog(@”run for new thread …”);
sleep(1);
end=YES;
NSLog(@”end.”);
}
但是這樣做,運行時會發現,while迴圈後執行的語句會在很長時間後才被執行。
那是不是可以這樣:
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
縮短runloop的休眠時間,看起來解決了上面出現的問題。
不過這樣也又問題,runloop對象被經常性的喚醒,這違背了runloop的設計初衷。runloop的作用就是要減少cpu做無謂的空轉,cpu可在閒置時候休眠,以節約電量。
那麼怎麼做呢?正確的寫法是:
-(void)runOnNewThread{
NSLog(@”run for new thread …”);
sleep(1);
[self performSelectorOnMainThread:@selector(setEnd) withObject:nil waitUntilDone:NO];
NSLog(@”end.”);
}
-(void)setEnd{
end=YES;
}
見黑體斜體字部分,要將直接設定變數,改為向主線程發送訊息,執行方法。問題得到解決。
這裡要說一下,造成while迴圈後語句延緩執行的原因是,runloop未被喚醒。因為,改變變數的值,runloop對象根本不知道。延緩的時間長度總是不定的,這是因為,有其他事件在某個時點喚醒了主線程,這才結束了while迴圈。那麼,向主線程發送訊息,將喚醒runloop,因此問題就解決了。
NSRunLoop runMode:
NSDefaultRunLoopMode/NSRunLoopCommonModes
eg.
[[NSRunLoopcurrentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];