Block底層解密
Block底層解密
block想必做過一段iOS開發的同學都用過吧,但是大部分人都是僅僅會用,不怎麼理解他是怎麼實現的,今天就讓我們來一步一步的分析一下底層是怎麼實現的吧。
檢視原始碼
void (^block)(void) =^(){ NSLog(@"this is a block!"); };
這樣一個簡單的 block
塊大家都應該知道吧,但是這個 block
塊是怎麼實現的呢?
想要了解OC物件主要是基於C/C++的什麼資料結構實現的,我們首先要做的就是將Object-C程式碼轉化為C/C++程式碼,這樣我們才能清楚的看清是怎麼實現的
然後我們開啟終端,在命令列找到cd到檔案目錄,然後中輸入:
xcrun-sdkiphoneosclang-archarm64-rewrite-objc main.m
執行結束以後,會生成 main.cpp
檔案,我們開啟 main.cpp
檔案,拉到最下邊就是我們的 main
函式實現的。
我們得到c++程式碼的block實現
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
我們知道 (void *)
這種型別的都是型別的強制轉換,為了更好的識別我們的這個Block程式碼,我們把型別轉化去掉
void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
我們在分別查詢 __main_block_impl_0
, __main_block_func_0
, __main_block_desc_0_DATA
代表什麼意思
__main_block_impl_0
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; // 建構函式(類似於OC的init方法),返回結構體物件 __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_impl
裡面是什麼
struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; };
__main_block_func_0
// 封裝了block執行邏輯的函式 static void __main_block_func_0(struct __main_block_impl_0 *__cself) { NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_c60393_mi_0); }
__main_block_desc_0_DATA
static struct __main_block_desc_0 { size_t reserved; size_t Block_size;//記憶體大小描述 } __main_block_desc_0_DATA
所以我們可以總結
- 1、
__main_block_impl_0
中__block_impl
存放的是一些變數資訊,其中存在isa
,所以可以判斷block的本質其實就是OC物件 - 2、初始化
__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方法
void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
對應上面的初始化我們可以看出第一個引數傳遞的是 執行方法
,第二個引數為 描述資訊
Block底層結構圖

Block1.png
成員變數的捕獲
為了保證block內部能夠正常的訪問外部變數,block有個變數捕獲機制,這裡我們先說結果,然後在進行證明

Block2.png
我們在 main
函式寫下這些程式碼,然後在把 main
函式生成c++程式碼
#import <Foundation/Foundation.h> int height = 180; int main(int argc, const char * argv[]) { @autoreleasepool { int age = 10; static int weight = 65; void (^block)(void) =^(){ NSLog(@"age---------%d",age); NSLog(@"weight---------%d",weight); NSLog(@"height---------%d",height); }; block(); } return 0; }
我們直接找到c++程式碼裡面存放變數的結構體 __main_block_impl_0
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int age; int *weight; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_weight, int flags=0) : age(_age), weight(_weight) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
我們可以看到變數捕獲為 age
, *weight
,但是沒有捕獲到全域性變數 height
。為了方便的理解,我們先來了解一些記憶體空間的分配。
- 1、棧區(stack) 由編譯器自動分配並釋放,存放函式的引數值,區域性變數等。棧空間分靜態分配 和動態分配兩種。靜態分配是編譯器完成的,比如自動變數(auto)的分配。動態分配由alloca函式完成。
- 2、堆區(heap) 由程式員分配和釋放,如果程式設計師不釋放,程式結束時,可能會由作業系統回收 ,比如在ios 中 alloc 都是存放在堆中。
- 3、全域性區(靜態區) (static) 全域性變數和靜態變數的儲存是放在一起的,初始化的全域性變數和靜態變數存放在一塊區域,未初始化的全域性變數和靜態變數在相鄰的另一塊區域,程式結束後有系統釋放。
- 4、程式程式碼區 存放函式的二進位制程式碼
總結:
- 1、因為自動變數(auto)分配的記憶體空間在
棧區(stack)
,編譯器會自動幫我們釋放,如果我們把block寫在另外一個方法中呼叫,自動變數age
就會被釋放,block在使用的時候就已經被釋放了,所以需要重新copy一下 - 2、靜態變數在程式結束後有系統釋放,所以不需要擔心被釋放,block只需要知道他的記憶體地址就行
- 3、對於全域性變數,任何時候都可以直接訪問,所以根本就不需要捕獲
Block型別
block有3種類型,可以通過呼叫class方法或者isa指標檢視具體的型別,但是最終都是繼承者NSBlock型別
- 1、 NSGlobalBlock ,沒有訪問auto變數
- 2、 NSStackBlock ,訪問了auto變數
- 3、 NSMallocBlock , NSStackBlock 呼叫了copy方法
她們的記憶體分配

Block3.png
每一種型別的Block呼叫copy後的結果
- 1、 NSStackBlock 原來在棧區,copy以後從棧複製到堆
- 2、 NSGlobalBlock 原來在程式的資料段,copy以後什麼也不做
- 3、 NSMallocBlock 原來在堆區,複製以後引用計數加1
我們來寫一小段程式碼證明一下
void (^block1)(void) =^(){ NSLog(@"block1"); }; int age = 10; void (^block2)(void) =^(){ NSLog(@"block2"); NSLog(@"age---------%d",age); }; void (^block3)(void) = [ ^(){ NSLog(@"block3"); NSLog(@"age---------%d",age); } copy]; NSLog(@"block1:%@---->block2:%@----->block3:%@",[block1 class],[block2 class],[block3 class]);
列印結果為

Block4.png
為什麼 block2
列印型別為 __NSMallocBlock__
,而不是 __NSStackBlock__
,因為ARC環境導致了,ARC會自動幫我們copy了一下 __NSStackBlock__
auto變數修飾符__weak
在開始之前,我先說一下結論,然後我們在去印證
- 1、當Block內部訪問了auto變數時,如果block是在棧上,將不會對auto變數產生強引用
- 2、如果block被拷貝到堆上,會根據auto變數的修飾符(__strong,__weak,__unsafe_unretained),對auto變數進行強引用或者弱引用
- 3、如果block從堆上移除的時候,會呼叫block內部的dispose函式,該函式自動釋放auto變數
- 4、在多個block相互巢狀的時候,auto屬性的釋放取決於最後的那個強引用什麼時候釋放
下面我們把ARC環境變成MRC環境,同時稍微修改一下程式碼,我們在看看 dealloc
什麼時候列印
選擇專案 Target -> Build Sttings -> All -> 搜尋‘automatic’ -> 把 Objective-C Automatic Reference Counting 設定為 NO
我們寫一個 Person
類,在MRC環境,重寫 dealloc
方法
- (void)dealloc{ [super dealloc]; NSLog(@"person--->dealloc"); }
我們在 main
函式裡面寫下這個方法
{ Person *p = [[Person alloc] init]; [p release]; } NSLog(@"--------");
我們肯定都知道列印結果吧:先列印 person--->dealloc
,然後列印 --------
如果我們新增一個Block呢,
typedef void (^Block)(void); Block block; { Person *p = [[Person alloc] init]; block = ^{ NSLog(@"%@",p); }; [p release]; } block(); NSLog(@"--------");
列印結果為

Block5.png
在ARC環境下,程式碼稍微的改變一下
Block block; { Person *p = [[Person alloc] init]; block = ^{ NSLog(@"%@",p); }; } block(); NSLog(@"--------");
列印結果為

Block6.png
注意列印順序:
- MRC環境下,是先列印
dealloc
,然後在列印p
的 - ARC環境下,是先列印
p
,然後在列印dealloc
的
當Block內部訪問了auto變數時,如果block是在棧上,將不會對auto變數產生強引用,因為當Block在棧上的時候,他自己都不能保證自己什麼時候被釋放,所以block也就不會對自動變數進行強引用了
在ARC環境下如果我們對自動變數進行一些修飾符,那麼block對auto變數是進行怎麼引用呢
我們還是老方法,把main檔案轉化為c++檔案,我們找到 __main_block_func_0
執行函式,
- 當不用修飾符修飾的時:
Person *p = __cself->p; // bound by copy
- 當使用
__strong
修飾時:Person *strongP = __cself->strongP; // bound by copy
- 當使用
__weak
修飾的時:Person *__weak weakP = __cself->weakP; // bound by copy
我們執行xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
出錯了,我們需要支援ARC,指定執行時系統版本,xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
Block會自動copy自動變數的修飾屬性
__Block修飾
我們都知道想要修改Block外邊的變數,我們都會用 __Block
來修飾自動變數,但是為什麼使用 __Block
修飾就可以在Block內部來更改自動變量了呢。
我們先寫一小段程式碼
__block int age = 10; NSLog(@"block前age地址1:%p",&age); Block block = ^{ age = 20; NSLog(@"block內%d-->age地址2:%p",age,&age); }; block(); NSLog(@"block後%d-->age地址3:%p",age,&age);
列印結果為

Block7.png
根據記憶體地址變化可見,__block所起到的作用就是隻要觀察到該變數被 block 所持有,就將“外部變數”在棧中的記憶體地址放到了堆中。
我們把 main
函式轉化為C++程式碼,然後在age使用 __Block
前後,對Block結構體進行分析

Block8.png
在 __Block
所起到的作用就是隻要觀察到該變數被 block 所持有之後, age
其實變成了OC物件,裡面含有 isa
指標
__Block的記憶體管理原則
__Block

Block9.png

Block10.png

Block11.png
我們先看到了結果,這裡我們在來分析一下原始碼
__block int age = 10;
轉化為C++程式碼,會變成這樣
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10}; //為了便於觀察,我們可以將強制轉化去掉 __Block_byref_age_0 age = { 0, &age, 0, sizeof(__Block_byref_age_0), 10};
唯一我們不太清除的就是 __Block_byref_age_0
了,我們查詢一下發現
typedef void (*Block)(void); struct __Block_byref_age_0 { void *__isa; __Block_byref_age_0 *__forwarding; int __flags; int __size; int age; };
然後我們在來查詢Block實現程式碼
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { __Block_byref_age_0 *age = __cself->age; // bound by ref (age->__forwarding->age) = 20; NSLog((NSString *)&__NSConstantStringImpl__var_folders_nb_9qtf99yd2qlbx2m97hdjf2yr0000gn_T_main_1757f5_mi_0,(age->__forwarding->age)); }
我們來檢視一下age是怎麼變成20的 (age->__forwarding->age) = 20;
,先是找到 __forwarding
結構體,然後在找到結構提裡面的 age
總結
- 1、在ARC環境下,Block被引用的時候,會被Copy一次,由棧區copy到了堆
- 2、在Block被copy的時候,Block內部被引用的
變數
也同樣被copy一份到了堆上面 - 3、被__Block修飾的變數,在被Block引用的時候,會變成結構體也就是OC物件,裡面的
__forwarding
也會由棧copy道對上面 - 4、棧上__block變數結構體中
__forwarding
的指標指向堆上面__block變數結構體,堆上__block變數結構體中__forwarding
指標指向自己 - 5、當block從堆中移除時,會呼叫block內部的dispose函式,dispose函式內部會呼叫_Block_object_dispose函式,_Block_object_dispose函式會自動釋放引用的__block變數(release)
解決迴圈引用
我們看了那麼長時間的原始碼了,一定還記得在auto變數為OC物件的時候,在沒有修飾符修飾的時候Block內部會強引用OC物件,而物件如果也持有Block的時候就會造成相互引用,也就是迴圈引用的問題。

Block12.png
我們也只能在Block持有OC物件的時候,給OC物件新增弱引用修飾符才比較合適,有兩個弱引用修飾符 __weak
和 __unsafe_unretained
- 1、 __weak:不會產生強引用,指向的物件銷燬時,會自動讓指標置為nil
- 2、__unsafe_unretained:不會產生強引用,不安全,指向的物件銷燬時,指標儲存的地址值不變
其實還有一種解決方法,那就是使用 __Block
,需要在Block內部吧OC物件設定為nil

Block13.png
__block id weakSelf = self; self.block = ^{ weakSelf = nil; } self.block();
使用 __Block
解決必須呼叫Block