第14章 代碼塊和併發性
14.1 代碼塊
代碼塊對象(通常稱爲代碼塊)是對C語言中函數的擴展。除了函數中的代碼,代碼塊還包含變量綁定。代碼塊有時也稱爲閉包(closure)。
代碼塊包含兩種類型的綁定:自動型和託管型。自動綁定(automatic binding)使用的是棧中的內存,而託管綁定(managed binding)是通過堆創建的。
14.1.1 代碼塊和函數指針
代碼塊借鑑了函數指針的語法。與函數指針相似,代碼塊具有以下特徵:
- 返回類型可以手動聲明,也可以由編譯器推導;
- 具有指定類型的參數列表
- 擁有名稱
1. 代碼塊的定義和實現
int (^square_block)( int number ) = ^(int number) {
return (number * number);
};
int result = square_block(6);
NSLog(“Result = %d “,result);
說明:
等號前面的內容:int (^square_block)( int number ),是代碼塊的定義。
等號後面的內容:是代碼塊的實現內容。
一般我們可以用如下關係來表示它們:
<returntype> ( ^ blockname) ( list of arguments ) = ^( arguments ) { body; };
返回類型可以省略。
2. 使用代碼塊
可以像函數一樣使用代碼塊。例如:
int result = square_block(6);
使用代碼塊的時候通常不需要創建一個代碼塊變量,而是在代碼中內聯代碼塊的內容。通常需要將代碼塊作爲參數的方法或函數。
NSArray *array = [NSArray arrayWithObjects:@“a”,@“b”,@“c”,@“d”,nil];
NSArray *sortedArray = [array sortedArrayUsingComparator:^(NSString *object1, NSString *object2){
return [object1 compare: object2];
}];
3. 使用typedef關鍵字
像上面那樣那麼長的變量定義語句,在輸入這些代碼的時候很容易引起錯誤。我們可以用typedef關鍵字。
typedef double (^ MyBlockName)(double a, double b);
這行代碼定義了一個名爲MyBlockName的代碼塊變量類型,它包含兩個雙浮點類型的參數,並且返回一個雙浮點類型的數值。
有了typedef,就可以像下面這樣使用這個代碼塊變量。
MyBlockName *myBlock = ^(double a, double b){
return a * b;
};
NSLog(@“%f, %f”, myBlock (2, 4 ) , myBlock (3, 4) );
4. 代碼塊和變量
代碼塊被聲明後會捕捉創建點時的狀態。
4.1 本地變量
typedef double (^ MyBlock)(void);
double a = 10, b = 20;
MyBlock myBlock = ^(void){
return a * b;
};
a = 30;
b = 20;
NSLog(@“%f”,myBlock());
這段代碼最後輸出地的是100,而不是600。因爲變量是本地變量,代碼塊會在定義的時候複製並保存它們的狀態。
全局變量、參數變量與函數中表現相同。
4.2 _block變量
本地變量會被代碼塊作爲常量獲取到。如果你想要修改他們的值,必須將他們聲明爲可修改的,否則像下面這個實例,編譯時會出現錯誤。
double c = 3;
MyBlock myBlock = ^(double a, double b){
c = a * b;
};
編譯器會報這個錯誤:
Variable is not assignable (missing __block type specifier)
想要修復這個編譯錯誤,需要將變量c標記爲__block。
__block double c = 3;
MyBlock myBlock = ^(double a, double b){
c = a * b;
};
有些變量是無法聲明爲__block類型的。
包括:
1)長度可變的數組
2)包含可變長度數組的結構體
14.2 併發性
GCD技術
蘋果公司爲了減輕在多核上變成的負擔,引入了Grand Central Dispatch,我們稱之爲GCD。
- GCD技術減少了不少線程管理的麻煩,如果要使用GCD,你需要提交代碼塊或者函數作爲線程來運行。
- GCD是一個系統級別(system-level)的技術,因此你可以在任意級別的代碼中使用它。
- GCD決定需要多少線程來安排他們運行的進度。
- 因爲GCD是運行在系統級別的,所以可以平衡應用程序所有內容的加載,這樣可以提高計算機或設備的執行效率。
14.2.1 同步
Objective-C提供了一個語言級別的(language-level)關鍵字@synchronized。這個關鍵字擁有一個參數,通常這個對象是可以修改的。
@synchronized(theObject)
{
//Critical section
}
它可以確保不同的線程會連續地訪問臨界區的代碼。
nonatomic屬性
如果你定義了一個屬性,並且沒有指定關鍵字nonatomic作爲屬性的特性,編譯器會生成強制彼此互斥的getter和setter方法,但是這樣設置代碼和變量,會產生一些消耗,比直接訪問慢一些。爲了提高性能,可以添加nonatomic特性。
1. 選擇性能
NSObject提供方法以供一些代碼只在後臺執行。這些方法中都有performSelector:,最簡單的就是performSelectorInBackground:WithObject:,它能在後臺執行一個方法。它通過創建一個線程來運行方法。定義這些方法時必須遵從以下限制:
1)這些方法運行在各自的線程裏,因此你必須爲這些Cocoa對象創建一個自動釋放池,而主自動釋放池是與主線程相關的。
2)這些方法不能有返回值,並且要麼沒有參數,要麼只有一個參數對象。換句話說,你只能使用以下代碼格式中的一種:
-(void)myMethod;
-(void)myMethod:(id)myObject;
-(void)myBackgroundMethod:(id)myObject
{
@autoreleasepool
{
NSLog(@“My Background Method %@”,myObject);
}
}
在後臺執行你的方法
[self performSelectorInBackground:@selector(myBackgroundMethod) withObject:argumentObjectl];
當方法執行結束之後,Objective-C運行時會特地清理並棄掉線程。需要注意:方法執行結束後並不會通知你,這是比較簡單的代碼。如果想要做一些更復雜的事情,需要學習調度隊列。
14.2.2 調度隊列
GCD可以使用調度隊列(dispatch queue),只需寫下你的代碼,把它指派爲一個隊列,系統就會執行它了。可以同步或異步執行任意代碼。
有三種類型的隊列:
1)連續隊列:每個連續隊列都會根據指派的順序執行任務。可以按自己的想法創建任意數量的隊列,他們會並行操作任務。
2)併發隊列:每個併發隊列都能併發執行一個或多個任務。任務會根據指派到隊列的順序開始執行。你無法創建併發隊列,只能從系統提供的三個隊列內選擇一個來使用。
3)主隊列:它是應用程序中有效的主隊列,執行的是應用程序的主線程任務。
連續隊列
當有一連串任務需要按照一定順序執行的時候,可以使用連續隊列。任務執行順序爲先進先出(FIFO):只要任務是異步提交的,隊列會確保任務根據預定順序執行。這些隊列都是不會發生死鎖的。
使用:
dispatch_queue_t my_serial_queue;
my_serial_queue = dispatch_queue_create(“com.appress.MySerialQueue1”,NULL);
第一個參數是隊列的名稱,第二個參數負責提供隊列的特性(現在用不到,所以必須爲NULL)。當隊列創建好以後,就可以給他指派任務。
併發隊列
併發調度隊列適合那些可以並行執行的任務。併發隊列也遵從先進先出(FIFO)的規範,且任務可以在前一個任務結束前就開始執行。每一次運行同一個程序,併發任務的數量可能是不一樣的,因爲它會根據其它運行的任務在不同時間變化。
說明:如果需要確保每次運行的任務數量都是一樣的,可以通過線程API來手動管理線程。
三種併發隊列:
(1)高優先級(high):優先級選項是DISPATCH_QUEUE_PRIORITY_HIGH
(2)默認優先級(default):優先級選項是DISPATCH_QUEUE_PRIORITY_DEFAULT
(3)低優先級(low):優先級選項是DISPATCH_QUEUE_PRIORITY_LOW
如果想要引用他們,可以調用dispatch_get_global_queue方法。
dispatch_queue_t myQueue;
myQueue = dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
說明:第一個參數是優先級選項,對應不同的優先級。第二個參數暫時都用0。因爲它們都是全局的,所以無需爲他們管理內存。不需要保留這些隊列的引用,在需要的時候使用函數來訪問就行了。
主隊列
使用dispatch_get_main_queue可以訪問與應用程序主線程相關的連續隊列。
dispatch_queue_t main_queue = dispatch_get_current_queue(void);
因爲這個隊列與主線程相關,所以必須小心安排這個隊列中的任務順序,否則它們可能會阻塞主應用程序運行。通常要以同步的方式使用這個隊列,提交過個任務並在它們操作完畢後執行一些動作。
獲取當前隊列
可以通過dispatch_get_current_queue()來找出當前運行的隊列代碼塊。如果在代碼塊對象之外調用了這個函數,則它將返回主隊列。
dispatch_queue_t myQueue = dispatch_get_current_queue();
調度程序
(1) 通過代碼塊添加任務
代碼塊必須是dispatch_block_t這樣的類型,要定義爲沒有參數和返回值才行。
typedef void(^dispatch_block_t)(void);
下面的示例添加異步代碼塊。這個函數擁有兩個參數,分別是隊列和代碼塊。
dispatch_async(_serial_queue, ^{
NSLog(@“Serial Task 1”);
});
如果是同步添加,使用dispatch_sync函數。
(2) 通過函數添加任務
函數的標準原型必須要像下面這樣:
void fucntion_name(void argument)
示例:
void myDispatchFunction(void *argument)
{
NSLog(@“Serial Task %@”,(__bridge NSNumber *)argument);
NSMutableDictionary *context = (__bridge NSMutableDictionary *)
dispatch_get_context(dispatch_get_current_queue());
NSNumber *value = [context objectForKey:@“value”];
NSLog(@“value = %@“,value);
}
向隊列添加這個函數
調用函數擁有三個參數:隊列、需要傳遞的任意上下文以及函數。如果沒有信息要發送給函數,也可以只傳遞一個NULL值。
dispatch_async_f(_serial_queue, (__bridge void *) [NSNumber numberWithInt:3],
(dispatch_function_t)myDispatchFunction);
如果想以同步的方式添加到隊列中,請調用dispatch_sync_f函數。
暫停隊列
如果出於某個原因要暫停隊列,請調用dispatch_susend()函數並傳遞隊列名稱。
dispatch_suspend(_serial_queue);
重新啓用隊列
隊列暫停之後,可以調用dispatch_resume()函數來重新啓用。
dispatch_resume(_serial_queue);
14.2.3 操作隊列
Objective-C提供一些被稱爲操作(operation)的API,使隊列在Objective-C層級上使用起來更加簡單。
如果想要使用操作,首先需要創建一個操作對象,然後將其指派給操作隊列,並讓隊列執行它。一共有三種創建隊列的方式。
(1)NSInvocationOperation
如果已經有一個可以完成工作的類,並且想要在隊列上執行它,可以嘗試使用這種方法。
(2)NSBlockOperation
類似於包含了需要執行代碼塊的dispatch_async函數。
(3)自定義操作
如果需要更靈活的操作類型,可以創建自己的自定義類型。必須通過NSOperation子類來定義你的操作。
創建調用操作(invocation operation)
@implementation MyCustomClass
-(NSOperation *)operationWithData:(id)data
{
return [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(myWorkerMethod:) object:data];
}
//This is the method that does the actual work
-(void)myWorkerMethod:(id)data
{
NSLog(@“My Worker Method %@“,data);
}
@end
一旦向隊列中添加了操作,任務即將執行時便會調用類裏面的myWorkerMethod:方法。
創建代碼塊操作 (block operation)
如果你有一個需要執行的代碼塊,那麼可以創建這個操作並讓隊列執行它。
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWihBlock:^{
//Do my work
}];
一旦創建了第一個代碼塊,你便可以通過addExecutionBlock:方法繼續添加更多的代碼塊。根據隊列的類型(連續的還是併發的),代碼塊會分別以連續或者併發的方式進行。
[blockOperation addExecutionBlock:^{
//do some more work
}];
向隊列中添加操作
一旦創建了操作,你就需要向隊列中添加代碼塊。NSOperationQueue一般會併發執行。它具有相關性,因此如果某操作是基於其他操作的,它們會相應地執行。
如果要確保你的操作是連續執行的,可以設置最大併發操作數是1,這樣任務就會按照先入先出的規範執行。在向隊列添加操作之前,需要某個方法來引用到那個隊列。可以創建一個新隊列或使用之前已經定義過的隊列(比如當前運行的隊列)。
NSOperationQueue *currentQueue = [NSOperationQueue currentQueue];
或主隊列:
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
以下就是創建隊列的代碼:
NSOperationQueue *_operationQueue = [[NSOperationQueue alloc] init];
添加操作
[_operationQueue addOperation:blockOperation];
也可以添加需要執行的代碼塊來替代操作對象
[_operationQueue addOperationWithBlock:^{
NSLog(“My Block”);
}];
一旦隊列中添加了操作,它就會被安排進度並執行。