Block 底層原理

寫在前面

Block 是 C 語言的擴充功能。可以用一句話來表示 Block 的擴充功能:帶有自動變量(局部變量)的匿名函數(shù)。

基本概念

Block 定義

語法:^ 返回值類型 參數(shù)列表 表達式

^int(int count){
    retrun count + 1;
}

跟普通函數(shù)就兩點不同

  • 沒有函數(shù)名
  • 帶有 ^

可以省略很多部分,省略返回類型:^ 參數(shù)列表 表達式

^(int count){
    retrun count + 1;
}

當沒有參數(shù)的時候,參數(shù)列表也可以省略:^ 表達式

// 未省略參數(shù)列表
^(void){
    NSLog(@"done");
}

// 省略參數(shù)列表
^{
    NSLog(@"done");
}

Block 類型變量

先看一下 C 語言的函數(shù)指針

int func(int count) {
    retrun count + 1;
}

int (*funcptr)(int) = &func;

而 Block 類型變量為:int (^blk)(int),僅僅是把 * 改成 ^ 。

int (^blk)(int) = ^(int count){
    return count + 1;
};

Block 截獲外部變量和 __block 修飾符

int global_val = 1;
static int global_static_val = 1;

int main(int argc, const char * argv[]) {
    int val = 1;
    static int static_val = 1;
    void(^blk)(void) = ^{
        NSLog(@"global_val:%d", global_val);
        NSLog(@"global_static_val:%d", global_static_val);
        NSLog(@"val:%d", val);
        NSLog(@"static_val:%d", static_val);
    };
    
    global_val++;
    global_static_val++;
    val++;
    static_val++;
    
    blk();
    
    return 0;
}

/*
打印如下:
global_val:2
global_static_val:2
val:1
static_val:2
*/

通過上面例子知道,全局變量和局部靜態(tài)變量在 block 表達式中,可以直接修改的;

當我們需要修改自動變量值時,需要使用 __block 修飾,否則無法編譯(__block 只能修飾局部自動變量

int main()
{
    __block int val = 1;
    void(^blk)(void) = ^{
        val++;
        NSLog(@"val:%d", val);
    };
    val++;
    blk();
    
    return 0;
}

/*
打印如下:
val:3
*/

底層實現(xiàn)

通過 clang -rewrite-objc main.m 解析下面源碼

// ARC 環(huán)境
int main(int argc, const char * argv[]) {
    void(^blk)(void) = ^{
        NSLog(@"done");
    };
    blk();
    
    return 0;
}
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr; //指向 block 表達式的函數(shù)指針
};

// block 實現(xiàn)的結構體
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 表達式的實現(xiàn)部分
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_47_6fxgzrp50fv6r72895k282k40000gn_T_main_01adff_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[]) {
    // 生成 __main_block_impl_0 結構體實例 blk
    void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    
    // 通過結構體類的函數(shù)指針,調用block表達式部分
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    
    return 0;
}

這里可以得到一個結論:Block 底層是一個結構體實現(xiàn)。

再看看 clang 截獲普通外部變量的結構

// ARC 環(huán)境

int global_val = 1;
static int global_static_val = 1;

int main(int argc, const char * argv[]) {
    int val = 1;
    static int static_val = 1;
    void(^blk)(void) = ^{
        NSLog(@"global_val:%d", global_val);
        NSLog(@"global_static_val:%d", global_static_val);
        NSLog(@"val:%d", val);
        NSLog(@"static_val:%d", static_val);
    };
    blk();
    
    return 0;
}
int global_val = 1;
static int global_static_val = 2;

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int val;         // 局部自動變量,值傳遞
    int *static_val; // 局部靜態(tài)變量,指針傳遞
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int *_static_val, int flags=0) : val(_val), static_val(_static_val) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int val = __cself->val; // bound by copy
    int *static_val = __cself->static_val; // bound by copy
    
    // 全局變量直接訪問
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_47_6fxgzrp50fv6r72895k282k40000gn_T_main_09c968_mi_0, global_val);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_47_6fxgzrp50fv6r72895k282k40000gn_T_main_09c968_mi_1, global_static_val);
    
    // 局部變量,通過傳遞訪問
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_47_6fxgzrp50fv6r72895k282k40000gn_T_main_09c968_mi_2, val);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_47_6fxgzrp50fv6r72895k282k40000gn_T_main_09c968_mi_3, (*static_val));
}

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[]) {
    int val = 3;
    static int static_val = 4;
    
    void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val, &static_val));
    
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    
    return 0;
}

小結如下:

變量類型 訪問方式 是否捕獲到block內部
局部自動變量 值傳遞
局部靜態(tài)變量 指針傳遞
全局變量 直接訪問

繼續(xù)看 __block 修飾的自動變量會是怎么樣的

// ARC 環(huán)境
int main()
{
    __block int val = 1;
    void(^blk)(void) = ^{
        val++;
        NSLog(@"val:%d", val);
    };
    val++;
    blk();
    
    return 0;
}
// __block 修飾的變量 val,轉換為結構體
struct __Block_byref_val_0 {
    void *__isa;
    __Block_byref_val_0 *__forwarding;
    int __flags;
    int __size;
    int val;
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __Block_byref_val_0 *val; // by ref
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    __Block_byref_val_0 *val = __cself->val; // bound by ref
    
    (val->__forwarding->val)++;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_47_6fxgzrp50fv6r72895k282k40000gn_T_main_bfcad8_mi_0, (val->__forwarding->val));
}

// 可以理解為 retain
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}

// 可以理解為 release
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
    void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
    void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 
    0,
    sizeof(struct __main_block_impl_0), 
    __main_block_copy_0, 
    __main_block_dispose_0
};

int main()
{
    __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 1};
    void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
    (val.__forwarding->val)++;
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    
    return 0;
}

我們發(fā)現(xiàn)通過 __block 修飾的自定變量轉換為 __Block_byref_a_0 結構體,而且多了一個 __forwarding 指針。在分析 __forwarding 之前,先分析一下 _NSConcreteStackBlock,Block 結構體中都會有一個 isa 指針指向 _NSConcreteStackBlock ,了解 objc 底層就會知道,isa 一般都是指向對象的所屬類,也就是說 Blcok 也可以當做一個 objc 類理解。

在上面例子中出現(xiàn)的都是 _NSConcreteStackBlock,那么 _NSConcreteGlobalBlock_NSConcreteMallocBlock 是在什么條件生成的?

_NSConcreteGlobalBlock

void(^blk)(void) = ^{
    NSLog(@"done");
};

int main(int argc, const char * argv[]) {
    blk();
    return 0;
}

clang 之后

struct __blk_block_impl_0 {
    struct __block_impl impl;
    struct __blk_block_desc_0* Desc;
    __blk_block_impl_0(void *fp, struct __blk_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteGlobalBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

還有一種情況是,只要不截獲自動變量就會分配到數(shù)據區(qū)域

int a = 1;

int main(int argc, const char * argv[]) {
    void(^blk)(void) = ^{
        NSLog(@"%d", a);
    };
    blk();
    return 0;
}

雖然通過 clang 轉換為 _NSConcreteStackBlock,實際上卻不同,可以通過斷點打印驗證;

總結起來就是:如果 Block 表達式中沒有截獲自動變量,那么 Block 的內存會分配在數(shù)據區(qū)域。

_NSConcreteStackBlock 與 _NSConcreteMallocBlock

如果不是分配在數(shù)據區(qū)域的對象,那么就是分配在棧上;至于堆上的 Block,需要對 Block 對象調用 copy 方法,才能移到堆上,ARC 下系統(tǒng)會在合適的時機自動移到堆上。

接下來分析,堆和棧的區(qū)別?以及系統(tǒng)在哪些情況下會自動移動到堆上?

  • 棧上的 Block:超過作用域就會釋放
  • 堆上的 Block:通過引用計數(shù)管理
// MRC 環(huán)境下
+ (void)test
{
    int a = 1;
    void(^blk)(void) = ^{
        NSLog(@"%d", a);
    };
    NSLog(@"blk:%@", blk);
    NSLog(@"blk copy:%@", [blk copy]);
}

/*
打印如下:

blk:__NSStackBlock__
blk copy:__NSMallocBlock__
*/

小結如下:

內存區(qū)域 條件
_NSConcreteGlobalBlock(__NSGlobalBlock__) 數(shù)據區(qū)域(.data區(qū)) 表達式中沒有截獲自動變量
_NSConcreteStackBlock(__NSStackBlock__) 棧區(qū) 表達式中截獲自動變量
_NSConcreteMallocBlock(__NSMallocBlock__) 堆區(qū) __NSStackBlock__調用了copy

系統(tǒng)在哪些情況下會自動移動到堆上?這個需要分 MRC 和 ARC 的情況來考慮。

// ARC 環(huán)境
typedef int(^block)(int);

block getBlock(int rate)
{
    return ^(int count) {
        return rate * count;
    };
}

在 ARC 下通過編譯器可轉換如下:

block getBlock(int rate)
{
    // ARC 下會默認修飾符 __strong,block tmp = XXX; 相當于 __strong block tmp = XXX;
    block tmp = &__getBlock_block_impl_0((void *)__getBlock_block_func_0, &__getBlock_block_desc_0_DATA), rate);
    
    // 相當于普通 oc 對象的 objc_retain 函數(shù);內部會調用 _Block_copy(tmp);
    tmp = objc_retainBlock(tmp); 
    
    // 把 tmp 注冊到 autoreleasepool 中
    return objc_autoreleaseReturnValue(tmp);
}

_Block_copy 函數(shù)就是把 Block 從棧上復制到堆上。到這里就可以解釋為什么 MRC 下,block 經常需要手動調用 copy,屬性修飾符也一般都要使用 copy,比如:@property (nonatomic, copy) block blk;,主要目的是把 Block 從棧上拷貝到堆上。

ARC 自動 copy 到堆上的情況

  • Block 作為函數(shù)返回值時
  • 將 Block 賦值給 __strong 指針時
  • Block 作為 Cocoa API 中方法名含有 usingBlock 的方法參數(shù)時
  • Block 作為 GCD API 的方法參數(shù)時

有了這些做基礎,接下來解釋 __block 修飾的變量,生成的結構中 __forwarding 的作用了。

先說結論:通過 __forwarding 指針訪問,保證每次都能訪問到合適的內存。

當 block 還在棧上時,__forwarding 指向的是棧上的結構體對象;

Block_img_02.jpg

當 block 復制到堆上時,堆上的 __forwarding 指向堆上的結構體,棧上的 __forwarding 指向堆上的結構體

Block_img_01.jpg

所以通過 __forwarding->val 訪問結構體中的值會永遠都是合適的。

總結:Block 本質就是一個結構體,并且可以截獲內部使用的自動變量作為結構體的成員變量。

幾個注意的地方

循環(huán)引用問題

如果在 Block 中使用附有 __strong 修飾符的自動變量,那么當 Block 從棧復制到堆時,該對象為 Block 所強引用,這樣就會引起循環(huán)引用。

先看一個經典例子

// ARC 環(huán)境
typedef void(^Block)(void);

@implementation ARCObject
{
    Block _blk;
    NSString *_str;
}

+ (void)test
{
    ARCObject *obj = [ARCObject new];
    obj->_str = @"string";
    obj->_blk = ^{
        NSLog(@"%@", obj->_str);
    };
    obj->_blk();
}

@end

clang 之后的結構體如下

struct __ARCObject__test_block_impl_0 {
    struct __block_impl impl;
    struct __ARCObject__test_block_desc_0* Desc;
    ARCObject *obj;  // 強引用
    __ARCObject__test_block_impl_0(void *fp, struct __ARCObject__test_block_desc_0 *desc, ARCObject *_obj, int flags=0) : obj(_obj) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

再看另一個經典例子,只訪問了成員變量

// ARC 環(huán)境
typedef void(^Block)(void);

@implementation ARCObject
{
    Block _blk;
    NSString *_str;
}

- (void)test
{
    _str = @"string";
    _blk = ^{
        NSLog(@"%@", _str);
    };
    _blk();
}

@end

clang 之后的結構體如下

struct __ARCObject__test_block_impl_0 {
    struct __block_impl impl;
    struct __ARCObject__test_block_desc_0* Desc;
    ARCObject *self;  // 強引用整個對象
    __ARCObject__test_block_impl_0(void *fp, struct __ARCObject__test_block_desc_0 *desc, ARCObject *_self, int flags=0) : self(_self) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

我們發(fā)現(xiàn)當 Block 訪問的是對象的成員變量時,會把對象的指針傳給 Block。

避免循環(huán)引用使用 __weak 修飾就可以了,比如上面列子,因為 __weak 是運行時添加的,所以沒法 clang 出來。

// ARC 環(huán)境
typedef void(^Block)(void);

@implementation ARCObject
{
    Block _blk;
    NSString *_str;
}

- (void)test
{
    _str = @"string";
    __weak typeof(_str) weakStr = _str;
    _blk = ^{
        NSLog(@"%@", weakStr);
    };
    _blk();
}

@end

總結

現(xiàn)在就比較好理解:Block 是一個帶有自動變量(局部變量)的匿名函數(shù)。

  1. 通過一個結構體實現(xiàn) Block,然后把 Block 表達式中使用的局部變量(局部自動變量、局部靜態(tài)變量)設置為結構體的成員變量;
  2. Block 內存管理的方式,ARC 模式下,大部分情況編譯器會自動幫我們完成從??截惖蕉焉?,然后通過引用計數(shù)管理 Block 對象。以下為編譯器自動完成拷貝的情況:
    • Block 作為函數(shù)返回值時
    • 將 Block 賦值給 __strong 指針時
    • Block 作為 Cocoa API 中方法名含有 usingBlock 的方法參數(shù)時
    • Block 作為 GCD API 的方法參數(shù)時
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容