1. Block 1.1 什麼是Block 之前都是對block的簡單實用,這裡重新瞭解下。 代碼塊Block是蘋果在iOS4開始引入的對C語言的擴展,實現匿名函數的特性,Block是一種特殊的數據類型,其可以正常定義變數、作為參數、作為返回值,特殊的,block還可以保存一段代碼,在需要的時候調用 ...
1. Block
1.1 什麼是Block
之前都是對block的簡單實用,這裡重新瞭解下。
代碼塊Block是蘋果在iOS4開始引入的對C語言的擴展,實現匿名函數的特性,Block是一種特殊的數據類型,其可以正常定義變數、作為參數、作為返回值,特殊的,block還可以保存一段代碼,在需要的時候調用,目前Block廣泛的應用iOS開發中,常用於GCD、動畫、排序及各類回調。
註:Block的聲明與賦值只是保存了一段代碼段,必須調用才能執行內部的代碼。
1.2 Block簡單的使用
Block的聲明:
Block變數的聲明格式為: 返回值類型(^Block名字)(參數列表); // 聲明一個無返回值,參數為兩個字元串對象,叫做aBlock的Block void(^aBlock)(NSString *x, NSString *y); // 形參變數名稱可以省略,只留有變數類型即可 void(^aBlock)(NSString *, NSString *);
Block的賦值:
Block變數的賦值格式為: Block變數 = ^(參數列表){函數體}; aBlock = ^(NSString *x, NSString *y){ NSLog(@"%@ love %@", x, y); };
Block聲明並賦值:
int(^myBlock)(int) = ^(int num){ return num * 7; }; // 如果沒有參數列表,在賦值時參數列表可以省略 void(^aVoidBlock)() = ^{ NSLog(@"I am a aVoidBlock"); };
Block 變數的調用;
// 調用後控制台輸出"Li Lei love Han Meimei" aBlock(@"Li Lei",@"Han Meimei"); // 調用後控制台輸出"result = 63" NSLog(@"result = %d", myBlock(9)); // 調用後控制台輸出"I am a aVoidBlock" aVoidBlock();
2. Block 數據結構
2.1 Block 數據結構簡單認識
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實例實際上由6部分構成:
- isa 指針,所有對象都有該指針,用於實現對象相關的功能。
- flags,用於按bit位表示一些block的附加信息,本文後面介紹 block copy 的實現代碼可以看到對該變數的使用
- reserved 保留變數
- invoke 函數指針,指向具體的block 實現的函數調用地址
- descriptor 表示該block的附加描述信息,主要是size大小,以及 copy 和 dispose 函數的指針。
- variables , capture 過來的變數,block能夠訪問它外部的局部變數,就是因為將這些變數(或變數的地址)複製到了結構體中。
在 OC 語言中,一共有 3 種類型的 block:
- _NSConcreteGlobalBlock 全局的靜態 block,不會訪問任何外部變數。
- _NSConcreteStackBlock 保存在棧中的 block,當函數返回時會被銷毀
- _NSConcreteMallocBlock 保存在堆中的 block,當引用計數為 0 時會被銷毀。
遇到一個Block,我們怎麼確定這個Block的存儲位置呢?
a。Block不訪問外界變數(包括棧中和堆中的變數)
Block既不在棧又不在堆中,在代碼段中,ARC和MRC都是如此,此時為全局塊。
b。Block訪問外界變數
MRC 環境下:訪問外界變數的Block預設存儲在棧中。
ARC 環境下:訪問外界變數的Block預設存儲在堆中(實際是放在棧區,然後ARC情況下自動又拷貝到堆區),自動釋放。
2.2 NSConcreteGlobalBlock 類型的 block 的實現
我們可以新建一個block1.c文件:
#include <stdio.h> int main() { ^{ printf("Hello, World!\n"); } (); return 0; }
在終端輸入 clang -rewrite-objc block1.c ,就可以在目錄中看到 clang 輸出了一個 block1.cpp 的文件,這個文件就是 block 在 C 語言的實現:
struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* 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; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { printf("Hello, World!\n"); } 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) }; int main() { (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA) (); return 0; }
- 一個block實際就是一個對象,它主要由一個 isa 和一個 impl 和一個 descriptor 組成。
- 這裡我們看到 isa 指向的還是 _NSConcreteStackBlock,但在 LLVM 的實現中,開啟 ARC 時,block 應該是 _NSConcreteGlobalBlock 類型。感覺是當一個 block 被聲明的時候,它都是一個 _NSConcreteStackBlock類的對象。
- impl 是實際的函數指針,本例中,它指向 _main_block_func_0。這裡的 impl 相當於之前提到的 invoke 變數,只是 clang 編譯器對變數的命名不一樣。
- descriptor 是用於描述當前這個 block 的附加信息的,包括結構體的大小,需要 捕獲 和 處理 的變數列表等。結構體大小需要保存是因為,每個 block 因為會 捕獲 一些變數,這些變數會加到 __main_block_impl_0 這個結構體中,讓其體積變大。後面會看到相關代碼。
2.3 NSConcreteStackBlock 類型的 block 的實現
我們另外新建一個名為 block2.c 的文件,輸入一下內容:
#include <stdio.h> int main() { int a = 100; void (^block2)(void) = ^{ printf("%d\n", a); }; block2(); return 0; }
再次使用 clang 工具,轉換後的關鍵代碼如下:
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int a; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int a = __cself->a; // bound by copy printf("%d\n", a); } 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)}; int main() { int a = 100; void (*block2)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a); ((void (*)(__block_impl *))((__block_impl *)block2)->FuncPtr)((__block_impl *)block2); return 0; }
在本例中,我們可以看到:
- 本例中,isa 指向 _NSConcreteStackBlock,說明這是一個分配在棧上的實例。
- main_block_impl_0 中增加了一個變數a,在block中引用的變數a實際上是在申明block時,被覆制到 main_block_impl_0 結構體中的那個變數a。y因為這樣,我們就能理解,在block內部修改變數a的內容,不會影響外部的實際變數a。
- main_block_impl_0 中由於增加了一個變數a,所以結構體的大小變了,該結構體大小被寫在了 main_block_desc_0 中。
我們修改上面的代碼,在變數前面增加 __block 關鍵字:
#include <stdio.h> int main() { __block int i = 1024; void (^block1)(void) = ^{ printf("%d\n", i); i = 1023; }; block1(); return 0; }
生成的關鍵代碼如下,可以看到,差異很大:
struct __Block_byref_i_0 { void *__isa; __Block_byref_i_0 *__forwarding; int __flags; int __size; int i; }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __Block_byref_i_0 *i; // by ref __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__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_i_0 *i = __cself->i; // bound by ref printf("%d\n", (i->__forwarding->i)); (i->__forwarding->i) = 1023; } static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);} static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 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() { __attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 1024}; void (*block1)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344); ((void (*)(__block_impl *))((__block_impl *)block1)->FuncPtr)((__block_impl *)block1); return 0; }
從代碼中我們可以看到:
- 源碼中增加一個名為 __block_byref_i_0 的結構體,用來保存我們要 捕獲 並且修改的變數 i。
- main_block_impl_0 引用的是 Block_byref_i_0 的結構體指針,這樣就可以達到修改外部變數的作用。
- __Block_byref_i_0 結構體中帶有 isa,說明它也是一個對象。
- 我們需要負責 Block_byref_i_0 結構體相關的記憶體管理,所以 main_block_desc_0 中增加了 copy 和 dispose 函數指針,對於在調用前後修改響應變數的引用計數。
為什麼使用__block 修飾的外部變數的值就可以被block修改呢?
我們發現一個局部變數加上 __block 修飾符後竟然跟block一樣變成了一個__Block_byref_i_0結構體類型的自動變數實例。此時我們在block內部訪問 i 變數則需要通過一個叫 __forwarding 的成員變數來間接訪問 i 變數。
__block 變數和 __forwarding
在copy操作之後,既然__block變數也被copy到堆上去了,那麼訪問該變數是訪問棧上還是堆上的呢?
通過__forwarding, 無論是在block中還是 block外訪問__block變數, 也不管該變數在棧上或堆上, 都能順利地訪問同一個__block變數。
2.3 NSConcreteMallocBlock 類型的 block 的實現
NSConcreteMallocBlock 類型的 block 通常不會在源碼中直接出現,因為預設它是當一個 block 被 copy 的時候,才會將這個 block 賦值到堆中。以下是一個 block 被copy 時的示例代碼,可以看到,在第8步,目標的 block 類型被修改為 _NSConcreteMallocBlock。
static void *_Block_copy_internal(const void *arg, const int flags) { struct Block_layout *aBlock; const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE; // 1 if (!arg) return NULL; // 2 aBlock = (struct Block_layout *)arg; // 3 if (aBlock->flags & BLOCK_NEEDS_FREE) { // latches on high latching_incr_int(&aBlock->flags); return aBlock; } // 4 else if (aBlock->flags & BLOCK_IS_GLOBAL) { return aBlock; } // 5 struct Block_layout *result = malloc(aBlock->descriptor->size); if (!result) return (void *)0; // 6 memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first // 7 result->flags &= ~(BLOCK_REFCOUNT_MASK); // XXX not needed result->flags |= BLOCK_NEEDS_FREE | 1; // 8 result->isa = _NSConcreteMallocBlock; // 9 if (result->flags & BLOCK_HAS_COPY_DISPOSE) { (*aBlock->descriptor->copy)(result, aBlock); // do fixup } return result; }
3. 變數的複製
對於 block 外的變數引用,block預設是將其複製到其數據結構中來實現訪問的,也就是說block的自動變數只針對block內部使用的自動變數,不使用則不截獲,因為截獲的自動變數會存儲於block的結構體內部,會導致block體積變大,預設情況下 block 只能訪問不能修改局部變數的值,如下圖所示:
對於 __block 修飾的外部變數引用,block 是複製其引用地址來實現訪問的,block可以修改__block 修飾的外部變數的值,如下圖所示:
4. ARC 對 block 類型的影響
在 ARC 開啟的情況下,將只會有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 類型的 block。
原本的 NSConcreteStackBlock 的 block 會被 NSConcreteMallocBlock 類型的 block替代。證明方式是以下代碼再 XCode 中,會輸出 <__NSMallocBlock__: 0x100109960>。
在蘋果的官方文檔中也提到,當把棧中的block返回時,不需要調用 copy 方法了。
#import <Foundation/Foundation.h> int main(int argc, const char * argv[]) { @autoreleasepool { int i = 1024; void (^block1)(void) = ^{ printf("%d\n", i); }; block1(); NSLog(@"%@", block1); } return 0; }
ARC下,訪問外界變數的 Block 為什麼要從棧區拷貝到堆區呢?
棧上的Block,如果其所屬的變數作用域結束,該Block就被廢棄,如同一般的自動變數。當然,Block中的__block變數也同時被廢棄:
為瞭解決棧塊在其變數作用域結束之後被廢棄(釋放)的問題,我們需要把Block複製到堆中,延長其生命周期。開啟ARC時,大多數情況下編譯器會恰當地進行判斷是否有需要將Block從棧複製到堆,如果有,自動生成將Block從棧上複製到堆上的代碼。Block的複製操作執行的是copy實例方法。Block只要調用了copy方法,棧塊就會變成堆塊。
如下圖:
5. 鏈式語法的實現
類似於第三方自動佈局 Masonry 的代碼:
[view1 mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(superview.mas_top).with.offset(padding.top); make.left.equalTo(superview.mas_left).with.offset(padding.left); make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom); make.right.equalTo(superview.mas_right).with.offset(-padding.right); }];
5.1 如何實現
我們舉個例子,假如對於一個已有類的實例 classInstance,現在要用句點 . 和小括弧 () 的方式連續調用它的"方法" method1,method2,method3,如下圖所示:
從圖中我們可以知道,要實現鏈式語法,主要包含 點語法、小括弧調用、連續訪問 三部分:
- 點語法:在OC中,對於點語法的使用,最常見於屬性的訪問,比如對在方法內部調用 self.xxx,在類的實例中用 classInstance.xxx;
- 小括弧調用:OC中一般用中括弧 [] 來實現方法的調用,而對於 Block 的調用則還是保留使用小括弧 ( ) 的方式,因此我們可以考慮用 Block來實現鏈式語法中的 ();
- 如何實現連續訪問?:Block可以理解為帶有自動變數的匿名函數或函數指針,它也是有返回值的。我們可以把上述類實例每次方法的調用(實質為 Block 的調用)的返回值都設為當前類實例本身,即 classInstance.method1() 返回了當前 classInstance ,此時才能在其後面繼續執行 .method2() 的調用,以此類推。
總結一句話:我們可以定義類的一些只讀 Block 類型的屬性,並把這些 Block 的返回值類型設置為當前類本身,然後實現這些 Block 屬性的 getter 方法。
下麵是一個Demo,鏈式計算器的例子,可以連續地調用計算器的加減乘除進行計算:
@interface Cacluator : NSObject @property (assign, nonatomic) NSInteger result; // 下麵分別定義加減乘除四個只讀block類型的屬性 // 設置為只讀是為了限制只需要實現 getter方法 // 這裡每個 Block 類型的屬性攜帶一個 NSInteger 類型的參數,返回參數是當前類型 @property (copy, nonatomic, readonly) Cacluator *(^add)(NSInteger number); @property (copy, nonatomic, readonly) Cacluator *(^minus)(NSInteger number); @property (copy, nonatomic, readonly) Cacluator *(^multiply)(NSInteger number); @property (copy, nonatomic, readonly) Cacluator *(^divide)(NSInteger number); @end @implementation Cacluator // 此處為 add 屬性的 getter方法實現 // 前面聲明 add 屬性的類型為 block 類型,所以此處 getter 返回一個 block // 對於返回的 block,返回值類型為 Calculator,所以返回self -(Cacluator *(^)(NSInteger))add{ return ^id(NSInteger num){ self.result += num; return self; }; } -(Cacluator *(^)(NSInteger))minus{ return ^id(NSInteger num){ self.result -= num; return self; }; } -(Cacluator *(^)(NSInteger))multiply{ return ^id(NSInteger num){ self.result *= num; return self; }; } -(Cacluator *(^)(NSInteger))divide{ return ^id(NSInteger num){ NSAssert(num != 0, @"除數不能為0"); self.result /= num; return self; }; } @end
測試代碼:
Calculator *calc = [[Calculator alloc] init]; // 初始化一個計算器類實例 calc.add(8).minus(4).multiply(6).divide(3); // 鏈式調用 NSLog(@"%d", (int)calc.result); // 輸出 8
分析:
上面 calc.add 訪問 calc 的 add 屬性會調用 [calc add] 方法,此方法會返回一個Block如下: ^id(NSInteger num){ self.result += num; return self; }; 在這個Block中,前面已聲明其返回值類型為:Caculator,所以在其裡面返回了 self,這樣當調用該 Block 時,會返回 self (實例本身),流程如下: 1.calc.add 獲得一個 Block 2.calc.add(8) Block 的執行,並返回了 self (即實例 calc) 3.於是在 calc.add(8) 後面可繼續訪問 calc 的其他屬性,一路點下去
5.2 更簡潔的實現
上面是通過先聲明一系列的Block屬性, 再去實現Block屬性的getter 方法來實現鏈式調用,感覺還是有點麻煩,我們去看看是否有更簡潔的實現方式:
點語法的本質:
- 在OC中,點語法實際上只是一種替換手段,對於屬性的getter方法,class.xxx 的寫法最終會被編譯器替換成 [class xxx];對於setter 方法,即把 class.xxx 寫在等號左邊,class.xxx = value 會被轉換成 [class setXxx:value],本質都是方法調用
- 即使再class中並沒有顯式聲明 xxx 屬性,在編譯代碼時,代碼中如果有 class.xxx 的寫法也會被替換成 [class xxx],所以只要在class中有一個聲明為 xxx 的方法,即可在代碼中其它地方寫 class.xxx
所以,解決方案是:
在定義類的頭文件的@interface中,直接聲明某一方法名為xxx,該方法的返回值是一個Block,而此block的返回值設為該類本身。
@interface Calculator : NSObject @property (nonatomic, assign) NSInteger result; // 保存計算結果 // 上面的屬性聲明其實是可以省略的,只要聲明下麵方法即可; // 在 Objective-C 中,點語法只是一種替換手段,class.xxx 的寫法(寫在等號左邊除外)最終會被編譯器替換成 [class xxx],本質上是方法調用; // add、minus、multiply、divide 四個方法都會返回一個 Block, // 這個 Block 有一個 NSInteger 類型的參數,並且其返回值類型為當前 Calculator 類型; // 下麵四個方法的實現與上面 Calculator.m 中的一致。 - (Calculator * (^)(NSInteger num)) add; - (Calculator * (^)(NSInteger num)) minus; - (Calculator * (^)(NSInteger num)) multiply; - (Calculator * (^)(NSInteger num)) divide;
Masonry 也是這麼做的,只聲明瞭方法,並沒有聲明相應的屬性。另外,對於Masonry鏈式語法中的 .and、.with 等寫法只是為了讓代碼讀起來更通順,實現方式為:聲明一個名為 and 和 with 的方法,在方法里返回self:
- (MASConstraint *)with { return self; } - (MASConstraint *)and { return self; }
存在的問題:
當用點語法去訪問類的某一個 Block 屬性時,Block 後面的參數 Xcode
XXXHTTPManager *http = [XXXHTTPManager manager]; // 下麵 .get(...) 裡面的參數,Xcode 並不會提示自動補全,需要手動去填寫,.success(...) .failure(...) 等也一樣, // 這裡不能像傳統中括弧 [] 方法調用那樣,輸入方法名就可以自動提示該方法所有的參數並按回車自動補全。 http.get(@"https://kangzubin.cn", nil).success(^(NSURLSessionDataTask *task, id responseObject) { // Success TODO }).failure(^(NSURLSessionDataTask *task, NSError *error) { // Failure TODO }).resume();
Xcode 中有個強大但未被充分利用的功能:Code Snippets(代碼塊)可以解決。
http://www.imlifengfeng.com/blog/?utm_medium=email&utm_source=gank.io&p=457