Objective-C(六)Block與GCD

這是Objective-C系列的第6篇,也是《Effective Objective-C 2.0》系列的最後一篇。數組

1、最佳實踐

  • 用handler Block下降代碼分散程度
    • 在建立對象時,可使用內聯的handler Block將相關業務邏輯一併聲明。
    • 在有多個實例須要監控時,若是採用委託模式,那麼常常須要根據傳入的對象來切換,而若改用handler Block來實現,則可直接將Block與相關對象放在一塊兒;
    • 設計API時,若是遇到handler Block,那麼能夠新增一個參數,使調用者能夠經過該參數來決定應該把Block安排在哪一個隊列上執行。
  • 使用Block中發生的循環引用要避免
  • 多用派發隊列,少用同步鎖
    • 派發隊列可用來表述同步語義,這種作法要比使用@synchronizedNSLock對象更簡單;
    • 將同步與異步派發結合起來,能夠實現與普通加鎖機制同樣的同步行爲,而這麼作不會阻塞執行異步派發的線程;
    • 使用同步隊列及柵欄塊,能夠令同步行爲更高效;
  • 多用GCD,少用performSelector系列方法
    • performSelector系列方法在內存管理易有疏漏,它沒法肯定將要執行的選擇子具體是什麼,因此ARC編譯器也就沒法插入適當的內存管理方法;
    • performSelector系列方法所能處理的選擇子太過侷限,選擇子返回值類型及發送方給方法的參數個數都受到限制;
    • 若是想延遲執行,最好不要用performSelector系列方法,而是應該把任務封裝到Block裏,調用GCD來實現。
  • 掌握GCD及操做隊列的使用時機
    • 取消某個操做;
    • 指定操做間的依賴關係;
    • 經過鍵值觀察機制監控NSOperationNSOperation對象許多屬性都適合經過鍵值觀察機制來監聽,好比isCancelledisFinished
    • 指定操做的優先級;
  • 使用dispatch_once來執行只需運行一次的線程安全代碼
  • 不要使用dispatch_get_current_queue
  • 經過Dispatch Group機制,根據系統資源情況來執行任務
    • 一系列的任務可納入一個dispatch_group中,開發者能夠在這組任務完畢時得到通知;
    • 經過dispatch_group,能夠在併發時派發隊列同時執行多項任務。

2、實踐詳解

2.1 理解Block這一律念

​ Block用「^」(脫字符或插入符)來表示:安全

^{
  //Block implementation here
}
複製代碼

​ Block實際上是個值,自有其類型,與int、float或Objective-C對象同樣,也能夠把Block賦給變量,其與函數指針相似。 Block的完整的語法結構以下:網絡

return_type (^block_name)(parameters)
複製代碼

​ 看一個實例:多線程

int (^addBlock)(int a, int b) = ^(int a, int b) {
   return a+b;
}
複製代碼

​ 調用:併發

int add = addBlock(2, 5) //add = 7
複製代碼

​ 下面是各類狀況下的Block的寫法:框架

//屬性
@property (copy ,nonatomic)void (^callBack)(NSString *);

//函數參數
- (void)callbackAsAParameter:(void (^)(NSString *print))callBack {
    callBack(@"i am alone");
}

[self callbackAsAParameter:^(NSString *print) {
    NSLog(@"here is %@",print);
}];
    
//typedef
typedef void (^callBlock)(NSString *status);
CallBlock block = ^void(NSString *str){
    NSLog(@"%@",str);
};
複製代碼

​ Block的強大之處:在聲明它的範圍裏,全部變量均可覺得其所捕獲。就是Block裏能夠用該範圍的全部變量。異步

int additional = 5;
int (^addBlock)(int a, int b) = ^(int a, int b) {
    return a+b+additional;
}
int add = addBlcok(2, 5); //add = 12
複製代碼

​ 若是須要修改Block所捕獲的變量,須要加上__block。async

2.1.1 函數指針

​ 爲了更好說明Block,這裏說明下函數指針。函數

​ 函數指針是指向函數的指針變量。 於是「函數指針」自己首先應是指針變量,只不過該指針變量指向函數。這正如用指針變量可指向整型變量、字符型、數組同樣,這裏是指向函數。如前所述,C在編譯時,每個函數都有一個入口地址,該入口地址就是函數指針所指向的地址。有了指向函數的指針變量後,可用該指針變量調用函數,就如同用指針變量可引用其餘類型變量同樣,在這些概念上是大致一致的。函數指針有兩個用途:調用函數和作函數的參數。下面是個實例:oop

#include<stdio.h>
int max(int x,int y){return (x>y? x:y);}
int main()
{
    int (*ptr)(int, int);
    int a, b, c;
    ptr = max;		//ptr = &max;
    scanf("%d%d", &a, &b);
    c = (*ptr)(a,b);
    printf("a=%d, b=%d, max=%d", a, b, c);
    return 0;
}
複製代碼

2.1.2 Block的內部結構

​ Block自己是個對象,在存放Block的內存區域裏,第一個個變量是指向Class對象的指針,該指針叫作isa,其他內存裏含有對象正常運轉所需的各類信息:

Block內部結構

  • Impl 是個結構體。內部有個FuncPtr指向Block的實現代碼,此參數表明Block。Block實現了把原來標準C語言中須要「不透明的void指針」傳遞狀態變的透明,並且簡單易用。

  • descriptor是指向結構體的指針,每一個Block都包含該結構體。其中聲明瞭copy及dispose這兩個輔助函數所對應的函數指針。輔助函數在Block拷貝或者丟棄Block對象是運行。

    • size:Block的大小;
    • copy:輔助函數,保留捕獲的對象;
    • dispose:輔助函數,釋放捕獲的對象;
  • Block會將其所捕獲的全部變量都拷貝一份,置於descriptor以後,要注意的是,拷貝的並非對象自己,而是指向這些對象的指針變量。

    invoke函數爲什麼須要把Block做爲對象參數傳進來呢?緣由在於,執行Block時,要從內存中把這些捕獲到的變量讀出來。

2.1.3 全局Block、棧Block和堆Block

​ 定義Block時,其所佔的內存區域是分配在棧中的,即Block只在定義它的那個範圍內有效。如:

void (^block)();
if(//) {
   block = ^{
       NSLog(@"Block A");
    };
} else {
   block = ^{
      NSLog(@"Block B");
   };
}
複製代碼

​ if/else中定義的Block,都是在棧中,當離開了相應的範圍後,該棧內存有可能會被覆寫。因此在運行時,有可能正確運行,也有可能發生崩潰。這取決於編譯器是否覆寫了該Block內存。

棧內存中的Block對象,無需考慮對象的釋放,由於棧內存是系統管理的,系統會保證回收對象。

​ 爲了解決該問題,能夠給Block對象發送copy消息,以執行拷貝。就可把Block對象從棧內存拷貝到堆內存。

堆內存中的Block對象,同普通對象一致,有引用計數,拷貝是遞增引用計數,在ARC時無需手動釋放,在引用計數爲0時自動釋放等。

void (^block)();
if(//) {
   [block = ^{
   NSLog(@"Block A");
   } copy];
} else {
   [block = ^{
            NSLog(@"Block B");
   } copy];
}
複製代碼

​ 除了上面的「棧Block」和「堆Block」,還有一類叫作「全局Block」。全局Block,有下面幾個特色:

  • 不會捕捉任何狀態,好比外圍的變量等,運行時也無需有狀態來參與;

  • Block所使用的是整個內存區域,在編譯器已經徹底肯定,所以全局Block能夠聲明在全局內存裏,而不須要每次用到的時候在棧中建立;

  • 全局Block的拷貝操做是個空操做,由於全局Block決不可能爲系統所回收;

  • 全局Block至關於單例;

    下面是個全局Block:

void (^block) = ^{
    NSLog(@"This is a block");
};
複製代碼

​ 因爲運行該Block所需的所有信息都能在編譯器肯定,因此可把它作成全局Block,這徹底是種優化技術。若把如此簡單的Block當作複雜的Block來處理,那就會在複製或者丟棄該Block執行一些無謂的操做。

2.2 用handler Block下降代碼分散程度

​ 委託代理能很大程度上實現異步回調處理這樣的事,可是委託代理這種模式卻會使得代碼極度分散。

​ 用handler來集中代碼,是個不錯的選擇。

//風格一:
HONetworkFetcher *fetcher = [HONetworkFetcher alloc] initWirhURL:url];
[fetcher startWithCompletionHandler:^(NSData* data){
    //handle success
} failureHandler:^(NSError *error){
	    //handle failure
}];

//風格二:
HONetworkFetcher *fetcher = [HONetworkFetcher alloc] initWirhURL:url];
[fetcher startWithCompletionHandler:^(NSData* data,NSError *error){
  	if(error){
  	 	//handle success
  	}else{
      	//handle failure
  	}
}];
複製代碼

風格一:代碼易懂,將成功與失敗的邏輯分開來寫,必要時能夠忽略成功或者失敗的處理情形。

風格二:

  • 缺點:須要檢測error,且所有邏輯都在一塊兒,可能會令Block比較長,且比較複雜。
  • 優勢:更爲靈活,好比數據下載到一半時,網絡故障,此時能夠把數據即相關的錯誤傳給Block,以便保存已下載數據及對錯誤進行處理。另一個優勢就是,調用API的代碼可能會在處理處理成功的響應過程當中發現錯誤。此時能夠把成功中的錯誤處理同真正的錯誤一併處理,而不會形成代碼冗餘。假如分開處理,那麼就會有兩份同樣的錯誤處理代碼,而進一步,抽取成公共方法,又失去了本來要下降代碼分散的初衷。

總結:

  • 在建立對象時,可使用內聯的handler Block將相關業務邏輯一併聲明。

  • 在有多個實例須要監控時,若是採用委託模式,那麼常常須要根據傳入的對象來切換,而若改用handler Block來實現,則可直接將Block與相關對象放在一塊兒;

  • 設計API時,若是遇到handler Block,那麼能夠新增一個參數,使調用者能夠經過該參數來決定應該把Block安排在哪一個隊列上執行。

    - (id <NSObject>)addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block NS_AVAILABLE(10_6, 4_0);
    複製代碼

2.3 使用Block中發生的循環引用

以下代碼:

@interface HONetworkFetcher()

@property (nonatomic ,strong ,readwrite) NSURL *url;
@property (nonatomic ,copy)HONetworkFetcherCompletionHadler completionHandler;
@property (nonatomic ,strong)NSData *downloadedData;

@end
複製代碼
@implementation HONetworkFetcher

- (instancetype)initWithURL:(NSURL *)url {
    if (self = [super init]) {
        _url = url;
    }
    return self;
}

- (void)startWithCompletionHandler:(HONetworkFetcherCompletionHadler)completion
{
    self.completionHandler = completion;
        //start the request
        //request sets downloadedData property
        //When request is finished ,p_requestCompleted is called
}

- (void)p_requestCompleted
{
    if (_completionHandler) {
        _completionHandler(_downloadedData);
    }
}

@end
複製代碼

某個類做了以下的調用:

@interface HOClass : NSObject

@end

@interface HOClass()
{
    HONetworkFetcher *_networkFetcher;
    NSData *_fetchData;
}

@end
@implementation HOClass

- (void)downloadData
{
    NSURL *url = [NSURL URLWithString:@"www.com"];
    _networkFetcher = [[HONetworkFetcher alloc] initWithURL:url];
    [_networkFetcher startWithCompletionHandler:^(NSData *data) {
        _fetchData = data;
    }];
}
@end
複製代碼

分析下場景:

​ HOClass的實例對象實例變量_networkFetcher引用獲取器,_networkFetcher持有completionHandler,completionHandler又引用_fetchData,至關於持有HOClass的實例對象,因此就形成了循環引用。

​ 解除循環引用的方式很簡單,打破這個三角循環,要麼是使得_networkFetcher再也不引用,要麼獲取器再也不持有completionHandler。

​ 下面是一種解決方式:

- (void)downloadData
{
    NSURL *url = [NSURL URLWithString:@"www.com"];
    _networkFetcher = [[HONetworkFetcher alloc] initWithURL:url];
    [_networkFetcher startWithCompletionHandler:^(NSData *data) {
        _fetchData = data;
        _networkFetcher = nil;
    }];
}
複製代碼

另一種狀況是:completion handler所引用的對象最終又引用了這個Block自己。其中獲取器持有completion handler,而completion handler中又對獲取器的url進行引用。

- (void)downloadData
{
    NSURL *url = [NSURL URLWithString:@"www.com"];
    HONetworkFetcher * networkFetcher = [[HONetworkFetcher alloc] initWithURL:url];
    [_networkFetcher startWithCompletionHandler:^(NSData *data) {
        NSLog(@"Request URL %@ finished",networkFetcher.url)
        _fetchData = data;
    }];
}
複製代碼

​ 上面這種保留環,打破也很簡單:

- (void)p_requestCompleted
{
    if (_completionHandler) {
        _completionHandler(_downloadedData);
    }
    self.completionHandler = nil;
}
複製代碼

  • 如若Block所捕獲的對象直接或間接地保留了Block自己,那麼就得擔憂保留環問題;
  • 必定要找個恰當的時機解除保留環,而不能把責任推給API的調用者。

2.4 多用派發隊列,少用同步鎖

​ 在Objective-C中,多線程執行同一份代碼,使用鎖來實現某種同步機制,在GCD以前,有兩種辦法:

​ 其一是「同步Block」:

- (void)synchronizedMethod
{
	//此處同步行爲所針對的對象是self,會根據給定的對象自動建立一個鎖,並等待Block中的代碼執行完畢。執行到代碼結尾處,鎖就釋放了。若對self對象頻繁加鎖,則會須要等到另外一端與此無關的代碼執行完畢才能繼續執行當前的代碼。
    @synchronized(self){
       //Safe
       //do whatever
    }
}
複製代碼

​ 另一種就是:

_lock = [[NSlock alloc] init];
- (void)synchronizedMethod
{
    [_lock lock];
    //Safe
    [_lock unlock];
}
複製代碼

上面兩種方法有其缺陷:極端狀況下,都會致使死鎖,其效率也不高。

替代方案就是:GCD。

_syncQueue = dispatch_queue_create("sync.queue", DISPATCH_QUEUE_SERIAL);

- (NSString*)someString {
    __block NSString *localSomeString;
  	  dispatch_sync(_syncQueue, ^{
         localSomeString = _someString;
    });
    return localSomeString;
}

- (void)setSomeString:(NSString *)someString {
    dispatch_sync(_syncQueue, ^{
       _someString = someString 
    });
}
複製代碼

​ 上面將保證全部讀寫的操做都在同一隊列中,這相比上面加鎖機制,更爲高效(GCD基於底層的優化),也更爲整潔(全部的同步在GCD中實現)。

​ 上面能夠優化的就是,能夠將取值方法,異步讀取,串行隊列裏派發異步操做,會開啓一個新線程來執行異步操做,而不是同步操做那樣全部的操做在同一個線程。以下:

- (NSString*)someString {
    __block NSString *localSomeString;
    dispatch_async(_syncQueue, ^{
         localSomeString = _someString;
    });
    return localSomeString;
}
複製代碼

​ 但雖然是優化,不過有個優化陷進,就是執行異步派發是,須要拷貝Block。若拷貝Block的執行時間比Block執行所用的時間長,那麼就是個「僞優化」,則比原來更慢。因爲本例簡單,因此改完以後可能更慢。

​ 多個獲取方法能夠併發執行,而獲取方法與設置方法之間不能併發執行。能夠採用柵欄函數,此次改用併發隊列:

_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

- (NSString*)someString {
    __block NSString *localSomeString;
  	  dispatch_sync(_syncQueue, ^{
         localSomeString = _someString;
    });
    return localSomeString;
}

- (void)setSomeString:(NSString *)someString {
     dispatch_barrier_sync(_syncQueue, ^{
        _someString = someString 
    });
}
複製代碼

​ 下面是執行:

​ 併發隊列若是發現接下來要處理的塊是個柵欄塊,那麼就一直要等當前全部併發塊都執行完畢,纔會單獨執行這個柵欄塊,待柵欄塊執行事後,再按正常方式繼續向下處理。

​ 測試一下性能,這種作法比剛纔的確定更快。

​ 注意,設置函數也能夠用同步的柵欄塊來實現,那樣作可能會更高效,由於異步須要拷貝代碼塊。

​ 要選方案,仍是最好測一下實際的性能。

  • 派發隊列可用來表述同步語義,這種作法要比使用@synchronizedNSLock對象更簡單;
  • 將同步與異步派發結合起來,能夠實現與普通加鎖機制同樣的同步行爲,而這麼作不會阻塞執行異步派發的線程;
  • 使用同步隊列及柵欄塊,能夠令同步行爲更高效;

2.5 多用GCD,少用performSelector系列方法

​ NSObject中能夠調用任何方法,最簡單以下:

- (id)performSelector:(SEL)selector	
複製代碼

​ 若是選擇子在運行期決定,就能體現出此方式的強大之處了。這就至關於在動態綁定上再次使用動態綁定:

SEL selector;
if(/*some condition */) {
    selector = @selector(bar);
} else if(/* some ohter condition */) {
    selector = @selector(foo);
} else {
    selector = @selector(baz);
}
[object performSelector:selector];
複製代碼

​ 使用此特性的代價是:若是在ARC下編譯此代碼 ,那麼編譯器會發出下面警告:

​ warning:performSelector may cause a leak because its selector is unknown [-Warc-performSelector-leaks]

​ 由於沒法肯定選擇子,也就沒有運用內存管理規則判斷返回值是否是須要釋放。ARC採用了比較謹慎的方法,就是不添加釋放操做。然而這麼作可能致使內存泄漏。下面是一個實例:

SEL selector;
if(/*some condition */) {
    selector = @selector(newObject);
} else if(/* some ohter condition */) {
    selector = @selector(copy);
} else {
    selector = @selector(someProperty);
}
id ret =[object performSelector:selector];
複製代碼

​ 這段代碼,在執行第一個和第二個選擇子時,須要釋放ret對象,而第三個則不須要。可是這個問題很容易被忽視,或者用靜態分析器也沒法偵測到。

編者按:根據蘋果的命名規則,第一個和第二個選擇子建立對象時,會擁有對象的全部權,因此須要釋放。

​ 其次,performSelector方法只返回id類型,即只能是void或者對象類型,而不能是整形等純量類型。

​ 另外,還有幾個performSelector方法以下:

- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
複製代碼
@interface NSObject (NSThreadPerformAdditions)

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
	// equivalent to the first method with kCFRunLoopCommonModes

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
	// equivalent to the first method with kCFRunLoopCommonModes
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg NS_AVAILABLE(10_5, 2_0);

@end
複製代碼

​ 然而,上面的延時執行均可以用dispatch_after來處理:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        //todo
 });
複製代碼
  • performSelector系列方法在內存管理易有疏漏,它沒法肯定將要執行的選擇子具體是什麼,因此ARC編譯器也就沒法插入適當的內存管理方法;
  • performSelector系列方法所能處理的選擇子太過侷限,選擇子返回值類型及發送方給方法的參數個數都受到限制;
  • 若是想延遲執行,最好不要用performSelector系列方法,而是應該把任務封裝到Block裏,調用GCD來實現。

2.6 掌握GCD及操做隊列的使用時機

​ 使用NSOperationNSOperationQueue

  • 取消某個操做;
  • 指定操做間的依賴關係;
  • 經過鍵值觀察機制監控NSOperationNSOperation對象許多屬性都適合經過鍵值觀察機制來監聽,好比isCancelledisFinished
  • 指定操做的優先級;

2.7 使用dispatch_once來執行只需運行一次的線程安全代碼

+ (instancetype)sharedManager {
    static HOClass *shared = nil;
    @synchronized (self) {
        if (!shared) {
            shared = [[self alloc] init];
        }
    }
    return self;
}
複製代碼

​ 更優的實現方式:

+ (instancetype)sharedManager {
    static HOClass *shared = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        shared = [[self alloc]init];
    });
    return shared;
}
複製代碼

dispatch_once能夠簡化代碼而且完全保證線程安全,此外更高效,它沒有使用重量級的同步機制。

2.8 不要使用dispatch_get_current_queue

  • dispatch_get_current_queue函數的行爲經常與開發者所預期的不一樣。此函數已經廢棄,只應作調試只用。
  • 因爲派發隊列是按層級來組織的,因此沒法單用某個隊列對象來描述「當前隊列」這一律念;
  • dispatch_get_current_queue函數用於解決由不可重入代碼說引起的死鎖,然而此函數解決的問題,一般也能改用「隊列特定數據」來解決。

2.9 經過Dispatch Group機制,根據系統資源情況來執行任務

dispatch_group可以把任務分組,調用者能夠等待這組任務執行完畢,也能夠提供回調函數以後繼續往下執行,這組任務完成時,調用者會獲得通知。

  • 一系列的任務可納入一個dispatch_group中,開發者能夠在這組任務完畢時得到通知;
  • 經過dispatch_group,能夠在併發時派發隊列同時執行多項任務。此時GCD會根據系統資源情況來調度這些併發執行的任務。開發者若本身來實現此功能,則須要編寫大量代碼。
相關文章
相關標籤/搜索