Block通過Clang編譯器編譯成C++語言後,可以看到它其實是一個結構體。結構及成員變數的構成如下圖所示:
Block的結構中首地址指向的就是isa指標,因此Blcok其實也是我們OC中的物件。通過編譯器的處理成C++底層的程式碼時,Block就是一個結構體,其程式碼結構如下
struct __main_block_impl_0 {
// impl結構體
struct __block_impl {
void *isa; //block的isa指標
int Flags; //位移列舉標記(標記desc中有無 copy , dispose方法,有無方法簽名字元 Signature 等...)
int Reserved;
void *FuncPtr; //實現block的功能函式
} impl ;
struct __main_block_desc_0 {
size_t reserved;
size_t Block_size; //block 的 記憶體大小
/** 以下兩個函式是在 isa 指標指向 _NSConcreteMallocBlock時才會有 **/
void (*copy)(void);
void (*dispose)(void);
/** 以下字串是在impl.flag 包含((1 << 30)這個值是才有的變數),對應oc中的方法簽名NSMethodSignature**/
const char *signatureStr;
} * Desc;
/** 以下都是block捕獲的變數 ,變數順序和是否捕獲進來根據block的定義來決定 ,這裡只是簡單舉例**/
struct __Block_byref_var_0 *var ; // __block變數
TestClass *__strong strongTestVar ; // strong 變數
TestClass *__weak weakTestVar ; // weak 變數
int a ; //區域性普通資料型別
int *b ;//區域性靜態變數
/**全域性靜態變數是直接通過變數的地址訪問的不需要捕獲進來*/
}
複製程式碼
isa - Block 的型別(isa指標的指向)分為 3種
_NSConcreteStackBlock
: 只用到外部區域性變數 , 且沒有強指標引用的block , 其實質上就是函式棧上的區域性變數,在當前函式呼叫完後:(恢復棧空間的時候),就會被釋放掉。_NSConcreteGlobalBlock
: 完全沒有用到外部變數 ,或只用到全域性變數、靜態變數的block ,生命週期從建立到應用程式結束。PS : Block 訪問全域性變數或靜態變數 都是通過捕獲他們的地址進行內容訪問的,因為這些變數從定義的那一刻開始就確定了其地址,因此可以通過指標傳遞來捕獲到block內部進行訪問。而捕獲普通區域性變數就不一樣,區域性變數在函式返回後其記憶體有可能會被會回收掉,所以是不能通過捕獲區域性變數的地址到block訪問的而是通過值傳遞來傳進block內部
_NSConcreteMallocBlock
(估計是我們最常解除的block型別了) :特點是有強指標引用,或者被帶有copy修飾的屬性引用,或者作為函式返回值返回時。
Flags : 這是一個位移列舉的變數,標記著block的一些屬性,比如
- 結構體的
Desc
中有無copy
個dispose
函式 (1 << 25) - 結構體的
Desc
中有無signatureStr
type encodings (char * 型別字串) (1<<30) - ......
FuncPtr : 就是你定義block的內部邏輯實現函式的指標。通過編譯器把OC的程式碼處理成c語言的函式後在block初始化時,用這個變數記錄函式的指標地址,當block被呼叫時就是執行這個函式指標指向的函式。
Desc. Block_size : block的記憶體佔用空間的大小
Desc -> copy
+ dispose
函式 :block用於管理自身記憶體的函式
......
更詳細的 block底層原始碼實現 以及 __block變數的原理 推薦閱讀 深入研究Block捕獲外部變數和__block實現原理 這篇文章
清楚了Block的內部結構後,我們來看下如何理由 NSInvocation進行調 用。
NSInvocation
是一個OC中用來封裝訊息傳送的類,在Runtime
的訊息轉發的最後一個轉發步驟(Normal Forwarding)也有出現NSInvocation
。Normal Forwarding
首先呼叫- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
這個方法,向呼叫者返回一個selector
對應的方法簽名類NSMethodSignature
物件,如果沒有返回NSMethodSignature
這個類物件的話 就會丟擲找不到方法的錯誤,否則,就會利用返回的NSMethodSignature
物件 生成一個NSInvocation
物件傳進- (void)forwardInvocation:(NSInvocation *)anInvocation
方法中完成訊息轉發機制的最後一步。
首先根據上面分析的Block內部定義一個結構體 ,方便我們對block進行內部訪問。
struct BlockLayout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src); // (1<<25)
void (*dispose)(void *src);
const char *signature; // (1<<30)
} *descriptor;
// 捕獲的變數
};
enum {
DescFlagsHasCopyDispose = (1 << 25),
DescFlagsIsGlobal = (1 << 28),
DescFlagsHasSignature = (1 << 30)
};
typedef int BlockDescFlags;
複製程式碼
然後定義一個簡單的block
void(^testBlock)(int a , int b) = ^(int a , int b){
NSLog(@"成功呼叫了 block");
NSLog(@"引數1 -> a = %d , 引數2 -> b = %d" , a , b);
};
複製程式碼
下面開始對Block內部進行訪問,獲取去signature(const char * )後生成NSInvoction並傳參呼叫。
//強轉為自定義的block結構體指標
struct BlockLayout * blockLayoutPointer = (__bridge struct BlockLayout *)testBlock;
int flags = blockLayoutPointer -> flags;
if (flags & BlockDescFlagsHasSignature) { //有signature字串
void * signaturePoint = blockLayoutPointer -> descriptor;
signaturePoint += sizeof(unsigned long int); //reserved
signaturePoint += sizeof(unsigned long int); //size
if (flags & BlockDescFlagsHasCopyDispose) {
signaturePoint += sizeof(void (*)(void *dst , void *src)); //copy
signaturePoint += sizeof(void (*)(void *src)); //dispose
}
//拿到 signature 字串內容
const char * signatureStr = (* (const char **) signaturePoint);
NSMethodSignature * blockSignature = [NSMethodSignature signatureWithObjCTypes:signatureStr];
NSInvocation * invocation = [NSInvocation invocationWithMethodSignature:blockSignature];
invocation.target = testBlock;
//將要傳緊block的引數
int param1 = 10 ;
int param2 = 20 ;
// block(type encodeings 為 @?) 對應的 NSInvocation 第一個引數為 block本身
// SEL(type encodeings 為 :) 對應的 NSInvocation 第一個引數為 selector 的 呼叫者(targat type encodeings 為 @) ,第二個引數這是 _cmd (方法本身型別為SEL)
[invocation setArgument:¶m1 atIndex:1];
[invocation setArgument:¶m2 atIndex:2];
[invocation invoke];
}
複製程式碼
看下列印 ,成功地利用NSInvocation物件呼叫了 Block。
2018-09-03 15:10:55.182181+0800 BlockWithNSInvocation[10694:872743] 成功呼叫了 block
2018-09-03 15:10:55.182430+0800 BlockWithNSInvocation[10694:872743] 引數1 -> a = 10 , 引數2 -> b = 20
Program ended with exit code: 0
複製程式碼
關於型別強轉
型別強轉其實並沒有改變目標變數的實際記憶體的資料,型別強轉其實就是告訴編譯器 目標變數 是我強轉的型別資料,你對這個變數訪問時按照我指定的變數型別來訪問即可。