iOS 編寫高質量Objective-C程式碼(六)
級別: ★★☆☆☆
標籤:「iOS」「Block」「Objective-C」
作者:MrLiuQ
審校:QiShare團隊
前言: 這幾篇文章是小編在鑽研《Effective Objective-C 2.0》的知識產出,其中包含作者和小編的觀點,以及小編整理的一些demo。希望能幫助大家以簡潔的文字快速領悟原作者的精華。 在這裡,QiShare團隊向原作者Matt Galloway表達誠摯的敬意。
本篇的主題是iOS中的 “Block的原理及應用” 。
先簡單介紹一下今天的主角: block
。
- block(塊):是一種 “ 詞法閉包 ” ,通過block,開發者可將程式碼塊像物件一樣傳遞。
一、理解“block”的概念:
1. block的資料結構:
通過clang命令列工具(OC轉C++),我們先來看一下 block
的內部資料結構大概是什麼樣子的?
struct Block_descriptor { unsigned long int reserved; unsigned long int size; void (*copy)(void *dst, void *src); void (*dispose)(void *); }; struct Block_layout { void *isa; int flags; int reserved; void (*invoke)(void *, ...); struct Block_descriptor *descriptor; /* Imported variables. */ }; 複製程式碼
解析:很顯然,Block_layout是一個結構體:裡面有一個isa指標,指向Class物件。還有一個函式指標,指向了塊的實現程式碼。

2. block的三種類型:全域性塊、棧塊、堆塊。
根據block在記憶體中的位置,block被分成三種類型:
型別 | 記憶體位置 | 介紹 |
---|---|---|
__NSStackBlock__ | 棧區 | 棧內有效,出棧後銷燬。 |
__NSMallocBlock__ | 堆區 | copy到堆空間上。可以在定義的那個範圍之外使用。 |
__NSGlobalBlock__ | 全域性區 | 不捕捉任何外部變數,全部資訊在編譯器就已確定。 |
- 1. NSStackBlock 棧塊: 棧塊保存於棧區,超出變數作用域,棧上的
block
以及宣告的_block
都會被銷燬。
例如:
__block NSString *name = @"QiShare"; void (^block)(void) = ^{ NSLog(@"%@ is an iOS team which loves to share technology.", name); }; NSLog(@"block = %@", block); 複製程式碼
小知識點:當block內部需要修改或訪問外部變數時,外部變數需要額外用 __block
修飾。否則修改不了。
我們來看下列印:

什麼?居然是 __NSMallocBlock__
(堆塊)? 那是因為ARC環境下,編譯器自動幫我們加了copy操作。
這時我們關掉ARC:設定 Objective-C Automatic Reference Counting = NO
。再來看下列印:

- 2. NSMallocBlock 堆塊: 堆block記憶體位於堆區,在變數作用域結束時依然可以使用。
通過上面的例子: 在ARC下,block會預設加上copy操作:變成 __NSMallocBlock__
。
- 3. NSGlobalBlock 全域性塊: 塊中無任何外界物件,所需的記憶體在編譯時就可以確定,記憶體位於全域性區。 類似於“單例”,copy是一個空操作。
例如:
void (^qiShare)(void) = ^{ NSLog(@"We love sharing."); }; NSLog(@"%@",qiShare); 複製程式碼

二、為常用的block型別建立typedef
為了增加程式碼的***可讀性*** 和 可拓展性 , 需要為常用的block起個別名。
以 typedef
為塊起別名,也可令塊變數用起來更加簡單~ 比如:
- (void)getDataWithToken:(NSString *)token success:(void (^)(id responseDic))success; //! 以上要改成下面這種 typedef void (^SuccessBlock)(id responseDic); - (void)getDataWithToken:(NSString *)token success:(SuccessBlock)success; 複製程式碼
三、用handler塊降低程式碼分散程度
在我們iOS開發中,經常會非同步執行一些任務,等待任務執行結束後再通知物件呼叫相關方法。 一般有兩種做法:
- 第一種:使用NSNotificationCenter:
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
- 第二種:使用委託協議:詳情見 iOS 編寫高質量Objective-C程式碼(四) 。
- 第三種:使用block回撥:直接把block物件當做引數傳給相關方法執行。
舉個例子:AFNetworking的API設計及使用就是block回撥
- 介面設計:
- (NSURLSessionDataTask *)POST:(NSString *)URLString parameters:(id)parameters success:(void (^)(NSURLSessionDataTask *task, id responseObject))success failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure { return [self POST:URLString parameters:parameters progress:nil success:success failure:failure]; } 複製程式碼
- 使用:
AFHTTPSessionManager *manger =[AFHTTPSessionManager manager]; NSString *urlString = @""; NSMutableDictionary *parameter= @{@"":@"",@"":@""}; [manger POST:urlString parameters:parameter success:^(NSURLSessionDataTask * _Nonnull task, id_Nullable responseObject) { NSLog(@"成功"); } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { NSLog(@"%@",error); }]; } 複製程式碼
四、用block引用其所屬物件時避免出現迴圈引用
在我們日常開發中,如果block使用不當,很容易導致記憶體洩漏。
- 理由:如果
block
被當前ViewController(self
)持有,這時,如果block內部再持有ViewController(self
),就會造成迴圈引用。 - 解決方案:在
block
外部對 弱化self
,再在block內部 強化 已經弱化的weakSelf
For Example:
__weak typeof(self) weakSelf = self; [self.operationQueue addOperationWithBlock:^{ __strong typeof(weakSelf) strongSelf = weakSelf; if (completionHandler) { KTVHCLogDataStorage(@"serial reader async end, %@", request.URLString); completionHandler([strongSelf serialReaderWithRequest:request]); } }]; 複製程式碼
當然,也不是所有block中使用到 self
都要先弱化成 weakSelf
,再強化成 strongSelf
, 只要block沒有被self所持有的,在block中就可以使用self。 比如下面:
[QiNetwork requestBlock:^(id responsObject) { NSLog(@"%@",self.name); }]; 複製程式碼
小貼士:記憶體洩漏檢測相關知識請看: iOS 記憶體洩漏排查方法及原因分析
關注我們的途徑有:
QiShare(微信公眾號)