浅谈OC中Block的本质
生活随笔
收集整理的這篇文章主要介紹了
浅谈OC中Block的本质
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
Block簡介
- block是將函數及其執行上下文封裝起來的一個對象
- 在block實現的內部,有很多變量,因為block也是一個對象
- 其中包含了諸如isa指針,imp指針等對象變量,還有儲存其截獲變量的對象等
- 原文博客地址: 淺談OC中Block的本質
定義和使用
block根據有無參數和有無返回值有以下幾種簡單使用方式
// 無參數無返回值 void (^ BlockOne)(void) = ^(void){NSLog(@"無參數,無返回值"); }; BlockOne();//block的調用// 有參數無返回值 void (^BlockTwo)(int a) = ^(int a){NSLog(@"有參數,無返回值, 參數 = %d,",a); }; BlockTwo(100);// 有參數有返回值 int (^BlockThree)(int,int) = ^(int a,int b){ NSLog(@"有參數,有返回值");return a + b; }; BlockThree(1, 5);// 無參數有返回值 int(^BlockFour)(void) = ^{NSLog(@"無參數,有返回值");return 100; }; BlockFour();可是以上四種block底層又是如何實現的呢? 其本質到底如何? 接下來我們一起探討一下
Block的本質
- 為了方便我們這里新建一個Command Line Tool項目, 在main函數中執行上述中一個block
- 探索Block的本質, 就要查看其源碼, 這里我們使用下面命令把main.m文件生成與其對應的c++代碼文件
- 在main.m文件所在的目錄下, 執行如下命令, 會生成一個main.cpp文件
- 把main.cpp文件添加到項目中, 并使其不參與項目的編譯, 下面我們就具體看一下block的底層到底是如何實現的
打開main.cpp文件, 找到文件最底部, 可以看到block的相關源碼如下
// block的結構體 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;} };// 封裝了block執行邏輯的函數 static void __main_block_func_0(struct __main_block_impl_0 *__cself) {NSLog((NSString *)&__NSConstantStringImpl__var_folders_ty_804897ld2zg4pfcgx2p4wqh80000gn_T_main_11c959_mi_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)}; int main(int argc, const char * argv[]) {/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; // 定義block變量void (* BlockOne)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));// 執行block內部的源碼((void (*)(__block_impl *))((__block_impl *)BlockOne)->FuncPtr)((__block_impl *)BlockOne);}return 0; } static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };其中block的聲明和調用的對應關系如下
刪除其中的強制轉換的相關代碼后
// 定義block變量 void (* BlockOne)(void) = &__main_block_impl_0((void *)__main_block_func_0,&__main_block_desc_0_DATA);// 執行block內部的源碼 BlockOne->FuncPtr(BlockOne);上述代碼中__main_block_impl_0函數接受兩個參數, 并有一個返回值, 最后把函數的地址返回給BlockOne, 下面找到__main_block_impl_0的定義
// 結構體 struct __main_block_impl_0 {struct __block_impl impl;struct __main_block_desc_0* Desc;// c++中的構造函數, 類似于OC中的init方法// flags: 默認參數, 調用時可不傳__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;} };- __main_block_impl_0函數中的第一個參數__main_block_func_0賦值給了fp, fp又賦值給了impl.FuncPtr, 也就意味著impl.FuncPtr中存儲的就是我們要執行的__main_block_func_0函數的地址
- Block結構體中的isa指向了_NSConcreteStackBlock, 說明Block是一個_NSConcreteStackBlock類型, 具體后面會詳解
- __main_block_impl_0函數中的第二個參數__main_block_desc_0_DATA
- 其中reserved賦值為0
- Block_size被賦值為sizeof(struct __main_block_impl_0), 即為__main_block_impl_0這個結構體占用內存的大小
- __main_block_impl_0的第二個參數, 接受的即為__main_block_desc_0結構體的變量(__main_block_desc_0_DATA)的地址
Block變量捕獲
- 局部變量分為兩大類: auto和static
- auto: 自動變量, 離開作用域就會自動銷毀, 默認情況下定義的局部變量都是auto修飾的變量, 系統都會默認給添加一個auto
- auto不能修飾全局變量, 會報錯
- static作用域內修飾局部變量, 可以修飾全局變量
- 全局變量
局部變量
auto變量捕獲
auto局部變量在Block中是值傳遞
下述代碼輸出值為多少?
int age = 10;void (^BlockTwo)(void) = ^(void){NSLog(@"age = %d,",age); };age = 13; BlockTwo(); // 輸出10輸出值為什么是10而不是13呢? 我們還是生成main.cpp代碼看一下吧, 相關核心代碼如下
struct __main_block_impl_0 {struct __block_impl impl;struct __main_block_desc_0* Desc;// 這里多了一個age屬性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;} }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) {int age = __cself->age; // bound by copyNSLog((NSString *)&__NSConstantStringImpl__var_folders_ty_804897ld2zg4pfcgx2p4wqh80000gn_T_main_80d62b_mi_0,age);}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 argc, const char * argv[]) {/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; // 定義屬性int age = 10;// block的定義void (*BlockTwo)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));// 改變屬性值age = 13;// 調用block((void (*)(__block_impl *))((__block_impl *)BlockTwo)->FuncPtr)((__block_impl *)BlockTwo);}return 0; } static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };那么下面我們一步步看一下, 吧一些強制轉換的代碼去掉之后
int age = 10;void (*BlockTwo)(void) = &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA,age);age = 13; BlockTwo->FuncPtr(BlockTwo);在上面的__main_block_impl_0函數里面相比于之前的, 多了一個age參數
struct __main_block_impl_0 {struct __block_impl impl;struct __main_block_desc_0* Desc;// 新的屬性ageint age;// 構造函數, 多了_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中, 多了一個_age參數
- 同時后面多了一條age(_age)語句, 在c++中, age(_age)相當于age = _age, 即給age屬性賦值, 存儲構造函數傳過來的age屬性的值
- 所以在后面調用block的時候, block對應的結構體所存儲的age屬性的值仍然是10, 并沒有被更新
static變量捕獲
static局部變量在Block中是指針傳遞, 看一下下面代碼的輸出情況
auto int age = 10; static int weight = 20;void (^BlockTwo)(void) = ^(void){NSLog(@"age = %d, weight = %d,",age, weight); };age = 13; weight = 23; BlockTwo();- 上面代碼輸出結果: age = 10, weight = 23
- 重新賦值后age的結果不變, 之前已經說過了
- 可是weight的結果卻是賦值后的結果, 至于為什么, 請繼續向下看吧…
- 我們還是生成main.cpp代碼看一下吧, 相關核心代碼如下
- 從上面代碼可以看到__main_block_impl_0類中多了兩個成員變量age和weight, 說明兩個變量我們都可以捕獲到
- 不同的是, 同樣都是int變量, 使用不同的修飾詞修飾, __main_block_impl_0類中也是不同的
- static修飾的變量weight在block中存儲的是weight的地址, 在后面的block函數中我們使用的也是其地址
- 也就是說上面的構造函數中
- age保存的是一個準確的值
- weight保存的是weight所在的內存地址
- 所以在最后調用block內部邏輯的時候
- 也就是說, 同樣是局部變量
- auto修飾的變量在block中存儲的是變量的值(值傳遞)
- static修飾的變量在block中存儲的是變量的內存地址(地址傳遞)
全局變量
int age = 10; static int weight = 20;int main(int argc, const char * argv[]) {@autoreleasepool {void (^BlockTwo)(void) = ^(void){NSLog(@"age = %d, weight = %d,",age, weight);};age = 13;weight = 23;BlockTwo();}return 0; }上面代碼的輸出結果, 毫無疑問是13和23, 相關c++代碼如下
int age = 10; static int weight = 20;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) {// 封裝了block執行邏輯的函數NSLog((NSString *)&__NSConstantStringImpl__var_folders_ty_804897ld2zg4pfcgx2p4wqh80000gn_T_main_0ee0bb_mi_0,age, weight);}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 argc, const char * argv[]) {/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; // 定義block變量void (*BlockTwo)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));age = 13;weight = 23;((void (*)(__block_impl *))((__block_impl *)BlockTwo)->FuncPtr)((__block_impl *)BlockTwo);}return 0; } static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };- 從上面代碼可以看出__main_block_impl_0結構體重并沒有捕獲到age和weight的成員變量
- 同樣在定義block變量的時候中也不需要傳入age和weight的變量
- 在封裝了block執行邏輯的函數中, 就可以直接使用全局的變量即可
Block的類型
Block的三種類型
- 在之前的C++源碼中, __main_block_impl_0結構體中isa指向的類型是_NSConcreteStackBlock
- 下面就具體看一下, Block的只要類型有那些
- 先看一下下面這部分代碼的輸出結果
- block的類型NSBlock最終也是繼承自NSObject
- 這也可以解釋為什么block的結構體__main_block_impl_0中會有一個isa指針了
- 此外, block共有三種類型, 可以通過調用class方法或者isa指針查看具體類型, 最終都是繼承自NSBlock類型
- __NSGlobalBlock__或者_NSConcreteGlobalBlock
- __NSStackBlock__或者_NSConcreteStackBlock
- __NSMallocBlock__或者_NSConcreteMallocBlock
block在內存中的分配
- _NSConcreteGlobalBlock: 在數據區域
- _NSConcreteStackBlock: 在棧區域
- _NSConcreteMallocBlock: 在堆區域
- 應用程序的內存分配圖如上圖所示, 自上而下依次為內存的低地址–>內存的高地址
- 程序區域: 代碼段, 用于存放代碼
- 數據區域: 數據段, 用于存放全局變量
- 堆: 動態分配內存,需要程序員自己申請,程序員自己管理, 通常是alloc或者malloc方式申請的內存
- 棧: 用于存放局部變量, 系統會自動分配內存, 自動銷毀內存
區分不同的block類型
- 上面提到, 一共有三種block類型, 且不同的block類型存放在內存的不同位置
- 但是如何區分所定義的block
到底是哪一種類型呢
看看下面代碼的執行情況, 運行環境實在MRC環境下
針對各種不同的block總結如下
| __NSGlobalBlock__ | 沒有訪問auto變量 |
| __NSStackBlock__ | 訪問了auto變量 |
| __NSMallocBlock__ | __NSStackBlock__調用了copy |
- 由于__NSMallocBlock__是放在堆區域
- 要想創建出__NSMallocBlock__類型的block, 我們可以調用copy方法
- 從上面的代碼中我們可以明顯看到, __NSStackBlock__類型的block調用copy方法后, 就會變成__NSMallocBlock__類型的block
- 相當于生成的block是在堆區域的
- 那么另外兩種類型調用copy方法后,又會如何? 下面一起來看一下吧
- 從上面的代碼可以看到, 只有__NSStackBlock__類型的block調用copy之后才會變成__NSMallocBlock__類型, 其他的都是原類型
- 主要也是__NSStackBlock__類型的作用域是在棧中, 作用域中的局部變量會在函數結束時自動銷毀
- __NSStackBlock__調用copy操作后,分配的內存地址相當于從棧復制到堆;副本存儲位置是堆
- 其他的則可參考下面表格
| __NSStackBlock__ | 棧 | 從棧復制到堆 |
| __NSGlobalBlock__ | 程序的數據區域 | 什么也不做 |
| __NSMallocBlock__ | 堆 | 引用計數增加 |
- 在ARC環境下, 編譯器會根據情況自動將站上的block復制到堆上, 類似以下情況
- block作為函數返回值時
- 將block賦值給__strong修飾的指針時
- block作為GCD的方法參數時
__block修飾符
Question: 定義一個auto修飾的局部變量, 并在block中修改該變量的值, 能否修改成功呢?
auto int width = 10; static int height = 20; void (^block)(void) = ^(void){// 事實證明, 在Xcode中這行代碼是報錯的width = 22;// 但是static修飾的變量, 卻是可以賦值, 不會報錯height = 22;NSLog(@"width = %d, height = %d", width, height); };block();// width = 10, height = 22- 在之前提到, 在block中, auto修飾的變量是值傳遞
- static修飾的變量是指針傳遞, 所以在上述代碼中, block存儲的只是height的內存地址
- 同樣auto變量實在main函數中定義的, 而block的執行邏輯是在__main_block_func_0結構體的方法中執行的, 相當于局部變量不能跨函數訪問
- 至于static修飾的變量為什么可以修改?
- 在__main_block_impl_0結構體中height存儲的是其內存地址, 在其他函數或者結構體中訪問和改變height的方式都是通過其真真訪問的
- 類似賦值方式: (*height) = 22;
- 取值方式: (*height)
__block修飾auto變量
__block auto int width = 10;void (^block)(void) = ^(void) {// 很明顯, 這里就可以修改了width = 12;NSLog(@"width = %d", width); };block(); // width = 12為什么上面的代碼就可以修改變量了呢, 這是為什么呢…請看源碼
下面是生成的block的結構體
struct __main_block_impl_0 {struct __block_impl impl;struct __main_block_desc_0* Desc;// 這里的width被包裝成了一個__Block_byref_width_0對象__Block_byref_width_0 *width; // by ref// 這里可以對比一下之前的未被__block修飾的int變量// int width;__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_width_0 *_width, int flags=0) : width(_width->__forwarding) {impl.isa = &_NSConcreteStackBlock;impl.Flags = flags;impl.FuncPtr = fp;Desc = desc;} };- 上述代碼看到__block可以用于解決block內部無法修改auto修飾的變量值得問題
- 但是__block不能修飾全局變量和static修飾的靜態變量(同樣也不需要, 因為在block內部可以直接修改)
- 經過__block修飾的變量會被包裝成一個對象(__Block_byref_width_0)
- 下面是width被包裝后的對象的結構體, 在結構體內, 會有一個width成員變量
下面我們先看一下, auto和block的定義和調用
int main(int argc, const char * argv[]) {/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; // __block auto int width = 10;auto __Block_byref_width_0 width = {0,&width,0,sizeof(__Block_byref_width_0),10};void (*block)(void) = &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA,&width,570425344);block->FuncPtr(block);}return 0; }- 可以看到在定義的__Block_byref_width_0類型的width中的每一個參數分別賦值給了__Block_byref_width_0結構體中的每一個成員變量
- 而在block內部重新對width重新賦值的邏輯中
- 上面代碼中的width是一個__Block_byref_width_0類型的變量
- width對象通過找到內部的__forwarding成員變量
- 在__Block_byref_width_0結構體中__forwarding是一個指向自己本身的成員變量
- 所以最后再通過__forwarding找到__Block_byref_width_0的成員變量width, 在進行重新賦值
- 在NSLog中也是通過這種邏輯獲取width的值
總結
以上是生活随笔為你收集整理的浅谈OC中Block的本质的全部內容,希望文章能夠幫你解決所遇到的問題。