我所理解的 Block
作者:bool周 原文鏈接:我所理解的 Block
關于 block 的文章,網上已經有很多了。我這里只是將這個知識點再梳理一下,從 block 使用到底層原理。畢竟年紀大了,容易忘事。
拋磚引玉
圍繞 block 所產生的問題,太多太多。這里我將這些問題羅列出來,如果你對某些問題感到懵逼,可以在下文中找到答案。找不到,私信我。
- 為什么要用 block?畢竟它的語法難記,還容易產生內存泄漏。
- block 的各種書寫格式,你是否了解?
- 按內存區這一維度劃分,block 可以分為哪幾種類型,如何定義的?
- block 是 Objective-C 對象嗎?
- block 內部實現原理是怎樣的?
- 怎樣寫會造成循環引用,又是如何避免循環引用?
- 如果以上問題你都了解,可以不用往下看了。
為什么使用 Block
block 的唯一好處就是:使代碼變得更簡潔。
我們可以向一個方法以參數的形式傳遞一個 block,作為方法的 callback 函數。類似于向方法傳遞一個函數指針。這樣就不必再聲明一個新的方法,并調用,在一定程度上簡化了代碼。下面有一個例子:
使用 notification 時,常規方式是注冊一個 selector 并實現對應的方法,像這樣:
- (void)viewDidLoad {[super viewDidLoad];[[NSNotificationCenter defaultCenter] addObserver:selfselector:@selector(keyboardWillShow:)name:UIKeyboardWillShowNotification object:nil]; }- (void)keyboardWillShow:(NSNotification *)notification {// Notification-handling code goes here. } 復制代碼如果使用 block,可以寫成這樣:
- (void)viewDidLoad {[super viewDidLoad];[[NSNotificationCenter defaultCenter] addObserverForName:UIKeyboardWillShowNotificationobject:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {// Notification-handling code goes here. }]; } 復制代碼另外一個簡化代碼的特性就是,block 可以捕獲外部變量。這樣就不必再以參數的形式傳遞,簡化的方法的定義和調用。
Block 長什么樣
在最初接觸 block 時,我經常寫不對,它的語法太另類。fucking block syntax 提供了各種 block 的寫法,我這里就直接照搬過來了。
- 作為局部變量
- 作為屬性(property)
- 定義方法時,作為方法參數
- 調用方法時,作為參數傳遞
- 作為類型別名 (typedef),增加代碼可讀性
Block 內部原理是怎樣的
在編譯時,編譯器會將 block 語法轉化成 C 的源代碼,再將這部分 C 的源代碼編譯為編譯器處理的代碼。我們可以使用 clange (LLVM 編譯器) 來完成 "將 block 語法轉化為 C++ 源代碼 (本質還是 C)" 這一階段。具體命令如下:
clang -rewrite-objc 源代碼文件名 復制代碼1.一個簡單 Block 的結構
下面我們轉化一段 OC 代碼來分析 block。
使用 clang -rewrite-objc main.m 轉化如下代碼:
int main(int argc, char * argv[]) {void (^myBlock) (void) = ^{printf("test block");};myBlock();return 0; } 復制代碼轉化接入后是下面這個樣子(主要代碼)。因為語法和命名的關系,代碼看著很亂,但是邏輯很清晰。為了方便理解,我加了部分注釋。
// block 結構體。可以理解為 'block' 這種類型的基本結構 struct __block_impl {void *isa;int Flags;int Reserved;void *FuncPtr; };// 整個 block 的結構,命名有點歧義,理解即可 struct __main_block_impl_0 {struct __block_impl impl; // __block_impl 類型的成員變量struct __main_block_desc_0* Desc; // Desc 指針// 構造函數主要是為兩個成員變量賦值__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {impl.isa = &_NSConcreteStackBlock;impl.Flags = flags;impl.FuncPtr = fp;Desc = desc;} };// block 的代碼塊,實際執行部分 static void __main_block_func_0(struct __main_block_impl_0 *__cself) {printf("test block"); }// 版本升級所需的區域和 block 大小。不懂也沒關系 static struct __main_block_desc_0 {size_t reserved;size_t Block_size; } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};// main 方法 int main(int argc, char * argv[]) {// 定義 blockvoid (*myBlock) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));// 執行 block((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);return 0; } 復制代碼上述代碼中,定義了三個結構體:block 基本結構 __block_impl、Desc 指針 __main_block_desc_0、整個 block 的結構 __main_block_impl_0。其中 __main_block_impl_0 包含兩個成員變量,分別為 __block_impl 結構體實例和 __main_block_desc_0 指針。
上述還定義了兩個方法:block 實際執行方法 __main_block_func_0 和 main() 方法。
__main_block_func_0 方法為輸出對應的字符串("test block")。
main 方法主要分為兩步:
定義 block。將 block 實際執行方法,也就是 __main_block_func_0 的函數指針和 __main_block_desc_0_DATA 的地址傳入 __main_block_desc_0 的構造方法,構造成一個完整的 block。根據定義可以看出 __main_block_desc_0 初始化時所有的大小為 __main_block_impl_0 結構體大小。
執行 block。實際可以簡化為 *myBlock->impl.FuncPtr,就是調用對應的方法。
了解了這個基本結構,后面的都是在這基礎上追加部分代碼,很容易理解。
2.Block 結構與 isa 指針
在上述代碼中,我們可以看出 block 結構體,也就是 __block_impl 中有一個 isa 指針。我們先來看看這個 isa 指針。
“id" 這一變量類型用于存儲 OC 對象。在 runtime.h 中,它的定義如下:
typedef struct objc_objct {Class isa; } *id; 復制代碼Class 類型屬于一個結構體指針類型,定義為:
typedef struct objc_class *Class 復制代碼objc_class 結構體定義如下:
struct objc_class {Class isa; } 復制代碼綜上可知,OC 中每個類的結構體就是基于 objc_class 結構體。
在上面可以看到這樣一段代碼:
impl.isa = &_NSConcreteStackBlock; 復制代碼isa 被賦值為 _NSConcreteStackBlock 類型的指針。那么 _NSConcreteStackBlock 又是什么?通過 debug 界面我們可以看到如下情況 :
block 一供有三種類型,分別為 __NSGlobalBlock__、__NSStackBlock__、__NSMallocBlock__,這三種類型后面會詳細解釋。這里轉化的代碼和 debug 界面顯示的類型不一樣,但是基本類型以信仰,都是 Class 類型,不必糾結。
可以看出 _NSConcreteStackBlock 實際是 Class 類型。那么,block 本質就是 Objective-c 對象。
3.捕獲自動變量
我們將源代碼改為如下情況:
int main(int argc, char * argv[]) {int val = 10;void (^myBlock) (void) = ^{printf("value is %i", val);};myBlock();return 0; } 復制代碼使用 clang 進行轉化。我們只看轉化后的關鍵部分。即整個 block 結構:
struct __main_block_impl_0 {struct __block_impl impl;struct __main_block_desc_0* Desc;int val;__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : val(_val) {impl.isa = &_NSConcreteStackBlock;impl.Flags = flags;impl.FuncPtr = fp;Desc = desc;} };static void __main_block_func_0(struct __main_block_impl_0 *__cself) {int val = __cself->val; printf("value is %i", val); } 復制代碼可以看到局部變量 val 被自動追加到了 __main_block_impl_0 結構體中,并在構造函數中添加了參數。通過構造函數初始化 block 時,會將外部變量捕獲進來。這里捕獲的是引用,所以在 block 內部改變局部變量的值之后,并不會傳出去。
4.關于 __block
正常情況下,block 捕獲的變量是不可以修改的。但是有兩種方式可以讓其修改:
- 使用靜態變量、靜態全局變量、全局變量。因為前兩個生成在靜態數據區,最后一個生成在堆區。它們都不會隨著 block 棧的消失而被釋放。出了 block 作用域依然有效。但是平時使用這種變量諸多不變。
- 使用 __block 關鍵字修飾。它類似于 static、auto 和 register 這些關鍵字,主要來指定變量存儲在哪個區域。
為什么使用 __block 關鍵字修飾之后就可以修改。我們使用 clang 轉化如下一段代碼:
int main(int argc, char * argv[]) {__block int val = 10;void (^myBlock) (void) = ^{val = 20;};myBlock();return 0; } 復制代碼轉換后如下,可以看出加了一句 __block 多了很多代碼,依然是代碼很亂,但是邏輯很清晰,我們只看主要部分 :
struct __Block_byref_val_0 {void *__isa; __Block_byref_val_0 *__forwarding;int __flags;int __size;int val; };struct __main_block_impl_0 {struct __block_impl impl;struct __main_block_desc_0* Desc;__Block_byref_val_0 *val; // by ref__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {impl.isa = &_NSConcreteStackBlock;impl.Flags = flags;impl.FuncPtr = fp;Desc = desc;} };static void __main_block_func_0(struct __main_block_impl_0 *__cself) {__Block_byref_val_0 *val = __cself->val; // bound by ref(val->__forwarding->val) = 20; }static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/); }static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/); }static struct __main_block_desc_0 {size_t reserved;size_t Block_size;void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);void (*dispose)(struct __main_block_impl_0*); } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};int main(int argc, char * argv[]) {__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};void (*myBlock) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);return 0; } 復制代碼我們可以看出局部變量轉化為一個結構體:
struct __Block_byref_val_0 {void *__isa; __Block_byref_val_0 *__forwarding;int __flags;int __size;int val; }; 復制代碼在 __main_block_impl_0 中追加了一個 __Block_byref_val_0 結構體指針,后續的初始化和修改 val 的值也是通過指針來操作。所以修改后的值就可以傳出去了。
5.block 的存儲類型
前面有提到過,block 按照存儲類型劃分,可以分為三種:
- _NSConcreteGlobalBlock
- _NSConcreteStackBlock
- _NSConcreteMallocBlock
他們在內存中的存儲結構如下圖所示,對號入座:
我們分別來解釋一下。
**_NSConcreteGlobalBlock,也叫全局 block。**有兩種生成方式: 一種是在全局的地方生成,不存在捕獲局部變量的情況。例如:
void(^globalBlock)(void) = ^{printf("this is global block");};int main(int argc, char * argv[]) {globalBlock();return 0; } 復制代碼另一種是,block 中不截獲局部變量。例如:
typedef int (^TestBlock) (int); int main(int argc, char * argv[]) {TestBlock block = ^(int num) {printf("num is %d",num);}return 0; } 復制代碼**_NSConcreteStackBlock,也叫棧 block。**除了上述的初始化方式,通過其他方式初始化為的 block 都是棧 block。
_NSConcreteMallocBlock,也叫堆 block。 堆 block 不是由代碼初始化來的,而是由棧 block 調用 copy 方法時從棧內存拷貝到堆內存而得來的。
至于什么時候會發生 copy 操作,可以總結為一下幾點 (ARC 環境):
- Cocoa 框架的方法且方法名中含有 usingBlock。
- GCD 中的方法。
- block 賦值給強引用對象時。
- 作為返回值時。
- 顯示調用 copy 方法。
下面是一些例子:
typedef BOOL (^TestBlock)(NSString *); typedef void (^paramBlock)(NSString *);@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];int val = 10;// block1 is global blockvoid (^block1)(NSString *) = ^(NSString *name) {NSLog(@"this is global block");};// block2 is malloc blockvoid (^block2)(NSString *) = ^(NSString *name) {int value = 10 * val;NSLog(@"this is malloc block");};// block3 is stack block__weak void (^block3)(NSString *) = ^(NSString *name) {int value = 10 * val;NSLog(@"this is stack block");};// block4 is malloc blockTestBlock block4 = [self testWithBlock:^(NSString *name) {NSLog(@"noting");}];// block5 is global blockTestBlock block5 = [self getGlobalBlock];}- (TestBlock)testWithBlock:(paramBlock)block {dispatch_async(dispatch_get_main_queue(), ^{NSLog(@"capture block is %@",block); // malloc block});int val = 10;return ^BOOL(NSString *name) {int value = val * 10;NSLog(@"noting");return YES;}; }- (TestBlock)getGlobalBlock {return ^BOOL(NSString *name) {NSLog(@"nothing");return YES;}; } 復制代碼6. __block 變量結構中的 __forwarding
在前面的代碼中,我們發現 __block 代碼中有一個 __forwarding,如下面的代碼:
struct __Block_byref_val_0 {void *__isa; __Block_byref_val_0 *__forwarding;int __flags;int __size;int val; }; 復制代碼長話短說。當一個棧 block 捕獲了一個在棧上生成的 __block 變量,那么隨著 block 從棧上 copy 到堆上,這個 __block 變量也從棧上 copy 到堆上。因為有一個 __forwarding 指針,使得無論從從棧上還是堆上,訪問的都是一個變量。如果沒有明白看下面的圖和代碼。
__block int val = 10;void (^block)(int) = [^(int count) { val++;} copy];val++; block();NSLog(@"val is %d",val); // val is 12; 復制代碼無論是操作棧上的 val 變量還是堆上的 val 變量,最終修改的是同一個值。
7.block 與循環引用
發生循環引用說明出現了互相持有的現象,例如下面這樣:
上圖中 self 持有 blk 屬性,blk 持有 block,block 持有 self,這就形成了一個環。現如今的 Xcode 已經很智能,這種簡單的循環引用,會出現警告。
為避免循環引用,可以使用 __weak 關鍵字。例如下面這樣:
__weak typeof(self) weakSelf = self;self.blk = ^BOOL(NSString *name) {[weakSelf log];return YES; }; 復制代碼為了避免在 block 內使用 self 期間,self 被釋放。可以在 block 內部對 self 進行強引用。因為這個強引用生成在 block 棧內,會隨著 block 的作用域消失而消失。不會產生循環引用。
__weak typeof(self) weakSelf = self;self.blk = ^BOOL(NSString *name) {__strong typeof(self) self = weakSelf;[self log];return YES; }; 復制代碼如何使用 Block
前面講了很多原理,過程中也講了很多使用。這里只總結幾點,使用 block 一定要注意:
- block 的命名方式,牢記。
- 對于要再 block 內修改的變量,加 __block 修飾符。對于 OC 中的一些對象,例如 NSMutableArray,如果只修改數組內的元素,不需要加 __block;如果要修改數組的指針,需要加 __block。
- 使用自定義 block 時,注意循環引用的問題。尤其是各種間接關系產生的循環引用。
- 對于捕獲到 block 中的弱引用,如果怕使用期間被釋放,需要再 block 內部再次強引用一下。
綜上,block 總結完畢,祝好運。
參考文獻
1.A Short Practical Guide to Blocks
2.How Do I Declare A Block in Objective-C?
3.Objective-C高級編程
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的我所理解的 Block的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Django内置分页扩展
- 下一篇: JSP完全自学手册图文教程