一、代碼——命令行模式 //main.m block(20, 30); 分析:以下代碼的前提,因為我們知道block底層的構造就是上述結構體的構造,橋接的目的就是展示這樣的結構體內部是怎樣的; 二、調試 //lldb模式 1)第一個斷點 2)第二個斷點 3)轉入彙編 4)彙編界面 分析: 1)我們發 ...
一、代碼——命令行模式
//main.m
#import <Foundation/Foundation.h> struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; struct __main_block_desc_0 { size_t reserved; size_t Block_size; }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int age; }; int main(int argc, const char * argv[]) { @autoreleasepool { // ^{ // NSLog(@"This is a block"); // }(); int age = 10; void (^block)(int, int) = ^(int a, int b){ NSLog(@"This is a block:%d", age); NSLog(@"a:%d b:%d", a, b); }; struct __main_block_impl_0 *blockStruct = (__bridge struct __main_block_impl_0 *)block;
block(20, 30);
} return 0; }
分析:以下代碼的前提,因為我們知道block底層的構造就是上述結構體的構造,橋接的目的就是展示這樣的結構體內部是怎樣的;
struct __main_block_impl_0 *blockStruct = (__bridge struct __main_block_impl_0 *)block;
二、調試
//lldb模式
1)第一個斷點
2)第二個斷點
3)轉入彙編
4)彙編界面
分析:
1)我們發現內部變數的層次感:
第一層:包含impl、Desc、age;
第二層:impl包含isa、Flags、Reserved、FuncPtr;
2)block大括弧內部的代碼的第一行的地址跟FuncPtr指針指向的地址是一樣的,那麼block大括弧內的代碼是如何存放的,跟FuncPtr指針有什麼關係?往下看;
三、將main.m文件轉成底層實現代碼(即C++代碼)
1)命令行:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
說明:警告不用管;
2)找到main.cpp文件
3)拖入文件並打開
說明:不對mian.cpp文件編譯的目的是,自己可以任意對該文件的代碼操作而不報錯;
說明:
1)上面兩張圖中,1、2、3是一一對應關係(1:為block要引用的外部變數;2:定義的block;3:調用block);
2)在2處,本人把一些強制轉換去除了(如:void (*)等),便於閱讀;
四、底層代碼分析
1)block結構體(對2的分析)
很明顯,block是一個指針,指向__main_block_impl_0,那__main_block_impl_0又是什麼呢,往下看;
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int age; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
__main_block_impl_0是個結構體,內部成員變數有__block_impl類型的結構體變數,__main_block_desc_0類型的結構體變數,外部應用變數,以及一個__main_block_impl_0方法(該方法名跟所在的結構體名稱相同,為C++的一個構造方法,類似於init方法);
<1>__block_impl
struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; };
該結構體有四個成員變數,這跟上述lldb模式下顯示的成員變數相同,而第一個成員變數為isa指針,我們知道這是oc對象(實例、類、元類)的專屬標誌,很顯然__main_block_impl_0是一個oc對象,而oc對象的本質就是在記憶體中為結構體,此處完全吻合;
<2>__main_block_desc_0
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)};
該結構體有兩個成員變數,同時定義了一個結構體變數並對其賦值,reserved賦值為0,Block_size賦值為__main_block_impl_0即block的記憶體大小;
<3>__main_block_impl_0構造函數
__main_block_impl_0在main 函數中傳了三個參數:
//__main_block_func_0參數
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) { int age = __cself->age; // bound by copy NSLog((NSString *)&__NSConstantStringImpl__var_folders_tb_zgsq5gq15rd3zvbdmw1c09y80000gn_T_main_51dde3_mi_0, age); NSLog((NSString *)&__NSConstantStringImpl__var_folders_tb_zgsq5gq15rd3zvbdmw1c09y80000gn_T_main_51dde3_mi_1, a, b); }
很顯然,__main_block_func_0函數存放的是block大括弧裡面的代碼,而該函數是直接賦值給__main_block_impl_0構造函數的第一個參數fp指針,而fp又賦值給__block_impl結構體中的FuncPtr變數,因此,回顧上述lldb模式,FuncPtr指針存放的地址跟37號斷點處轉入彙編模式顯示的首地址是一樣的,結合此處,可以肯定,FuncPtr變數指向block大括弧內的代碼,該代碼存放在記憶體中的分配好的函數中(__main_block_func_0);
//__main_block_desc_0_DATA參數
__main_block_desc_0_DATA是一個結構體,包含了block的記憶體大小,最終賦值給__main_block_desc_0結構體類型變數Desc;
//age直接賦值給_age
構造函數中的第四個參數flags預設設置為0;
2)block調用(對3的分析)
block調用代碼可以簡化成以下代碼
(__block_impl *)block->FuncPtr(block, 20, 30);
<1>參數:經上述分析,我們知道FuncPtr指向block大括弧內的代碼塊即__main_block_func_0函數,該函數共有三個參數,block本身,和兩個int類型變數,實參與形參一一對應,這點沒問題;
<2>強引用:因為block是一個結構體指針,其引用結構體變數只能通過"->"形式引用(如果是結構體變數非指針,則可以通過點(.)引用——此處是C語言語法知識,稍啰嗦了點!);
但是FuncPtr並不是block(即__main_block_impl_0)結構體的成員變數,為什麼能直接引用,而不應該是block->impl.FuncPtr嗎?
我們看到block進行了強制轉換(__block_impl *),而__block_impl結構體中是存在FuncPtr變數的,但這完全是兩個不同的結構體,也不能強制轉換引用啊?
我們可以看到,__block_impl結構體在__main_block_impl_0結構體中是第一個成員變數類型,即__main_block_impl_0的首地址其實就是__block_impl結構體的首地址,也就是說,__main_block_impl_0結構體的地址是從isa指針變數開始的,即__main_block_impl_0結構體在__block_impl結構體的大小範圍內跟__block_impl結構體是完全重合的(其實就是同一片記憶體),只不過__main_block_impl_0結構體大小要比__block_impl結構體大——因此,經過強制轉換後block完全可以直接引用FuncPtr成員變數;
五、結論
【1】block本質是一個oc對象,以結構體形式存放在記憶體中;block本身是一個指針,存放的是該結構體的記憶體地址(可以把block理解成函數名——函數名也是指針,指向函數的入口地址);
【2】block大括弧內的代碼存放在固定的函數中,該函數的入口地址存放在block結構體的成員變數指針(FuncPtr)中;
【3】block結構體分為函數調用結構體變數impl(包含isa指針變數、函數調用指針變數)、信息描述結構體指針變數Desc(包含block記憶體大小成員變數)、外部引用變數(age),以及構造函數;
如下圖所示(構造函數沒有寫出來,size即為block記憶體大小):