OC 底層探索 - Block 詳解

Block 大綱.png

目錄

  • 1.Block 的基本使用
  • 2.Block 的底層數(shù)據(jù)結構
  • 3.Block 的變量捕獲機制
    3.1 auto 類型的局部變量
    3.2 static 類型的局部變量
    3.3 全局變量
    3.4 對象類型的 auto 變量
    3.5 __block 修飾的變量
    ?3.5.1 __block 作用
    ?3.5.2 __block 修飾符
    ?3.5.3 __block 的內存管理
    ?3.5.4 __block 的 __forwarding 指針
    ?3.5.5 對象類型的 auto 變量、__block 變量內存管理區(qū)別
    ?3.5.6 被 __block 修飾的對象類型
  • 4.Block 的類型
  • 5.Block 的 copy
  • 6.Block 的循環(huán)引用問題
    6.1 ARC
    6.2 MRC
  • 7.相關面試題

1.Block 的使用

Block 是什么?

塊,封裝了函數(shù)調用以及調用環(huán)境的 OC 對象,

Block 的聲明

// 1.
@property (nonatomic, copy) void(^myBlock1)(void);
// 2.BlockType:類型別名
typedef void(^BlockType)(void);
@property (nonatomic, copy) BlockType myBlock2;
// 3.
    // 返回值類型(^block變量名)(參數(shù)1類型,參數(shù)2類型,...)
    void(^block)(void);

Block 的定義

    // ^返回值類型(參數(shù)1,參數(shù)2,...){};
    // 1.無返回值,無參數(shù)
    void(^block1)(void) = ^{
        
    };
    // 2.無返回值,有參數(shù)
    void(^block2)(int) = ^(int a){
        
    };
    // 3.有返回值,無參數(shù)(不管有沒有返回值,定義的返回值類型都可以省略)
    int(^block3)(void) = ^int{
        return 3;
    };
    // 以上Block的定義也可以這樣寫:
    int(^block4)(void) = ^{
        return 3;
    };
    // 4.有返回值,有參數(shù)
    int(^block5)(int) = ^int(int a){
        return 3 * a;
    };

Block 的調用

    // 1.無返回值,無參數(shù)
    block1();
    // 2.有返回值,有參數(shù)
    int a = block5(2);

使用示例

    int multiplier = 7;
    int (^myBlock)(int) = ^(int num) {
        return num * multiplier;
    };
     
    printf("%d", myBlock(3));
    // prints "21"

Block 的 Code Snippets 快捷方式


2.Block 的底層數(shù)據(jù)結構

  • Block 本質上也是一個 OC 對象,它內部也有個isa指針;
  • Block 是封裝了函數(shù)調用以及調用環(huán)境的 OC 對象;
  • Block 的底層數(shù)據(jù)結構如下圖所示:
Block 的底層數(shù)據(jù)結構.png

通過 Clang 將以下 Block 代碼轉換為 C++ 代碼,來分析 Block 的底層實現(xiàn)。

// Clang
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
// main.m
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        void(^block)(void) = ^{
            NSLog(@"調用了block");
        };
        block();
    }
    return 0;
}
  • Block 底層數(shù)據(jù)結構就是一個__main_block_impl_0結構體對象,其中有__block_impl__main_block_desc_0兩個結構體對象成員。
    main:表示 block 所在的函數(shù)
    block:表示這個一個 block
    impl:表示實現(xiàn)(implementation)
    0:表示這是該函數(shù)中的第一個 block
  • __main_block_func_0結構體封裝了 block 里的代碼;
  • __block_impl結構體才是真正定義 block 的結構,其中的FuncPtr指針指向__main_block_func_0;
  • __main_block_desc_0是 block 的描述對象,存儲著 block 的內存大小等;
  • 定義 block 的本質:
    調用__main_block_impl_0()構造函數(shù),并且給它傳了兩個參數(shù)__main_block_func_0&__main_block_desc_0_DATA。拿到函數(shù)的返回值,再取返回值的地址&__main_block_impl_0,把這個地址賦值給 block 變量。
  • 調用 block 的本質:
    通過__main_block_impl_0中的__block_impl中的FuncPtr拿到函數(shù)地址,直接調用。
// main.cpp
struct __main_block_impl_0 {
    struct __block_impl impl;         // block的結構體
    struct __main_block_desc_0* Desc; // block的描述對象,描述block的大小等
    /*  構造函數(shù)
     ** 返回值:__main_block_impl_0 結構體
     ** 參數(shù)一:__main_block_func_0 結構體
     ** 參數(shù)二:__main_block_desc_0 結構體的地址
     ** 參數(shù)三:flags 標識位
     */
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock; //_NSConcreteStackBlock 表示block存在棧上
        impl.Flags = flags; 
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

// __main_block_func_0 封裝了block里的代碼
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_77_f_d18dtx6277bxbcd8s72my80000gn_T_main_58a448_mi_0);
}

struct __block_impl {
    void *isa;     // block的類型
    int Flags;     // 標識位
    int Reserved;  // 
    void *FuncPtr; // block的執(zhí)行函數(shù)指針,指向__main_block_func_0
};

static struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size; // block本質結構體所占內存空間
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)}; 
 
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        /*
          ** void(^block)(void) = ^{
                 NSLog(@"調用了block");
             };
         ** 定義block的本質:
         ** 調用__main_block_impl_0()構造函數(shù)
         ** 并且給它傳了兩個參數(shù) __main_block_func_0 和 &__main_block_desc_0_DATA
         ** __main_block_func_0 封裝了block里的代碼
         ** 拿到函數(shù)的返回值,再取返回值的地址 &__main_block_impl_0,
         ** 把這個地址賦值給 block
         */
        void(*block)(void) = ((void (*)())&__main_block_impl_0(
                                                               (void *)__main_block_func_0,
                                                               &__main_block_desc_0_DATA
                                                              ));
        /*
         ** block();
         ** 調用block的本質:
         ** 通過 __main_block_impl_0 中的 __block_impl 中的 FuncPtr 拿到函數(shù)地址,直接調用
         */      
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

3.Block 的變量捕獲機制

為了保證 block 內部能夠正常訪問外部的變量,block 有個變量捕獲機制。

  • 對于全局變量,不會捕獲到 block 內部,訪問方式為直接訪問;
  • 對于 auto 類型的局部變量,會捕獲到 block 內部,block 內部會自動生成一個成員變量,用來存儲這個變量的值,訪問方式為值傳遞;
  • 對于 static 類型的局部變量,會捕獲到 block 內部,block 內部會自動生成一個成員變量,用來存儲這個變量的地址,訪問方式為指針傳遞;
  • 對于對象類型的局部變量,block 會連同它的所有權修飾符一起捕獲。
    block 變量捕獲機制.png

3.1 auto 類型的局部變量

auto 自動變量:我們定義出來的變量,默認都是 auto 類型,只是省略了。

    auto int age = 10;

auto 類型的局部變量會捕獲到 block 內部,訪問方式為值傳遞。

通過 Clang 將以下代碼轉換為 C++ 代碼:

    int age = 10;
    void(^block)(void) = ^{
        NSLog(@"%d",age);
    };
    block();
  • __main_block_impl_0對象內部會生成一個相同的age變量;
  • __main_block_impl_0()構造函數(shù)多了個參數(shù),用來捕獲訪問的外面的age變量的,將它賦值給__main_block_impl_0對象內部的age變量。
struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    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 copy
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_77_f_d18dtx6277bxbcd8s72my80000gn_T_main_5ed490_mi_0,age);
}

......

    int age = 10;
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

由于是值傳遞,我們修改外部的age變量的值,不會影響到 block 內部的age變量。

    int age = 10;
    void(^block)(void) = ^{
        NSLog(@"%d",age);
    };
    age = 20;
    block();
    // 10

3.2 static 類型的局部變量

static 類型的局部變量會捕獲到 block 內部,訪問方式為指針傳遞。

通過 Clang 將以下代碼轉換為 C++ 代碼:

    static int age = 10;
    void(^block)(void) = ^{
        NSLog(@"%d",age);
    };
    block();
  • __main_block_impl_0對象內部會生成一個相同類型的age指針;
  • __main_block_impl_0()構造函數(shù)多了個參數(shù),用來捕獲訪問的外面的age變量的地址,將它賦值給__main_block_impl_0對象內部的age指針。
struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    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 copy
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_77_f_d18dtx6277bxbcd8s72my80000gn_T_main_a4bc7d_mi_0,(*age));
}

......

    static int age = 10;
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &age));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

由于是指針傳遞,我們修改外部的age變量的值,會影響到 block 內部的age變量。

    static int age = 10;
    void(^block)(void) = ^{
        NSLog(@"%d",age);
    };
    age = 20;
    block();
    // 20

3.3 全局變量

全局變量不會捕獲到 block 內部,訪問方式為直接訪問。

通過 Clang 將以下代碼轉換為 C++ 代碼:

int _age = 10;
static int _height = 20;
......
        void(^block)(void) = ^{
            NSLog(@"%d,%d",_age,_height);
        };
        block();
  • __main_block_impl_0對象內并沒有生成對應的變量,也就是說全局變量沒有捕獲到 block 內部,而是直接訪問。
int _age = 10;
static int _height = 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) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_77_f_d18dtx6277bxbcd8s72my80000gn_T_main_12efa5_mi_0,_age,_height);
}

......

    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

為什么局部變量需要捕獲,全局變量不用捕獲呢?

  • 作用域的原因,全局變量哪里都可以直接訪問,所以不用捕獲;
  • 局部變量,外部不能直接訪問,所以需要捕獲;
  • auto 類型的局部變量可能會銷毀,其內存會消失,block 將來執(zhí)行代碼的時候不可能再去訪問那塊內存,所以捕獲其值;
  • static 變量會一直保存在內存中, 所以捕獲其地址即可。

3.4 對象類型的 auto 變量

當 block 內部訪問了對象類型的 auto 變量時:

  • 如果 block 是在棧上,將不會對 auto 變量產生強引用
  • 如果 block 被拷貝到堆上
    ① block 內部的 desc 結構體會新增兩個函數(shù):
    ?copy(__main_block_copy_0,函數(shù)名命名規(guī)范同__main_block_impl_0
    ?dispose(__main_block_dispose_0
    ② 會調用 block 內部的 copy 函數(shù)
    ③ copy 函數(shù)內部會調用_Block_object_assign函數(shù)
    _Block_object_assign函數(shù)會根據(jù) auto 變量的修飾符(__strong、__weak、__unsafe_unretained)做出相應的操作,形成強引用(retain)或者弱引用
  • 如果 block 從堆上移除
    ① 會調用 block 內部的 dispose 函數(shù)
    ② dispose 函數(shù)內部會調用_Block_object_dispose函數(shù)
    _Block_object_dispose函數(shù)會自動釋放引用的 auto 變量(release)
函數(shù) 調用時機
copy 函數(shù) 棧上的 block 復制到堆時
dispose 函數(shù) 堆上的 block 被廢棄時

如下代碼,block 保存在堆中,當執(zhí)行完作用域2的時候,Person 對象并沒有被釋放,而是在執(zhí)行完作用域1的時候釋放,說明 block 內部對 Person 對象產生了強引用。

typedef void(^MyBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool { //作用域1
        MyBlock block;
        { //作用域2
            Person *p = [Person new];
            p.name = @"zhangsan";      
            block = ^{
                NSLog(@"%@",p.name);
            };
        }
        NSLog(@"-----");
    }
    return 0;
}
// -----
// Person-dealloc

通過 Clang 將以上代碼轉換為 C++ 代碼:

// 弱引用需要運行時的支持,所以需要加上 -fobjc-arc -fobjc-runtime=ios-8.0.0
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

__main_block_impl_0中生成了一個Person *__strong p指針,指向外面的 person 對象,且是強引用。

typedef void(*MyBlock)(void);

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    Person *__strong p; // 強引用
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *_p, int flags=0) : p(_p) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    Person *__strong p = __cself->p; // bound by copy
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_77_f_d18dtx6277bxbcd8s72my80000gn_T_main_9e5699_mi_1,((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("name")));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);}

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(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        MyBlock block;
        {
            Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("new"));
            ((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)p, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_77_f_d18dtx6277bxbcd8s72my80000gn_T_main_9e5699_mi_0);

            block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, p, 570425344));
        }
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_77_f_d18dtx6277bxbcd8s72my80000gn_T_main_9e5699_mi_2);
    }
    return 0;
}

添加了__weak修飾后,當執(zhí)行完作用域2的時候,Person 對象就被被釋放了。

typedef void(^MyBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool { //作用域1
        MyBlock block;
        { //作用域2
            __weak Person *p = [Person new];
            p.name = @"zhangsan";      
            block = ^{
                NSLog(@"%@",p.name);
            };
        }
        NSLog(@"-----");
    }
    return 0;
}
// Person-dealloc
// -----

同樣的,通過 Clang 將以上代碼轉換為 C++ 代碼。
__main_block_impl_0中生成了一個Person *__weak p指針,指向外面的 person 對象,且是弱引用。
說明當 block 內部 訪問了對象類型的 auto 變量時,如果 block 被拷貝到堆上,會連同對象的所有權修飾符一起捕獲。

......
struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    Person *__weak p; //弱引用
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak _p, int flags=0) : p(_p) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    Person *__weak p = __cself->p; // bound by copy
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_77_f_d18dtx6277bxbcd8s72my80000gn_T_main_c61841_mi_1,((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("name")));
}
......

3.5 __block 修飾的變量

3.5.1 __block 作用

默認情況下 block 是不能修改外面的 auto 變量的,解決辦法?

  • 變量用 static 修飾(原因:捕獲 static 類型的局部變量是指針傳遞,可以訪問到該變量的內存地址)
  • 全局變量
  • __block(我們只希望臨時用一下這個變量臨時改一下而已,而改為 static 變量和全局變量會一直在內存中)

3.5.2 __block 修飾符

  • __block 可以用于解決 block 內部無法修改 auto 變量值的問題;
  • __block 不能修飾全局變量、靜態(tài)變量;
  • 編譯器會將 __block 變量包裝成一個對象(struct __Block_byref_age_0(byref:按地址傳遞));
  • 加 __block 修飾不會修改變量的性質,它還是 auto 變量;
  • 一般情況下,對被捕獲變量進行賦值(賦值!=使用)操作需要添加 __block 修飾符。比如給數(shù)組添加或者刪除對象,就不用加 __block 修飾;
  • 在 MRC 下使用 __block 修飾對象類型,在 block 內部不會對該對象進行 retain 操作,所以在 MRC 環(huán)境下可以通過 __block 解決循環(huán)引用的問題。

使用示例

    __block int age = 10;
    void(^block)(void) = ^{
        age = 20;
        NSLog(@"block-%d",age);
    };
    block();
    NSLog(@"%d",age);
    // block-20
    // 20

通過 Clang 將以上代碼轉換為 C++ 代碼。

  • 編譯器會將 __block 修飾的變量包裝成一個__Block_byref_age_0對象;
  • 以上age = 20;的賦值過程為:通過 block 結構體里的(__Block_byref_age_0)類型的 age 指針,找到__Block_byref_age_0結構體的內存(即被 __block 包裝成對象的內存),把__Block_byref_age_0結構體里的 age 變量的值改為20。
  • 由于編譯器將 __block 變量包裝成了一個對象,所以它的內存管理幾乎等同于訪問對象類型的 auto 變量,但還是有差異,下面會講到。
struct __Block_byref_age_0 {
    void *__isa;
    __Block_byref_age_0 *__forwarding;
    int __flags;
    int __size;
    int age; 
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __Block_byref_age_0 *age; // by ref
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__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_age_0 *age = __cself->age; // bound by ref
    (age->__forwarding->age) = 20;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_77_f_d18dtx6277bxbcd8s72my80000gn_T_main_9578d0_mi_0,(age->__forwarding->age));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->age, 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(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_77_f_d18dtx6277bxbcd8s72my80000gn_T_main_9578d0_mi_1,(age.__forwarding->age));
    }
    return 0;
}

3.5.3 __block 的內存管理

  • 當 block 在棧上時,并不會對 __block 變量產生強引用
  • 當 block 被 copy 到堆時
    ① block 內部的 desc 結構體會新增兩個函數(shù):
    ?copy(__main_block_copy_0,函數(shù)名命名規(guī)范同__main_block_impl_0
    ?dispose(__main_block_dispose_0
    ② 會調用 block 內部的 copy 函數(shù)
    ③ copy 函數(shù)內部會調用_Block_object_assign函數(shù)
    _Block_object_assign函數(shù)會對 __block 變量形成強引用(retain)
  • 當 block 從堆中移除時
    ① 會調用 block 內部的 dispose 函數(shù)
    ② dispose 函數(shù)內部會調用_Block_object_dispose函數(shù)
    _Block_object_dispose函數(shù)會自動釋放引用的 __block 變量(release)
    __block 的內存管理.png

3.5.4 __block 的 __forwarding 指針

__block 的__forwarding指針存在的意義?
為什么要通過 age 結構體里的__forwarding指針拿到 age 變量的值,而不直接 age 結構體拿到 age 變量的值呢?

__block 的__forwarding是指向自己本身的指針,為了不論在任何內存位置,都可以順利的訪問同一個 __block 變量。

  • block 對象 copy 到堆上時,內部的 __block 變量也會 copy 到堆上去。為了防止 age 的值賦值給棧上的 __block 變量,就使用了__forwarding;
  • 當 __block 變量在棧上的時候,__block 變量的結構體中的__forwarding指針指向自己,這樣通過__forwarding取到結構體中的 age 給它賦值沒有問題;
  • 當 __block 變量 copy 到堆上后,棧上的__forwarding指針會指向 copy 到堆上的 _block 變量結構體,而堆上的__forwarding指向自己;

這樣不管我們訪問的是棧上還是堆上的 __block 變量結構體,只要是通過__forwarding指針訪問,都是訪問到堆上的 __block 變量結構體;給 age 賦值,就肯定會賦值給堆上的那個 __block 變量中的 age。

__forwarding.png

3.5.5 對象類型的 auto 變量、__block 變量內存管理區(qū)別

  • 當 block 在棧上時,對它們都不會產生強引用
  • 當 block 拷貝到堆上時,都會通過 copy 函數(shù)來處理它們
對象類型的 auto 變量
(假設變量名叫做p)
__block 變量
(假設變量名叫做a)
_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/); _Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_assign函數(shù)會根據(jù) auto 變量的修飾符(__strong、__weak__unsafe_unretained)做出相應的操作,形成強引用(retain)或者弱引用 _Block_object_assign函數(shù)會對 __block 變量形成強引用(retain)
  • 當 block 從堆上移除時,都會通過 dispose 函數(shù)來釋放它們
對象類型的 auto 變量
(假設變量名叫做p)
__block 變量
(假設變量名叫做a)
_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/); _Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);

3.5.6 被 __block 修飾的對象類型

  • 當 __block 變量在棧上時,不會對指向的對象產生強引用
  • 當 __block 變量被 copy 到堆時
    __Block_byref_object_0即 __block 變量內部會新增兩個函數(shù):
    ?copy(__Block_byref_id_object_copy
    ?dispose(__Block_byref_id_object_dispose
    ② 會調用 __block 變量內部的 copy 函數(shù)
    ③ copy 函數(shù)內部會調用_Block_object_assign函數(shù)
    _Block_object_assign函數(shù)會根據(jù)所指向對象的修飾符(__strong、__weak__unsafe_unretained)做出相應的操作,形成強引用(retain)或者弱引用(注意:這里僅限于 ARC 時會 retain,MRC 時不會 retain)
  • 如果 __block 變量從堆上移除
    ① 會調用 __block 變量內部的 dispose 函數(shù)
    ② dispose 函數(shù)內部會調用_Block_object_dispose函數(shù)
    _Block_object_dispose函數(shù)會自動釋放指向的對象(release)
int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        
        __block NSObject *object = [[NSObject alloc] init];
        void(^block)(void) = ^{
            object = [[NSObject alloc] init];
        };
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
struct __Block_byref_object_0 {
    void *__isa;
    __Block_byref_object_0 *__forwarding;
    int __flags;
    int __size;
    void (*__Block_byref_id_object_copy)(void*, void*); // copy
    void (*__Block_byref_id_object_dispose)(void*);     // dispose
    NSObject *__strong object;
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __Block_byref_object_0 *object; // by ref
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_object_0 *_object, int flags=0) : object(_object->__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_object_0 *object = __cself->object; // bound by ref
    (object->__forwarding->object) = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->object, (void*)src->object, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->object, 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(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));

        __attribute__((__blocks__(byref))) __Block_byref_object_0 object = {
            (void*)0,
            (__Block_byref_object_0 *)&object, 
            33554432, 
            sizeof(__Block_byref_object_0), 
            __Block_byref_id_object_copy_131,
            __Block_byref_id_object_dispose_131, 
            ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"))
        };
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_object_0 *)&object, 570425344));
    }
    return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
    // __block 對象結構體的地址+40個字節(jié),即為結構體中 object 對象的地址
    _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
    _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}

注意:在 MRC 下使用 __block 修飾對象類型,在 block 內部不會對該對象進行 retain 操作,所以在 MRC 環(huán)境下可以通過 __block 解決循環(huán)引用的問題。

示例(MRC)

// 對象類型的捕獲,連同所有權修飾符一起捕獲,所以 block 對 person 強引用
    HTPerson *person = [[HTPerson alloc] init];

    void(^block)(void)  = [^{
        NSLog(@"%p", person);
    } copy];
        
    [person release];
        
    block();
        
    [block release];

// 0x10053da30
// -[HTPerson dealloc]
// __block 修飾的對象類型的捕獲,MRC 下在 block 內部不會對該對象進行 retain 操作
    __block HTPerson *person = [[HTPerson alloc] init];

    void(^block)(void)  = [^{
        NSLog(@"%p", person);
    } copy];
        
    [person release];
        
    block();
        
    [block release];

// -[HTPerson dealloc]
// 0x1007337d0

4.Block 的類型

block 有 3 種類型,可以通過調用 class 方法或者 isa 指針 查看具體類型,最終都是繼承自 NSBlock 類型。

block類型 描述 環(huán)境
__ NSGlobalBlock __
( _NSConcreteGlobalBlock )
全局block,保存在數(shù)據(jù)段 沒有訪問auto變量
__ NSStackBlock __
( _NSConcreteStackBlock )
棧block,保存在棧區(qū) 訪問了auto變量
__ NSMallocBlock __
( _NSConcreteMallocBlock )
堆block,保存在堆區(qū) __ NSStackBlock __調用了copy

打印各種 block 的類型,以及遍歷 block 的父類類型,如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        void(^block1)(void) = ^{
            NSLog(@"hello");
        };
        /*  block2
         ** 在ARC下會自動copy,從棧復制到堆,所以為__NSMallocBlock__類型
         ** 在MRC下為__NSStackBlock__類型,需要手動調用copy方法才會變?yōu)開_NSMallocBlock__類型
         **     同時,在不需要該block的時候需要手動調用release方法
         */ 
        int age = 10;
        void(^block2)(void) = ^{
            NSLog(@"%d",age);
        };
        
        NSLog(@"%@,%@,%@", [block1 class], [block2 class], [^{
            NSLog(@"%d",age);
        } class]);
        
        Class class = [block1 class];
        while (class) {
            NSLog(@"%@",class);
            class = [class superclass];
        }
    }
    return 0;
}
// __NSGlobalBlock__,__NSMallocBlock__,__NSStackBlock__
// __NSGlobalBlock__
// __NSGlobalBlock
// NSBlock
// NSObject

每一種類型的 block 調用 copy 后的結果如下所示:

block類型 副本源的配置存儲區(qū) 復制效果
_NSConcreteGlobalBlock 程序的數(shù)據(jù)段區(qū) 什么也不做
_NSConcreteStackBlock 從棧復制到堆
_NSConcreteMallocBlock 引用計數(shù)增加

__ NSStackBlock __ 存在的問題:

以下是在 MRC 環(huán)境下,block 類型為__NSStackBlock__
當 test() 函數(shù)執(zhí)行完畢,棧上的東西可能會被銷毀,數(shù)據(jù)就會變成垃圾數(shù)據(jù)。盡管 block 還能正常調用,但是輸出的 age 的值發(fā)生了錯亂。

void (^block)(void);
void test()
{    
    // __NSStackBlock__
    int age = 10;
    block = ^{
        NSLog(@"%d", age);
    };
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        block();
    }
    return 0;
}
// 272632936

解決辦法:調用copy方法,將棧 block 復制到堆。

void (^block)(void);
void test()
{    
    // __NSMallocBlock__
    int age = 10;
    block = [^{
        NSLog(@"%d", age);
    } copy];
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        block();
        [block release];
    }
    return 0;
}
// 10

5.Block 的 copy

ARC 環(huán)境下,編譯器會根據(jù)情況自動將棧上的 block 復制到堆上,比如以下幾種情況:

  • 手動調用 block 的 copy 方法時;
  • block 作為函數(shù)返回值時(Masonry 框架中用很多);
  • 將 block 賦值給__strong指針時;
  • block 作為 Cocoa API 中方法名含有usingBlock的方法參數(shù)時;
  • block 作為 GCD API 的方法參數(shù)時。

block 作為屬性的寫法:

  • ARC 下寫strong或者copy都會對 block 進行強引用,都會自動將 block 從棧 copy 到堆上;
  • 建議都寫成copy,這樣 MRC 和 ARC 下一致。
// MRC
@property (nonatomic, copy) void(^block)(void);
// ARC
@property (nonatomic, copy) void(^block)(void);
@property (nonatomic, strong) void(^block)(void);

6.Block 的循環(huán)引用問題

為什么 block 會產生循環(huán)引用?

  • 相互循環(huán)引用:如果當前 block 對當前對象的某一成員變量進行捕獲的話,可能會對它產生強引用。而當前 block 又由于當前對象對其有一個強引用,就產生了相互循環(huán)引用的問題;
  • 大環(huán)引用:我們如果使用__block的話,在 ARC 下可能會產生循環(huán)引用(MRC 則不會),在 ARC 下可以通過斷環(huán)的方式去解除循環(huán)引用。但是有一個弊端,如果該 block 一直得不到調用,循環(huán)引用就一直存在。

6.1 ARC

  • __weak或者__unsafe_unretained解決:
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        NSLog(@"%p",weakSelf);
    };
    __unsafe_unretained id weakSelf = self;
    self.block = ^{
        NSLog(@"%p",weakSelf);
    };

注意__unsafe_unretained會產生懸垂指針。

  • __block解決(必須要調用 block):
    缺點:必須要調用 block,而且 block 里要將指針置為 nil。如果一直不調用 block,對象就會一直保存在內存中,造成內存泄漏。
    __block id weakSelf = self;
    self.block = ^{
        NSLog(@"%p",weakSelf);
        weakSelf = nil;
    };
    self.block();

6.2 MRC

  • __unsafe_unretained解決:同 ARC
  • __block解決(在 MRC 下使用 __block 修飾對象類型,在 block 內部不會對該對象進行 retain 操作,所以在 MRC 環(huán)境下可以通過 __block 解決循環(huán)引用的問題)
    __block id weakSelf = self;
    self.block = ^{
        NSLog(@"%p",weakSelf);
    };

7.相關面試題

Q:block 的本質是什么?

封裝了函數(shù)調用以及調用環(huán)境的 OC 對象。

Q:block 的屬性修飾詞為什么是 copy?使用 block 有哪些使用注意?

block 一旦沒有進行 copy 操作,就不會在堆上。
使用注意:循環(huán)引用問題。

Q:block在給 NSMutableArray 添加或移除對象,需不需要添加 __block?

不需要。

Q:block 的變量捕獲機制

block 的變量捕獲機制,是為了保證 block 內部能夠正常訪問外部的變量。

  • 對于全局變量,不會捕獲到 block 內部,訪問方式為直接訪問;
  • 對于 auto 類型的局部變量,會捕獲到 block 內部,block 內部會自動生成一個成員變量,用來存儲這個變量的值,訪問方式為值傳遞;
  • 對于 static 類型的局部變量,會捕獲到 block 內部,block 內部會自動生成一個成員變量,用來存儲這個變量的地址,訪問方式為指針傳遞
  • 對于對象類型的局部變量,block 會連同它的所有權修飾符一起捕獲。

Q:為什么局部變量需要捕獲,全局變量不用捕獲呢?

  • 作用域的原因,全局變量哪里都可以直接訪問,所以不用捕獲;
  • 局部變量,外部不能直接訪問,所以需要捕獲;
  • auto 類型的局部變量可能會銷毀,其內存會消失,block 將來執(zhí)行代碼的時候不可能再去訪問那塊內存,所以捕獲其值;
  • static 變量會一直保存在內存中, 所以捕獲其地址即可。

Q:self 會不會捕獲到 block 內部?

會捕獲。
OC 方法都有兩個隱式參數(shù),方法調用者self和方法名_cmd。
參數(shù)也是一種局部變量。

Q:_name 會不會捕獲到 block 內部?

會捕獲。
不是將_name變量進行捕獲,而是直接將self捕獲到 block 內部,因為_name是 Person 類的成員變量,_name來自當前的對象/方法調用者self(self->_name)
如果使用self.name即調用selfgetter方法,即給self對象發(fā)送一條消息,那還是要訪問到self。self是局部變量,不是全局變量,所以self會捕獲到 block 內部。

Q:__ NSStackBlock __ 存在的問題:

如果沒有將 block 從棧上 copy 到堆上,那我們訪問棧上的 block 的話,可能會由于變量作用域結束導致棧上的 block 以及 __block 變量被銷毀,而造成內存崩潰?;蛘邤?shù)據(jù)可能會變成垃圾數(shù)據(jù),盡管將來 block 還能正常調用,但是它捕獲的變量的值已經(jīng)錯亂了。
解決辦法:將 block 的內存放堆里,意味著它就不會自動銷毀,而是由我們程序員來決定什么時候銷毀它。

Q:默認情況下 block 是不能修改外面的 auto 變量的,解決辦法?

  • 變量用 static 修飾(原因:捕獲 static 類型的局部變量是指針傳遞,可以訪問到該變量的內存地址)
  • 全局變量
  • __block(我們只希望臨時用一下這個變量臨時改一下而已,而改為 static 變量和全局變量會一直在內存中)

Q:__block 修飾符使用注意點:

在 MRC 下使用 __block 修飾對象類型,在 block 內部不會對該對象進行 retain 操作,所以在 MRC 環(huán)境下可以通過 __block 解決循環(huán)引用的問題。

Q:__block 的 __forwarding 指針存在的意義?為什么要通過 age 結構體里的 __forwarding 指針拿到 age 變量的值,而不直接 age 結構體拿到 age 變量的值呢?

見 3.5.4 __block 的 __forwarding 指針。

Q:為什么通過 __weak 去修飾成員變量或對象就可以達到規(guī)避循環(huán)引用的目的呢?

block 對于對象類型的局部變量連同所有權修飾符一起截獲,所以如果我們在外部定義的對象是 __weak 所有權修飾符的,那么在 block 中所產生的結構體里所持有的變量也是 __weak 類型的。

Q:解決在 block 內部通過弱指針訪問對象成員時編譯器報錯的問題:

    __weak typeof(self) weakSelf = self;
    self.block = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        NSLog(@"%d",strongSelf->age);
    };

Q:以下代碼的打印結果是?

    __block int multiplier = 6;
    int (^block)(int) = ^(int num) {
        return num * multiplier;
    };
    multiplier = 4;
    NSLog(@"%d",block(2));
    // 8

Q:以下代碼有問題嗎?

    __block id weakSelf = self;
    self.block = ^{
        NSLog(@"%p",weakSelf);
    };
  • 在 MRC 下,不會產生循環(huán)引用;
  • 在 ARC 下,會產生循環(huán)引用,導致內存泄漏,解決方案如下。
    __block id weakSelf = self;
    self.block = ^{
        NSLog(@"%p",weakSelf);
        weakSelf = nil;
    };
    self.block();

缺點:必須要調用 block,而且 block 里要將指針置為 nil。如果一直不調用 block,對象就會一直保存在內存中,造成內存泄漏。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 提示:下面會把OC相應的類轉化為C++代碼,OC代碼轉C++代碼的生成 一、block 知識回顧block 是一個...
    IIronMan閱讀 811評論 0 2
  • 第一部分:Block本質 Q:什么是Block,Block的本質是什么? block本質上也是一個OC對象,它內部...
    sheldon_龍閱讀 606評論 0 0
  • 一 block基本使用 二 block底層結構 三 block變量捕獲 四 block的類型 五 block對象類...
    當前明月閱讀 1,335評論 3 4
  • Block原理 Block變量捕獲 Block類型 copy操作和Block內部訪問對象類型的變量 __block...
    75b9020bd6db閱讀 726評論 0 0
  • 一、Block的底層結構及本質 (1)block本質: 從代碼可以看出,Block的本質就是NSObject. 也...
    王的for閱讀 723評論 0 2

友情鏈接更多精彩內容