OC基礎回顧(十三)程式碼塊和併發性
阿新 • • 發佈:2019-01-27
1.程式碼塊
程式碼塊物件(通常稱為程式碼塊)是對C語言中函式的擴充套件。除了函式中的程式碼,程式碼塊還包含變數繫結。程式碼塊有時也稱為閉包(closure)。
程式碼塊包含兩種型別的繫結:自動型和託管型。自動繫結(automatic binding)使用的是棧中的記憶體,而託管繫結(managed binding)是通過堆建立的。
1.1 程式碼塊定義和實現
程式碼塊借鑑了函式指標的語法。與函式指標相似,程式碼塊具有以下特徵:- 返回型別可以手動宣告,也可以由編譯器推導;
- 具有指定型別的引數列表
- 擁有名稱
說明: 等號前面的內容:int (^square_block)( int number ),是程式碼塊的定義。 等號後面的內容:是程式碼塊的實現內容。 一般我們可以用如下關係來表示它們: returntype ( ^ blockname) ( list of arguments ) = ^( arguments ) { body; };int (^square_block)( int number ) = ^(int number) { return (number * number); }; int result = square_block(6); NSLog(“Result = %d “,result);
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):指的是兩個或多個任務在等待其他任務執行結束,就像是幾輛汽車同時位於一個很擁擠的停車場裡。 下面討論三種佇列及其使用:- 連續佇列
- 併發佇列
dispatch_queue_t myQueue;
myQueue = dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
說明:第一個引數是優先順序選項,對應不同的優先順序。第二個引數暫時都用0。因為它們都是全域性的,所以無需為他們管理記憶體。不需要保留這些佇列的引用,在需要的時候使用函式來訪問就行了。
- 主佇列
- 獲取當前佇列
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。
- 清理函式
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,我們並不想自己管理上下文的記憶體,而是想交給系統來管理。
- 新增任務
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);
}
- 向佇列新增這個函式
- 暫停佇列
- 重新啟用佇列
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 *currentQueue = [NSOperationQueue currentQueue];
或主佇列:
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
以下就是建立佇列的程式碼:
NSOperationQueue *_operationQueue = [[NSOperationQueue alloc] init];
//新增操作
[_operationQueue addOperation:blockOperation];
也可以新增需要執行的程式碼塊來替代操作物件
[_operationQueue addOperationWithBlock:^{
NSLog(“My Block”);
}];
一旦佇列中添加了操作,它就會被安排進度並執行。