iOS-底層原理分析之Block本質
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(); } } ```
畫個圖來表示各個結構體之間的關係就是:
Block的底層資料結構也可以用一張圖表示:
探索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捕獲,是直接訪問全域性變數。
進而也可以得出如下結論:
__block修飾變數
如果想修改區域性變數的值,可以通過__block修飾實現。
oc
void blockTest()
{
__block int age = 20;
void (^block)(void) = ^{
age = 26;
NSLog(@"Hello==%d", age);
};
block();
}
通過Clang指令生成C++程式碼:
__blockTest_block_impl_0多出來一個成員變數__Block_byref_age_0 *age,我們看到經過__block修飾的變數型別變成了結構體__Block_byref_age_0,block捕獲的是__Block_byref_age_0型別指標。呼叫函式的時候先通過__forwarding找到age指標,然後去取出age值。
並且可以看到這次的C++程式碼多出了兩個函式:
__blockTest_block_copy_0
中呼叫的是_Block_object_assign
,__blockTest_block_dispose_0
中呼叫的是_Block_object_dispose
並且這個兩個函式都有個引數8,看註釋說是這個列舉值BLOCK_FIELD_IS_BYREF,在Block_private.h 中可以檢視到:
這些列舉值表示的含義分別為:
- 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;
__block
和Block
類似,如果在棧區,會重新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指向的物件有三種類型:
那在ARC環境下,哪些情況下編譯器會自動把棧區Block拷貝到堆上
當把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++程式碼
可以看到捕獲的物件被強引用了,進行了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的迴圈引用
我們知道產生迴圈引用的條件是相互持有,就像下面這個圖畫的一樣
要想解決這個問題,就是打破這個迴圈,通過__weak修飾物件
__unsafe_unretained也可以解決迴圈引用,不安全,指向的物件銷燬時,指標儲存的地址值不變
__block修飾物件,不用的時候把物件置為null,也一樣可以打破迴圈
以上就是最近我整理的關於Block的知識點,文章中如有紕漏,希望大家指正。
- Flutter:仿京東專案實戰(4)-購物車頁面功能實現
- Flutter整合原生遇到的問題彙總
- Flutter:仿京東專案實戰(3)-商品詳情頁功能實現
- Flutter-Dart中的非同步和多執行緒講解
- iOS-底層原理分析之Block本質
- Flutter-官方推薦的Flutter與原生互動外掛Pigeon
- Flutter-flutter_sound錄音與播放
- iOS-CocoaPods的原理及Podfile.lock問題
- iOS配置多環境的三種方案
- iOS-各種Crash防護
- iOS-Swift中常見的幾種閉包
- Flutter:仿京東專案實戰(2)-分類和商品列表頁面功能實現
- Flutter:仿京東專案實戰(1)-首頁功能實現
- Flutter-JSON轉Model的四種便捷方案
- Flutter-導航與路由堆疊詳解
- Flutter 與原生通訊的三種方式
- iOS-記憶體洩漏檢測
- Fastlane實現自動打包
- 懶人必備神器-Xcode程式碼塊
- Jenkins實現自動化打包