iOS多線程開發 NSThread
iOS 支持多個層次的多線程編程,層次越高的抽象程度越高,使用起來也越方便,也是蘋果最推薦使用的方法。下面根據抽象層次從低到高依次列出iOS所支持的多線程編程范式:
1, Thread;
2, Cocoa operations;
3, Grand Central Dispatch (GCD) (iOS4 才開始支持)
下面簡要說明這三種不同范式:
Thread 是這三種范式里面相對輕量級的,但也是使用起來最負責的,你需要自己管理thread的生命周期,線程之間的同步。線程共享同一應用程序的部分內存空間,它們擁有對數據相同的訪問權限。你得協調多個線程對同一數據的訪問,一般做法是在訪問之前加鎖,這會導致一定的性能開銷。在 iOS 中我們可以使用多種形式的 thread:
Cocoa threads: 使用NSThread 或直接從 NSObject 的類方法 performSelectorInBackground:withObject: 來創建一個線程。如果你選擇thread來實現多線程,那么 NSThread 就是官方推薦優先選用的方式。
POSIX threads: 基於 C 語言的一個多線程庫,
Cocoa operations是基於 Obective-C實現的,類 NSOperation 以面向對象的方式封裝了用戶需要執行的操作,我們只要聚焦於我們需要做的事情,而不必太操心線程的管理,同步等事情,因為NSOperation已經為我們封裝了這些事情。 NSOperation 是一個抽象基類,我們必須使用它的子類。iOS 提供了兩種默認實現:NSInvocationOperation 和 NSBlockOperation。
Grand Central Dispatch (GCD): iOS4 才開始支持,它提供了一些新的特性,以及運行庫來支持多核並行編程,它的關注點更高:如何在多個 cpu 上提升效率。
有了上面的總體框架,我們就能清楚地知道不同方式所處的層次以及可能的效率,便利性差異。下面我們先來看看 NSThread 的使用,包括創建,啟動,同步,通信等相關知識。這些與 win32/Java 下的 thread 使用非常相似。
線程創建與啟動
NSThread的創建主要有兩種直接方式:
[NSThread detachNewThreadSelector:@selector(myThreadMainMethod:) toTarget:self withObject:nil];
和
NSThread* myThread = [[NSThread alloc] initWithTarget:self
selector:@selector(myThreadMainMethod:)
object:nil];
[myThread start];
這兩種方式的區別是:前一種一調用就會立即創建一個線程來做事情;而后一種雖然你 alloc 了也 init了,但是要直到我們手動調用 start 啟動線程時才會真正去創建線程。這種延遲實現思想在很多跟資源相關的地方都有用到。后一種方式我們還可以在啟動線程之前,對線程進行配置,比如設置 stack 大小,線程優先級。
還有一種間接的方式,更加方便,我們甚至不需要顯式編寫 NSThread 相關代碼。那就是利用 NSObject 的類方法 performSelectorInBackground:withObject: 來創建一個線程:
[myObj performSelectorInBackground:@selector(myThreadMainMethod) withObject:nil];
其效果與 NSThread 的 detachNewThreadSelector:toTarget:withObject: 是一樣的。
線程同步
線程的同步方法跟其他系統下類似,我們可以用原子操作,可以用 mutex,lock等。
iOS的原子操作函數是以 OSAtomic開頭的,比如:OSAtomicAdd32, OSAtomicOr32等等。這些函數可以直接使用,因為它們是原子操作。
iOS中的 mutex 對應的是 NSLock,它遵循 NSLooking協議,我們可以使用 lock, tryLock, lockBeforeData:來加鎖,用 unLock來解鎖。使用示例:
BOOL moreToDo = YES;
NSLock *theLock = [[NSLock alloc] init];
...
while (moreToDo) {
/* Do another increment of calculation */
/* until there’s no more to do. */
if ([theLock tryLock]) {
/* Update display used by all threads. */
[theLock unlock];
}
}
我們可以使用指令 @synchronized 來簡化 NSLock的使用,這樣我們就不必顯示編寫創建NSLock,加鎖並解鎖相關代碼。
- (void)myMethod:(id)anObj
{
@synchronized(anObj)
{
// Everything between the braces is protected by the @synchronized directive.
}
}
還有其他的一些鎖對象,比如:循環鎖NSRecursiveLock,條件鎖NSConditionLock,分布式鎖NSDistributedLock等等,在這里就不一一介紹了,大家去看官方文檔吧。
用NSCodition同步執行的順序
NSCodition 是一種特殊類型的鎖,我們可以用它來同步操作執行的順序。它與 mutex 的區別在於更加精准,等待某個 NSCondtion 的線程一直被 lock,直到其他線程給那個 condition 發送了信號。下面我們來看使用示例:
某個線程等待着事情去做,而有沒有事情做是由其他線程通知它的。
[cocoaCondition lock];
while (timeToDoWork <= 0)
[cocoaCondition wait];
timeToDoWork--;
// Do real work here.
[cocoaCondition unlock];
其他線程發送信號通知上面的線程可以做事情了:
[cocoaCondition lock];
timeToDoWork++;
[cocoaCondition signal];
[cocoaCondition unlock];
線程間通信
線程在運行過程中,可能需要與其它線程進行通信。我們可以使用 NSObject 中的一些方法:
在應用程序主線程中做事情:
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
在指定線程中做事情:
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
在當前線程中做事情:
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
取消發送給當前線程的某個消息
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:
如在我們在某個線程中下載數據,下載完成之后要通知主線程中更新界面等等,可以使用如下接口:- (void)myThreadMainMethod
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// to do something in your thread job
...
[self performSelectorOnMainThread:@selector(updateUI) withObject:nil waitUntilDone:NO];
[pool release];
}
RunLoop
說到 NSThread 就不能不說起與之關系相當緊密的 NSRunLoop。Run loop 相當於 win32 里面的消息循環機制,它可以讓你根據事件/消息(鼠標消息,鍵盤消息,計時器消息等)來調度線程是忙碌還是閑置。
系統會自動為應用程序的主線程生成一個與之對應的 run loop 來處理其消息循環。在觸摸 UIView 時之所以能夠激發 touchesBegan/touchesMoved 等等函數被調用,就是因為應用程序的主線程在 UIApplicationMain 里面有這樣一個 run loop 在分發 input 或 timer 事件。
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],顯得代碼不緊湊而且容易出錯。
那么具體什么是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,有圖有真相:
iPhone開發應用中NSOperation多線程使用是本文要介紹的內容,首先創建一個線程類,RequestOperation,它繼承NSOperation,而后我們在控制器類當中,創建一個NSOperationQueue對象,將該線成加入到序列中 。它就會自動的從NSOperationQueue當中取到我們加入的線程,而后運行線成的start方法 。
1. #import "RootViewController.h"
2. @implementation RootViewController
3. #pragma mark -
4. #pragma mark View lifecycle
5. -(void)buttonClicked:(id)sender{
6. _queue=[[NSOperationQueue alloc] init];
7. //第一個請求
8. NSURLRequest *request=[NSURLRequest requestWithURL:[NSURL URLWithString:@"http:www.google.com"]];
9. RequestOperation *operation=[[RequestOperation alloc] initWithRequest:request];
10. [_queue addOperation:operation];
11. [operation release];
12. //第二個請求
13. //NSURLRequest *request2=[NSURLRequest requestWithURL:[NSURL URLWithString:@"http:www.baidu.com"]];
14. //RequestOperation *operation1=[[RequestOperation alloc]initWithRequest:request2];
15. //operation1.message=@"operation1---";
16. //[_queue addOperation:operation1];
17. }
18. #import <Foundation/Foundation.h>
19. @interface RequestOperation : NSOperation{
20. NSURLRequest *_request;
21. NSMutableData *_data;
22. NSString *message;
23. }
24. @property(nonatomic,retain)NSString *message;
25. -(id)initWithRequest:(NSURLRequest*)request;
26. @end
27.
28. //
29. // RequestOperation.m
30. // NSOperation
31. //
32. // Created by wangqiulei on 8/23/10.
33. // Copyright 2010 __MyCompanyName__. All rights reserved.
34. //
35. #import "RequestOperation.h"
36. @implementation RequestOperation
37. @synthesize message;
38. -(id)initWithRequest:(NSURLRequest *)request{
39.
40. if (self=[self init]) {
41. _request=[request retain];
42. _data=[[NSMutableData data]retain];
43. }
44. return self;
45. }
46. -(void)dealloc{
47. [_request release];
48. [_data release];
49. [super dealloc];
50. }
51. //如果返回為YES表示asychronously方式處理
52. -(BOOL)isConcurrent{
53.
54. return YES;
55. }
56. //開始處理
57. -(void)start{
58. if (![self isCancelled]) {
59.
60. NSLog(@"%@",self.message);
61. NSLog(@"-------------%d",[self retainCount]);
62. [NSURLConnection connectionWithRequest:_request delegate:self];
63. }
64. }
65. //取得數據
66. -(void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
67. //添加數據
68. [_data appendData:data];
69. NSLog(@"%@",_data);
70. }
71. //http請求結束
72. -(void)connectionDidFinishLoading:(NSURLConnection *)connection{
73. }
74. @end
在iphone開發中經常需要更新UI的顯示,一般需要啟動新的線程來運行相關數據地更新,然后在另一線程中更新UI. 這里利用NSThread實現這樣一個功能:更新進度條。
//
// NSThreadDemoAppDelegate.m
// NSThreadDemo
//
// Created by Chelsea Wang(420989762/wwssttt@163.com) on 11-10-11.
// Copyright 2011年 __MyCompanyName__. All rights reserved.
//
@implementation NSThreadDemoAppDelegate
float processValue = 0;
@synthesize window = _window;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Override point for customization after application launch.
UIProgressView* processView = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault];
[processView setFrame:CGRectMake(10, 50, 200, 30)];
[processView setTag:101];
[processView setProgress:0.0];
UILabel* processLabel = [[UILabel alloc] initWithFrame:CGRectMake(225, 30, 100, 50)];
[processLabel setText:[NSString stringWithFormat:@"%.2f%%",processValue*100]];
[processLabel setTag:102];
[self.window addSubview:processView];
[processView release];
[self.window addSubview:processLabel];
[processLabel release];
[self.window makeKeyAndVisible];
[NSThread detachNewThreadSelector:@selector(updateProcess) toTarget:self withObject:nil];
return YES;
}
-(void)updateProcess{
NSAutoreleasePool* p = [[NSAutoreleasePool alloc] init];
[self performSelectorOnMainThread:@selector(updateUI) withObject:nil waitUntilDone:YES];
[p release];
}
-(void)updateUI{
if (processValue <= 1.0) {
processValue += 0.1;
UIProgressView* processView = (UIProgressView*)[self.window viewWithTag:101];
[processView setProgress:processValue];
UILabel* processLabel = (UILabel*)[self.window viewWithTag:102];
[processLabel setText:[NSString stringWithFormat:@"%.2f%%",processValue*100]];
[NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(updateUI) userInfo:nil repeats:NO];
}else{
processValue = 0.0;
[NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(updateUI) userInfo:nil repeats:NO];
}
}