iPhone開發之NSRunLoop的進一步理解


iPhone應用開發中關於NSRunLoop的概述是本文要介紹的內容,NSRunLoop是一種更加高明的消息處理模式,他就高明在對消息處理過程進行了更好的抽象和封裝,這樣才能是的你不用處理一些很瑣碎很低層次的具體消息的處理,在NSRunLoop中每一個消息就被打包在input source或者是timer source中了,來看詳細內容。

1.什么是NSRunLoop

我們會經常看到這樣的代碼:

  1. - (IBAction)start:(id)sender  
     
  2. {  
     
  3. pageStillLoading = YES;  
     
  4. [NSThread detachNewThreadSelector:@selector(loadPageInBackground:)toTarget:self withObject:nil];  
     
  5. [progress setHidden:NO];  
     
  6. while (pageStillLoading) {  
     
  7. [NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];  
     
  8. }  
     
  9. [progress setHidden:YES];  
     
  10. }  
復制代碼

這段代碼很神奇的,因為他會“暫停”代碼運行,而且程序運行不會因為這里有一個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具體的工作原理,首先是官方文檔提供的說法,看圖:

1.jpg

通過所有的“消息”都被添加到了NSRunLoop中去,而在這里這些消息並分為“input source”和“Timer source” 並在循環中檢查是不是有事件需要發生,如果需要那么就調用相應的函數處理。為了更清晰的解釋,我們來對比VC++和iOS消息處理過程。

VC++中在一切初始化都完成之后程序就開始這樣一個循環了(代碼是從戶sir mfc程序設計課程的slides中截取):

  1. int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR  lpCmdLine,int nCmdShow){  
     
  2. ...  
     
  3. while (GetMessage(&msg, NULL, 0, 0)){  
     
  4. if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)){  
     
  5. TranslateMessage(&msg);  
     
  6. DispatchMessage(&msg);  
     
  7. }  
     
  8. }  
     
  9. }  

可以看到在GetMessage之后就去分發處理消息了,而iOS中main函數中只是調用了UIApplicationMain,那么我們可以介意猜出UIApplicationMain在初始化完成之后就會進入這樣一個情形:

  1. int UIApplicationMain(...){  
     
  2. ...  
     
  3. while(running){  
     
  4. [NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];  
     
  5. }  
     
  6. ...  
     
  7. }

所以在UIApplicationMain中也是同樣在不斷處理runloop才是的程序沒有退出。剛才的我說了NSRunLoop是一種更加高明的消息處理模式,他就高明在對消息處理過程進行了更好的抽象和封裝,這樣才能是的你不用處理一些很瑣碎很低層次的具體消息的處理,在NSRunLoop中每一個消息就被打包在input source或者是timer source中了,當需要處理的時候就直接調用其中包含的相應對象的處理函數了。

所以對外部的開發人員來講,你感受到的就是,把source/timer加入到runloop中,然后在適當的時候類似於[receiver action]這樣的事情發生了。甚至很多時候,你都沒有感受到整個過程前半部分,你只是感覺到了你的某個對象的某個函數調用了。

比如在UIView被觸摸時會用touchesBegan/touchesMoved等等函數被調用,也許你會想,“該死的,我都不知道在那里被告知有觸摸消息,這些處理函數就被調用了!?”所以,消息是有的,只是runloop已經幫你做了!為了證明我的觀點,我截取了一張debug touchesBegan的call stack,如圖:
 

2.jpg

 

 

利用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]];
 
 
 
 

from CFRunLoop Reference

CFRunLoop monitors sources of input to a task and dispatches control when they become ready for processing.

Examples of input source might include user input devices,network connection,periodic or timed-delay events,and asynchronous callbacks.

Three types of objects can be monitored by a run loop:

sources(CFRunLoopSource)

timers(CFRunLoopTimer)

observers(CFRunLoopObserver)

Each sources,times or observers added to a run loop must be associated with one or more run loop modes.

There is exactly one run loop per thread.

RunLoop 是事件循環,用於schedule work 和協調輸入事件.

RunLoop 接受2種不同的事件源,input sources (異步事件)和 timer sources(同步事件)

何時使用RunLoop?

1.使用ports或自定義輸入源 與其他線程通訊

2.在線程中使用定時器

3.在程序中使用performSelector等方法

4.讓線程周期性執行某個任務


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM