iOS-底層原理分析之Block本質

語言: CN / TW / HK

highlight: a11y-dark

前言

寫本篇文章的目的就是要搞明白以下幾個問題?

  • block的底層實現原理
  • block的幾種型別?
  • __block的作用是什麼?有什麼使用注意點?
  • block的迴圈引用問題解決?

什麼是Block

Block是將函式及其上下文封裝起來的物件。

Block的底層實現原理

Block的底層實現是一個結構體

```oc void blockTest() {     void (^block)(void) = ^{         NSLog(@"Hello");     };

block(); }

int main(int argc, char * argv[]) {     @autoreleasepool {         blockTest();     } } ```

可以通過clang命令檢視編譯器是如何實現Block的,進入main.m的目錄,在終端輸入xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m,然後會在當前目錄生成main.cpp的C++檔案,程式碼如下:

```oc ///Block的C++實現 struct __blockTest_block_impl_0 {   struct __block_impl impl;   struct __blockTest_block_desc_0 Desc;     ///建構函式   __blockTest_block_impl_0(void fp, struct __blockTest_block_desc_0 *desc, int flags=0) {     impl.isa = &_NSConcreteStackBlock;     impl.Flags = flags;     impl.FuncPtr = fp;     Desc = desc;   } };

struct __block_impl {   ///isa指標,指向一個類物件,有三種類型:_NSConcreteStackBlock、_NSConcreteGlobalBlock和_NSConcreteMallocBlock   void isa;     ///block的負載資訊(引用計數和型別資訊),按位儲存   int Flags;     ///保留變數   int Reserved;     ///指向Block執行時呼叫的函式,也就是Block需要執行的程式碼塊   void FuncPtr; };

///Block 執行時呼叫的函式 static void __blockTest_block_func_0(struct __blockTest_block_impl_0 __cself) {         NSLog((NSString )&__NSConstantStringImpl__var_folders_59_c8zx7n553c34b8d791v8l7ww0000gn_T_main_ab6df5_mi_0);     }

static struct __blockTest_block_desc_0 {     ///Block版本升級所需要的預留區空間   size_t reserved;     ///Block的大小=sizeof(struct __blockTest_block_impl_0)   size_t Block_size; } __blockTest_block_desc_0_DATA = { 0, sizeof(struct __blockTest_block_impl_0)};

void blockTest() {     ///block變成了一個指標,指向一個通過__blockTest_block_impl_0建構函式例項化的結構體例項     ///__blockTest_block_func_0表示Block塊的函式指標     ///__blockTest_block_desc_0_DATA作為靜態全域性變數初始化__main_block_desc_0的結構體例項指標     void(block)(void) = ((void ()())&__blockTest_block_impl_0((void )__blockTest_block_func_0, &__blockTest_block_desc_0_DATA));     ///呼叫Block     ((void ()(__block_impl ))((__block_impl )block)->FuncPtr)((__block_impl *)block); }

int main(int argc, char * argv[]) {     / @autoreleasepool / { __AtAutoreleasePool __autoreleasepool;          blockTest();     } } ```

畫個圖來表示各個結構體之間的關係就是:

截圖2022-06-12 14.30.59.png

Block的底層資料結構也可以用一張圖表示:

截圖2022-06-26 14.39.40.png

探索Block的變數捕獲

Block根據其型別可以分為三類:全域性區Block、棧區Block、堆區Block。然而通過上面Block的C++底層實現,可以看到__block_impl有一個屬性isa,而這個isa指向的物件有三種類型,也就是這三種Block。通過這個也可以看出,Block的也是一個物件。

截獲auto變數值

oc void blockTest() {     int age = 20;     void (^block)(void) = ^{         NSLog(@"Hello==%d", age);     };     block(); } 通過Clang指令生成C++程式碼:

```oc struct __blockTest_block_impl_0 {   struct __block_impl impl;   struct __blockTest_block_desc_0* Desc;   int age;

__blockTest_block_impl_0(void fp, struct __blockTest_block_desc_0 desc, int _age, int flags=0) : age(_age) {     impl.isa = &_NSConcreteStackBlock;     impl.Flags = flags;     impl.FuncPtr = fp;     Desc = desc;   } }; ``` 可以看到__blockTest_block_impl_0多了一個成員變數age,並且建構函式也多了一個引數age,傳的僅僅是a ge的值。可以得出Block捕獲的值,所以要想在Block內部修改區域性變數的值是不行的。

使用static修飾變數

把上面的age改成使用static修飾 oc void blockTest() {     static int age = 20;     void (^block)(void) = ^{         NSLog(@"Hello==%d", age);     };     block(); } 通過Clang指令生成C++程式碼:

oc struct __blockTest_block_impl_0 {   struct __block_impl impl;   struct __blockTest_block_desc_0* Desc;   int *age;   __blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int *_age, int flags=0) : age(_age) {     impl.isa = &_NSConcreteStackBlock;     impl.Flags = flags;     impl.FuncPtr = fp;     Desc = desc;   } }; 可以看出捕獲的不再是變數的值,而是變數的指標地址,所以也可以在Block內部修改age的值。並且static修飾的區域性變數叫作靜態區域性變數,是儲存在靜態儲存區,這塊記憶體只有在程式結束才會銷燬,但是隻是在宣告它的程式碼塊可見,所以傳入變數的指標也不用擔心變數銷燬的問題。

全域性變數

oc int age = 20; void blockTest() {     void (^block)(void) = ^{         NSLog(@"Hello==%d", age);     };     block(); } 通過Clang指令生成C++程式碼:

oc struct __blockTest_block_impl_0 {   struct __block_impl impl;   struct __blockTest_block_desc_0* Desc;   __blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int flags=0) {     impl.isa = &_NSConcreteStackBlock;     impl.Flags = flags;     impl.FuncPtr = fp;     Desc = desc;   } };

可以看出並沒有把全域性變數age捕獲,是直接訪問全域性變數。

進而也可以得出如下結論:

截圖2022-06-12 14.22.43.png

__block修飾變數

如果想修改區域性變數的值,可以通過__block修飾實現。

oc void blockTest() {     __block int age = 20;     void (^block)(void) = ^{         age = 26;         NSLog(@"Hello==%d", age);     };     block(); } 通過Clang指令生成C++程式碼:

截圖2022-06-12 15.31.40.png

__blockTest_block_impl_0多出來一個成員變數__Block_byref_age_0 *age,我們看到經過__block修飾的變數型別變成了結構體__Block_byref_age_0,block捕獲的是__Block_byref_age_0型別指標。呼叫函式的時候先通過__forwarding找到age指標,然後去取出age值。

並且可以看到這次的C++程式碼多出了兩個函式:

截圖2022-06-12 15.41.21.png

__blockTest_block_copy_0中呼叫的是_Block_object_assign__blockTest_block_dispose_0中呼叫的是_Block_object_dispose

截圖2022-06-12 15.42.43.png

並且這個兩個函式都有個引數8,看註釋說是這個列舉值BLOCK_FIELD_IS_BYREF,在Block_private.h 中可以檢視到:

截圖2022-06-12 15.46.43.png

這些列舉值表示的含義分別為:

  • BLOCK_FIELD_IS_OBJECT:OC物件型別
  • BLOCK_FIELD_IS_BLOCK:是一個block
  • BLOCK_FIELD_IS_BYREF:在棧上被__block修飾的變數
  • BLOCK_FIELD_IS_WEAK:被__weak修飾的變數,只在Block_byref管理內部物件記憶體時使用
  • BLOCK_BYREF_CALLER:處理Block_byref內部物件記憶體的時候會加的一個額外標記(告訴內部實現不要進行retain或者copy)

Block copy流程

```js // 拷貝 block // 如果原來就在堆上,就將引用計數加 1; // 如果原來在棧上,會拷貝到堆上,引用計數初始化為 1,並且會呼叫 copy helper 方法(如果存在的話 // 如果 block 在全域性區,不用加引用計數,也不用拷貝,直接返回 block 本身 // 引數 arg 就是 Block_layout 物件, // 返回值是拷貝後的 block 的地址 // 執行?stack -》malloc void _Block_copy(const void arg) {     struct Block_layout *aBlock;     // 如果 arg 為 NULL,直接返回 NULL     if (!arg) return NULL;

// The following would be better done as a switch statement     // 強轉為 Block_layout 型別     aBlock = (struct Block_layout )arg;     const char signature = _Block_descriptor_3(aBlock)->signature;

// 如果現在已經在堆上     if (aBlock->flags & BLOCK_NEEDS_FREE) {         // latches on high         // 就只將引用計數加 1         latching_incr_int(&aBlock->flags);         return aBlock;     }

// 如果 block 在全域性區,不用加引用計數,也不用拷貝,直接返回 block 本身     else if (aBlock->flags & BLOCK_IS_GLOBAL) {         return aBlock;     }

else {         // Its a stack block.  Make a copy.         // block 現在在棧上,現在需要將其拷貝到堆上         // 在堆上重新開闢一塊和 aBlock 相同大小的記憶體         struct Block_layout *result =

(struct Block_layout *)malloc(aBlock->descriptor->size);

// 開闢失敗,返回 NULL         if (!result) return NULL;

// 將 aBlock 記憶體上的資料全部複製新開闢的 result 上         memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first

if __has_feature(ptrauth_calls)

// Resign the invoke pointer as it uses address authentication.         result->invoke = aBlock->invoke;

endif

// reset refcount         // 將 flags 中的 BLOCK_REFCOUNT_MASK 和 BLOCK_DEALLOCATING 部分的位全部清為 0         result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed

// 將 result 標記位在堆上,需要手動釋放;並且引用計數初始化為 1         result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1

// copy 方法中會呼叫做拷貝成員變數的工作         _Block_call_copy_helper(result, aBlock);

// Set isa last so memory analysis tools see a fully-initialized object.         // isa 指向 _NSConcreteMallocBlock         result->isa = _NSConcreteMallocBlock;         return result;     } } ```

在_Block_copy原始碼中,從棧區copy到堆區的過程中,_Block_call_copy_helper(result, aBlock)的呼叫時為了複製棧區的Block裡面的成員變數,給堆區的Block。其實最終會發現呼叫的是這個函式_Block_object_assign,根據引數 flags 的型別(物件、block、byref...),做了不同的處理

  • 物件型別,增加物件的引用計數;
  • block型別,會對該block執行一次block_copy操作;
  • __block修飾,會呼叫_Block_byref_copy;

__blockBlock類似,如果在棧區,會重新malloc一份,進行深拷貝操作,但這兩個的forwarding都會指向堆區的,如果已經在堆區,只會將其引用計數+1。上面也有提到關於__block修飾變數。

Block_release 流程

js void _Block_release(const void *arg) { // 1. 將指標轉換為Block_layout結構 struct Block_layout *aBlock = (struct Block_layout *)arg; if (!aBlock) return; // 2. 如果是全域性block,那麼直接返回 if (aBlock->flags & BLOCK_IS_GLOBAL) return; // 3. 如果不是堆block,那麼直接返回 if (! (aBlock->flags & BLOCK_NEEDS_FREE)) return; // 4. 處理堆block的引用計數 if (latching_decr_int_should_deallocate(&aBlock->flags)) { // 5. 釋放block捕獲的變數 _Block_call_dispose_helper(aBlock); _Block_destructInstance(aBlock); // 6. 釋放堆block free(aBlock); } }

Block的記憶體管理

之前提到Block的型別有三種,也就是__block_impl中的isa指向的物件有三種類型:

截圖2022-06-12 15.50.27.png

那在ARC環境下,哪些情況下編譯器會自動把棧區Block拷貝到堆上

截圖2022-06-12 15.53.09.png

當把Block拷貝到堆上,會有哪些變化

oc typedef void(^Block)(void); int main(int argc, char * argv[]) {     @autoreleasepool {         NSObject *obj = [[NSObject alloc]init];         Block block = ^{             NSLog(@"%p",obj);         };         block();     } } 通過Clang指令 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m 轉成C++程式碼

截圖2022-06-12 16.04.34.png

可以看到捕獲的物件被強引用了,進行了copy操作,在copy函式內部的_Block_object_assign會根據物件修飾符strong或者weak而對其進行強引用或者弱引用。

總結:

  • 當Block內部訪問了物件型別的auto物件時,如果Block是在棧上,將不會對auto物件產生強引用。

  • 如果Block被拷貝到堆上,會呼叫Block內部的copy函式,copy函式內部會呼叫_Block_object_assign函式,_Block_object_assign會根據auto物件的修飾符(__strong,__weak,__unsafe_unretained)做出相應的操作,當使用的是__strong時,將會對auto物件的引用計數加1,當為__weak時,引用計數不變。

  • 如果Block從堆上移除,會呼叫block內部的dispose函式,內部會呼叫_Block_object_dispose函式,這個函式會自動釋放引用的auto物件。

解決Block的迴圈引用

我們知道產生迴圈引用的條件是相互持有,就像下面這個圖畫的一樣

截圖2022-06-12 16.19.28.png

要想解決這個問題,就是打破這個迴圈,通過__weak修飾物件

截圖2022-06-12 16.21.12.png

__unsafe_unretained也可以解決迴圈引用,不安全,指向的物件銷燬時,指標儲存的地址值不變

截圖2022-06-12 16.22.55.png

__block修飾物件,不用的時候把物件置為null,也一樣可以打破迴圈

截圖2022-06-12 16.28.30.png

以上就是最近我整理的關於Block的知識點,文章中如有紕漏,希望大家指正。