1. 程式人生 > >OC基礎回顧(十三)程式碼塊和併發性

OC基礎回顧(十三)程式碼塊和併發性

1.程式碼塊

程式碼塊物件(通常稱為程式碼塊)是對C語言中函式的擴充套件。除了函式中的程式碼,程式碼塊還包含變數繫結。程式碼塊有時也稱為閉包(closure)。
程式碼塊包含兩種型別的繫結:自動型和託管型。自動繫結(automatic binding)使用的是棧中的記憶體,而託管繫結(managed binding)是通過堆建立的。

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; };

1.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];
}]; 

1.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) );

1.4 程式碼塊和變數

1.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.因為變數是本地變數,程式碼塊會在定義的時候複製並儲存它們的狀態。

1.4.2 引數變數

程式碼塊中的引數變數和函式中的引數變數具有同樣的作用。
typedef double (^ MyBlock)(double c, double d);
MyBlock myBlock = ^(double a, double b){
     return a * b;
};
NSLog:(@“%f, %f”,myBlock(12,2), myBlock(2,4));

1.4.3 __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)包含可變長度陣列的結構體

1.4.4 程式碼塊內部的本地變數

這些變數與本地變數具有相同的作用:
void (^MyBlock)(void) = ^(void){
          double a = 3;
          double b = 4;
          NSLog(@“%f”, a * b);
};
MyBlcok();

1.5 程式碼塊與記憶體管理

在程式碼塊中使用Objective-C變數時必須小心 ,以下規則能幫助你處理記憶體管理。 1)如果引用了一個Objective-C物件,必須要保留它; 2)如果通過引用訪問了一個例項變數,要保留一次self(即執行方法的物件); 3)如果通過數值訪問了一個例項變數,變數需要保留。 解釋規則(1)的示例:
NSString *string1 = ^{
          return [_theString stringByAppendingString:_theString];
};
在這個示例中,_theString是聲明瞭程式碼塊的類中的例項變數。因為在程式碼塊中直接訪問了例項變數,所以包含它的物件(self)需要保留。
__block NSString *localObject = _theString;
NSString *string2 = ^{
          return [localObject stringByAppendingString:localObject];
};
在這個例子中,我們是間接訪問:建立了一個指向例項變數的本地引用,並在程式碼塊中使用。因此要保留的是localObject,而不是self。 因為程式碼塊是物件,所以可以向它傳送任何與記憶體管理有關的訊息。在C語言級別中,必須使用Block_copy()和Block_release()函式來適當地管理記憶體.
MyBlock block1 = ^{
          NSLog(@"Block1”);
};
block1();

MyBlock block2 = ^{
          NSLog(@“Block2”);
};
block2();
Block_release(block2);
 
block2 = Block_copy(block1);
block2(); 

2.併發性

2.1 引入執行緒的概念

      用來執行Xcode的Mac電腦的處理器至少擁有兩個核心,也可能更多。現在最新的iOS裝置都是多核的。這意味著你可以在同一時間進行多項任務。蘋果公司提供了多種可以利用多核特性的API。能夠在同一時間執行多項任務的程式稱其為併發的(concurrent)程式。       利用併發性最基礎的方法是使用POSIX執行緒來處理程式的不同部分使其能夠獨立執行。POSIX執行緒擁有支援C語言和Objective-C的API。編寫併發程式需要建立多個執行緒,而編寫執行緒程式碼是很具有挑戰性的。       執行緒是級別較低的API,需要手動管理,處理所有的執行緒是需要技巧的,一旦遇到問題,可能不使用執行緒會更好一些。

2.2 GCD技術

 蘋果公司為了減輕在多核上變成的負擔,引入了Grand Central Dispatch,我們稱之為GCD。

  • GCD技術減少了不少執行緒管理的麻煩,如果要使用GCD,你需要提交程式碼塊或者函式作為執行緒來執行。
  • GCD是一個系統級別(system-level)的技術,因此你可以在任意級別的程式碼中使用它。
  • GCD決定需要多少執行緒來安排他們執行的進度。
  • 因為GCD是執行在系統級別的,所以可以平衡應用程式所有內容的載入,這樣可以提高計算機或裝置的執行效率。

2.2.1 同步

我們如何在由多核組成的通路中管理交通呢?可以使用同步裝置,比如在通道入口立一個標記(flag)或者一個互斥(mutex)。 說明:mutex是mutual exclusion 的縮寫,它指的是確保兩個執行緒不會在同一時間進入臨界區。 Objective-C提供了一個語言級別的(language-level)關鍵字@synchronized。這個關鍵字擁有一個引數,通常這個物件是可以修改的。
@synchronized(theObject)
{
          //Critical section
}

它可以確保不同的執行緒會連續地訪問臨界區的程式碼。 如果你定義了一個屬性,並且沒有指定關鍵字nonatomic作為屬性的特性,編譯器會生成強制彼此互斥的getter和setter方法,但是這樣設定程式碼和變數,會產生一些消耗,比直接訪問慢一些。為了提高效能,可以新增nonatomic特性。

1.選擇效能

   NSObject提供方法以供一些程式碼只在後臺執行。這些方法中都有performSelector:,最簡單的就是performSelectorInBackground:WithObject:,它能在後臺執行一個方法。它通過建立一個執行緒來執行方法。定義這些方法時必須遵從以下限制: 1)這些方法執行在各自的執行緒裡,因此你必須為這些Cocoa物件建立一個自動釋放池,而主自動釋放池是與主執行緒相關的。 2)這些方法不能有返回值,並且要麼沒有引數,要麼只有一個引數物件。換句話說,你只能使用以下程式碼格式中的一種: -(void)myMethod; -(void)myMethod:(id)myObject; 示例:
-(void)myBackgroundMethod
{
          @autoreleasepool
          {
                    NSLog(@“My Background Method”);
          }
}
或:
-(void)myBackgroundMethod:(id)myObject
{
          @autoreleasepool
          {
                    NSLog(@“My Background Method %@”,myObject);
          }
}


在後臺執行你的方法:
[self performSelectorInBackground:@selector(myBackgroundMethod) withObject:nil];

或者:
[self performSelectorInBackground:@selector(myBackgroundMethod) withObject:argumentObjectl];

當方法執行結束之後,Objective-C執行時會特地清理並棄掉執行緒。需要注意:方法執行結束後並不會通知你,這是比較簡單的程式碼。如果想要做一些更復雜的事情,需要學習排程佇列。

2 排程佇列

GCD可以使用排程佇列(dispatch queue),只需寫下你的程式碼,把它指派為一個佇列(百度百科“佇列”:http://baike.baidu.com/subview/38959/14411740.htm),系統就會執行它了。可以同步或非同步執行任意程式碼。 有三種類型的佇列: 1)連續佇列:每個連續佇列都會根據指派的順序執行任務。可以按自己的想法建立任意數量的佇列,他們會並行操作任務。 2)併發佇列:每個併發佇列都能併發執行一個或多個任務。任務會根據指派到佇列的順序開始執行。你無法建立連續佇列,只能從系統提供的三個佇列內選擇一個來使用。 3)主佇列:它是應用程式中有效的主佇列,執行的是應用程式的主執行緒任務。 死鎖(deadlock):指的是兩個或多個任務在等待其他任務執行結束,就像是幾輛汽車同時位於一個很擁擠的停車場裡。 下面討論三種佇列及其使用:
  • 連續佇列
        當有一連串任務需要按照一定順序執行的時候,可以使用連續佇列。任務執行順序為先進先出(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();

2.2.2 佇列的記憶體管理

     排程佇列是引用計數物件。可以使用dispatch_retain()和dispatch_release來修改佇列的保留計數值。它們與一般的retain和release語句相似。你只能對你自己建立的佇列使用這些函式,而無法用在全域性排程佇列上。事實上,如果你向全域性佇列傳送這些訊息,是會被忽略的。如果你編寫了一個使用了垃圾回收機制的OS X應用程式,那麼你必須手動管理這些佇列。

     1.佇列的上下文(context)

     “在軟體工程中,上下文是一種屬性的有序序列,它們為駐留在環境內的物件定義環境。在物件的啟用過程中建立上下文,物件被配置為要求某些自動服務,如同步、事務、實時啟用、安全性等等。又比如計算機技術中,相對於程序而言,上下文就是程序執行時的環境。具體來說就是各個變數和資料,包括所有的暫存器變數、程序開啟的檔案、記憶體資訊等。”      你可以向排程物件(包括排程佇列)指派全域性資料上下文,可以在上下文中指派任意型別的資料,比如Objective-C物件或指標。系統只知道上下文包含了於佇列相關的資料,上下文資料的記憶體管理只能由你來做。在為上下文分配記憶體的時候,可以使用dispatch_set_context()和dispatch_get_context()函式。 程式碼:
NSMutableDictionary *myContext = [[NSMutableDictionary alloc] initWithCapacity:5];
[myContext setObject:@“My Context” forKey:@“title”];
[myContext setObject:[NSNumber numberWithInt:0] forKey:@“value”];
dispatch_set_context(_serial_queue, (__bridge_retained void *) myContext);

在這個例項中,我們建立一個字典來儲存上下文,當然也可以使用其它的指標型別。分配好記憶體之後就可以使用。 在最後一行程式碼中,我們必須保證物件是有效的,所以使用了__bridge_retained來給myContext的保留計數器的值加1。
  • 清理函式
     設定完上下文物件的資料之後,不需要真的知道上下文物件在何時何地會被棄用。可以讓物件在它棄用的時候呼叫一個函式,就像類裡面的dealloc函式。函式的格式應該如下所示:      void function_name(void *context); 我們將建立一個會在上下文物件棄用時呼叫的示例函式,通常稱為終結器(finalizer)函式。
void myFinalizerFunction(void *context)
{
     NSLog(@“myFinalizerFunction”);
     NSMutableDictionary *theData = (__bridge_transfer NSMutableDictionary *)context;
     [theData removeAllObjects];
}

__bridge_transfer 關鍵字:
這個關鍵字將物件的記憶體管理由全域性釋放池變換成了我們的函式。當我們的函式結束後,ARC將會給它的保留計數的值減1,如果保留計數的值被減到了0,物件將會被釋放。如果物件沒有被釋放,myContext將會一直留在記憶體中。 如何在程式碼塊中訪問上下文內容? NSMutableDictionary *myContext = (__bridge NSMutableDictionary *)dispatch_get_context(dispatch_get_current_queue()); 這行程式碼中添加了__bridge關鍵字。是用來告訴ARC,我們並不想自己管理上下文的記憶體,而是想交給系統來管理。
  • 新增任務
有兩種方法可以向佇列中新增任務: (1)同步:佇列會一直等待前面任務結束。 (2)非同步:新增任務後,不必等待任務,函式會立刻返回。推薦優先使用這種方式,因為它不會阻塞其他程式碼的執行。 可以選擇向佇列新增程式碼塊或函式。一共有四個排程函式,分別是程式碼塊和函式各自的同步與非同步方式。 注意:為了避免出現死鎖,不要給執行在同一佇列中的任務呼叫dispatch_sync或dispatch_sync_f函式。

     2.排程程式

(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);

2.3 操作佇列

Objective-C提供一些被稱為操作(operation)的API,使佇列在Objective-C層級上使用起來更加簡單。 如果想要使用操作,首先需要建立一個操作物件,然後將其指派給操作佇列,並讓佇列執行它。一共有三種建立佇列的方式。 (1)NSInvocationOperation:      如果已經有一個可以完成工作的類,並且想要在佇列上執行它,可以嘗試使用這種方法。
(2)NSBlockOperation:      類似於包含了需要執行程式碼塊的dispatch_async函式。
(3)自定義操作:      如果需要更靈活的操作型別,可以建立自己的自定義型別。必須通過NSOperation子類來定義你的操作。

2.3.1 建立呼叫操作(invocation operation)

NSInvocationOperation會為執行任務的類呼叫選擇器。因此你擁有 一個包含所需方法的類,使用這種方式來建立會非常方便。
@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:^{
     //dow 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”);
}];

一旦佇列中添加了操作,它就會被安排進度並執行。