前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >iOS 进阶之 Block 的本质及原理

iOS 进阶之 Block 的本质及原理

作者头像
网罗开发
发布2021-01-29 15:59:42
发布2021-01-29 15:59:42
67200
代码可运行
举报
文章被收录于专栏:网罗开发网罗开发
运行总次数:0
代码可运行

目录

1. 前言

2. block 是什么

3. 生成对应的 main.cpp 文件

3.1 创建 Command Line Tool 项目

3.2 获取 main 文件

4. 查看 block 内部结构

4.1 查看内部结构

4.2 声明的 int age

4.3 参数分析

5. block() 执行内部代码
6. 验证 block 结构体类型
7. 验证 block 是不是对象

8. block 的三种类型基本概念

8.1 NSGlobalBlock

8.2 NSMallocBlock

8.3 NSStackBlock

9. 总结

1. 前言

相信稍微有点开发经验的开发者,应该都对 block 有一定的了解。刚开始使用 block 的时候,可能觉得很方便。知道怎么去用,但是知其然知其所以然的态度来分析一下 block 的原理和内部结构是什么。

2. block 是什么

  • block 本质上也是一个 OC 对象,它内部也有个 isa 指针
  • block 是封装了函数调用以及函数调用环境的 OC 对象
  • block 是封装函数及其上下文的 OC 对象

block 内部构成(以局部变量为例子来了解整个执行流程)

3. 生成对应的 main.cpp 文件

3.1 创建 Command Line Tool 项目

创建一个 macOS 的 Command Line Tool 项目,在项目里写上 block 代码如下:

代码语言:javascript
代码运行次数:0
运行
复制
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int age = 10;
        void(^block)(int, int) = ^(int a, int b) {
            
            NSLog(@"this is a block! -- %d", age);
            NSLog(@"this is a block!");
            NSLog(@"this is a block!");
            NSLog(@"this is a block!");
        };
        age = 20;
        block(10,10);
    }
    return 0;
}

3.2 获取 main 文件

然后在终端进到 main.m 所在的文件目录下,指向下面的指令:

代码语言:javascript
代码运行次数:0
运行
复制
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

然后在 main.m 所在的文件夹下对象 oc 的 main 文件编译生成了对应的 c++ 的 main.cpp 文件,在 main.cpp 中可以查看对应的。如下图:

4. 查看 block 内部结构

编译后的 main 方法内代码如下:

代码语言:javascript
代码运行次数:0
运行
复制
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        int age = 10;
        void(*block)(int, int) = ((void (*)(int, int))
        &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age)); 
        //block(10,10)的的内部结构代码
        ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 10);
    }
    return 0;
}

从以上 c++ 代码中 block 的声明和定义分别与 oc 代码中相对应显示。将 c++ 中 block 的声明和调用分别取出来查看其内部实现。定义 block 变量对应的代码:

代码语言:javascript
代码运行次数:0
运行
复制
 void(*block)(int, int) = ((void (*)(int, int))
 &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
 
//简化后的代码
 void (*block)(int, int) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age);

上述定义代码中,可以发现所有带 _main_block ,__main 表示是 main.cpp 文件,_block 代表 block 的变量名字就是 block。 block 定义中调用了 __main_block_impl_0 函数,并且将 __main_block_impl_0 函数的地址赋值给了 block。

所以我先来看看 __main_block_impl_0 函数的内部结构。在 main.cpp 中查找__main_block_impl_0 函数代码如下:

代码语言:javascript
代码运行次数:0
运行
复制
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) {
    //在函数栈上声明,则为_NSConcreteStackBlock
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

从上面的代码中可以看出:

  1. 声明了 struct __block_impl impl 结构体;
  2. 声明了 struct __main_block_desc_0 * Desc 结构体;
  3. 声明 int age;
  4. __main_block_imp_0 结构体内,声明了一个同名构造函数__main_block_imp_0,构造函数中对一些变量进行了赋值最终会返回一个结构体。最终将一个 __main_block_imp_0 结构体的地址赋值给了block 变量;
  5. _main_block_impl_0 结构体内可以发现 __main_block_impl_0 构造函数中传入了四个参数:
    1. void *)__main_block_func_0
    2. &__main_block_desc_0_DATA
    3. age(age(_age) 表示传入的 _age 参数会自动赋值给 age 成员,相当于 age = _age)
    4. int flags=0(其中flags有默认值,也就说 flags 参数在调用的时候可以省略不传。)

4.1 查看内部结构

先查看一下__block_impl 和 __main_block_desc_0 的内部结构,代码如下

代码语言:javascript
代码运行次数:0
运行
复制
struct __block_impl {  void *isa; //指明对象的 Class  int Flags;  int Reserved;  void *FuncPtr;};//block 描述信息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)};

如上代码可以表明:

  • __block_impl 结构体,即为 Block 的结构体,可理解为 Block 的类结构。
  • _main 表示的是 main.cpp 的文件名,_block 表示定义 Block 的变量名字
  • block 也有一个 isa 指针,所以block是一个 OC 对象
  • FuncPtr:指向调用函数的地址
  • __main_block_desc_0 :block 描述信息
  • Block_size:block 的大小

4.2 声明的 int age

block 是封装函数及其上下文的 OC 对象,block 可以根据上下文环,因为block 内部使用 age,所以在结构体也声明 int age,并且把赋值__main_bloc_impl_0 里面的 age 参数值赋值 10 给 age。

4.3 参数分析

__main_block_imp_0 中的 (void *)__main_block_func_0、&__main_block_desc_0_DATA、age 分别代表什么

4.3.1 首页来查看(void *)__main_block_func_0 的内部结构

代码语言:javascript
代码运行次数:0
运行
复制
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {

    int age = __cself->age; // bound by copy
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gh_3fkjx11j0zb3jm_f752skdn00000gn_T_main_f3f131_mi_0, age);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gh_3fkjx11j0zb3jm_f752skdn00000gn_T_main_f3f131_mi_1);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gh_3fkjx11j0zb3jm_f752skdn00000gn_T_main_f3f131_mi_2);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gh_3fkjx11j0zb3jm_f752skdn00000gn_T_main_f3f131_mi_3);

}

从上边 __main_block_func_0 函数代码表明:

  1. 首先取出 block 中 age 的值,紧接着可以看到四个熟悉的 NSLog,可以发现这段代码恰恰是我们在 block 块中写下的代码。
  2. 表明 __main_block_func_0 函数中其实存储着我们 block 中写下的代码
  3. 我们写在block块中的代码封装成__main_block_func_0 函数,并将__main_block_func_0 函数的地址传入了 __main_block_impl_0 的构造函数中保存在结构体内。
  4. _cself 相当于 Objective-C 中的 self,前面不是 __main_block_impl_0 结构体函数把传进来的值传进来,int age = __cself->age; // bound by copy 表明在函数内声明一个 int age 局部变量。所以在 block 后定义后改变值的话,block 内部函数 age 值是不变的。

4.3.2 其次我在看看 struct __main_block_desc_0 *Desc 内部结构,&__main_block_desc_0_DATA 赋值给了 Desc

代码语言:javascript
代码运行次数: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)
};

从上面的代码中可以表明,__main_block_desc_0 中存储着两个参数:

  • reserved 赋值为 0
  • Block_size 存储 __main_block_impl_0 的占用空间大小
  • 将 __main_block_desc_0 结构体的地址传入 __main_block_func_0 中赋值给 Desc

4.3.3 age 参数

age 也就是我们定义的局部变量

因为在 block 块中使用到 age 局部变量,所以在 block 声明的时候这里才会将 age 作为参数传入,也就说block会捕获 age

如果没有在 block 中使用 age,这里将只会传入 (void *)__main_block_func_0,&__main_block_desc_0_DATA 两个参数。

这里可以根据源码思考一下为什么当我们在定义 block 之后修改局部变量age 的值,在 block 调用的时候无法生效?

block 在定义的之后已经将 age 的值传入,存储在 __main_block_imp_0 结构体中并在调用的时候将 age 从 block 中取出来使用。

在 block 定义之后对局部变量进行改变是无法被 block 捕获的。

4.3.4 通过上面对 __main_block_impl_0 结构体构造函数三个参数在把上面的代码写下强调一下

代码语言:javascript
代码运行次数:0
运行
复制
 __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;
  }

_block_impl 结构体中isa指针存储着 &_NSConcreteStackBlock 地址,可以暂时理解为其类对象地址,block 就是 _NSConcreteStackBlock 类型的。

block 代码块中的代码被封装成 __main_block_func_0 函数,FuncPtr 则存储着 __main_block_func_0 函数的地址。

Desc 指向 __main_block_desc_0 结构体对象,其中存储 __main_block_impl_0 结构体所占用的内存。

5. block() 执行内部代码
代码语言:javascript
代码运行次数:0
运行
复制
//block(10,10)执行的内部代码  
((void (*)(__block_impl *, int, int))
((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 10);

通过查看 block 执行内部代码表明:

  1. 发现调用 block 是通过 block 找到 FunPtr 直接调用
  2. 通过上面分析我们知道 block 指向的是 __main_block_impl_0 类型结构体
  3. 我们发现 __main_block_impl_0 结构体中并不直接就可以找到 FunPtr,而FunPtr 是存储在 __block_impl 中的

为什么 block 可以直接调用 __block_impl 中的 FunPtr 呢?

  1. 通过上边的执行 block(10,10) 的内部代码,这块 (__block_impl *)block代码表明 block 强制转化为 __block_impl 类型的
  2. 因为 __block_impl 是 __main_block_impl_0 结构体的第一个成员,相当于将 __block_impl 结构体的成员直接拿出来放在 __main_block_impl_0中
  3. 那么也就说明 __block_impl 的内存地址就是 __main_block_impl_0 结构体的内存地址开头。所以可以转化成功。并找到 FunPtr 成员。

通过以上面的叙述也可以表明:

  1. FunPtr 中存储着通过代码块封装的函数地址,那么调用此函数,也就是会执行代码块中的代码。
  2. 回头查看 __main_block_func_0 函数,可以发现第一个参数就是__main_block_impl_0 类型的指针。也就是说将 block 传入__main_block_func_0 函数中,便于从中取出 block 捕获的值。
6. 验证 block 结构体类型
验证b lock 的本质是 __main_block_impl_0 结构体类型,通过模拟一下 block的内部代码:
代码语言:javascript
代码运行次数:0
运行
复制
#import <Foundation/Foundation.h>

struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
};

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int age;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        void (^block)(int, int) =  ^(int a , int b){
            NSLog(@"this is a block! -- %d", age);
            NSLog(@"this is a block!");
            NSLog(@"this is a block!");
            NSLog(@"this is a block!");
        };
        // 将底层的结构体强制转化为我们自己写的结构体,通过我们自定义的结构体探寻block底层结构体
        struct __main_block_impl_0 *blockStruct = (__bridge struct __main_block_impl_0 *)block;
        block(10, 10);
    }
    return 0;
}

在 block(10, 10) 和 NSLog(@"this is a block! -- %d", age) 打个断点,查看一下 blockStruct 内部的堆栈信息。如下图:

表明 FunPtr 函数地址是 0x100000eb0

在看一下 block{} 里实现的代码看一下地址和 FunPtr 地址是都一样,在 NSLog(@"this is a block! -- %d", age); 断点处时候,在 xcode->debug->Debug Workflow->always show Disassembly, 会显示堆栈信息中的函数调用地址。

如上图说明两个地址是一样的,可以说明 block 的本质就是_main_block_impl_0

7. 验证 block 是不是对象
代码语言:javascript
代码运行次数:0
运行
复制
void (^block1)(int, int) =  ^(int a , int b){
    NSLog(@"this is a block! -- %d", age);
    NSLog(@"this is a block!");
    NSLog(@"this is a block!");
    NSLog(@"this is a block!");
};

NSLog(@"%@",[block1 class]);
NSLog(@"%@",[[block1 class] superclass]);
NSLog(@"%@",[[[block1 class] superclass] superclass]);
NSLog(@"%@",[[[[block1 class] superclass] superclass] superclass]);
NSLog(@"%@",[[[[[block1 class] superclass] superclass] superclass] superclass]);

打印结果如下:

代码语言:javascript
代码运行次数:0
运行
复制
 __NSGlobalBlock__
 __NSGlobalBlock
 NSBlock
 NSObject
 (null)

从打印结果可以表明:

  • 输出了block1 的类型,也证实了 block 是对象,最终继承 NSObject
  • block 的类型是 NSGlobalBlock(下面会讲)

8. block 的三种类型基本概念

根据 Block 对象创建时所处数据区不同而进行区别:

  • _NSConcreteStackBlock:在栈上创建的 Block 对象
  • _NSConcreteMallocBlock:在堆上创建的 Block 对象
  • _NSConcreteGlobalBlock:全局数据区的 Block 对象

如下图:

对着三种类型 block 进行 copy 操作后的结果?

  • __NSGlobalBlock __ 调用 copy 操作后,什么也不做
  • __NSMallocBlock __ 调用 copy 操作后,复制效果是:引用计数增加;副本存储位置是堆
  • __NSStackBlock __ 调用 copy 操作后,复制效果是:从栈复制到堆;副本存储位置是堆

8.1 NSGlobalBlock

就是全局数据区的 Block 对象,那么什么时候 block 的类型是_NSConcreteGlobalBlock 类型?

主要还是看 block 捕获变量的类型来确定的,以下几种情况 block 类型是_NSConcreteGlobalBlock:

  • 不捕获全局变量(包括全局静态变量)
  • 没有捕获变量或者捕获的只有局部静态变量
  • 以指针形式截获局部静态变量

下面就通过代码来验证一下:

没有捕获变量的 block

代码语言:javascript
代码运行次数:0
运行
复制
void (^myBlock)(int, int) =  ^(int a , int b){
           
    NSLog(@"没有捕获变量");   
};

NSLog(@"myBlockType:%@",[myBlock class]);

打印结果如下:

代码语言:javascript
代码运行次数:0
运行
复制
myBlockType:__NSGlobalBlock__

表明没有捕获变量的情况是 NSGlobalBlock 全局数据区 block

代码语言:javascript
代码运行次数:0
运行
复制
int global_var = 50; //全局变量
static int global_staic_var = 20; //全局静态变量
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        static int staic_var = 10; //局部静态变量
        void (^myBlock)(int, int) =  ^(int a , int b){
            //只捕获局部静态变量
            NSLog(@"staic_var:%d",staic_var);
        };
        NSLog(@"myBlockType:%@",[myBlock class]);
        myBlock(10, 10);
    }
}

通过运行打印结果如下:

代码语言:javascript
代码运行次数:0
运行
复制
myBlockType:__NSGlobalBlock__
staic_var:10

下面看一下捕获全局变量和局部变量编译成的 cpp 文件代码:

代码语言:javascript
代码运行次数:0
运行
复制
int global_var = 50; //全局变量
static int global_staic_var = 20; //全局静态变量
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        static int staic_var = 10; //局部静态变量
        void (^myBlock)(int, int) =  ^(int a , int b){
            //只捕获局部静态变量
            NSLog(@"staic_var:%d",staic_var);
            NSLog(@"global_var:%d", global_var);
            NSLog(@"global_staic_var:%d", global_staic_var);
        };
        NSLog(@"myBlockType:%@",[myBlock class]);
        myBlock(10, 10);
    }
}

通过指令转换成 cpp 文件看一下:

代码语言:javascript
代码运行次数:0
运行
复制
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

打开 main.cpp 文件:

代码语言:javascript
代码运行次数:0
运行
复制
int global_var = 50;
static int global_staic_var = 20;
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *staic_var;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_staic_var, int flags=0) : staic_var(_staic_var) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

//发现全局静态变量、全局变量直接用
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
    
    //赋值指针
    int *staic_var = __cself->staic_var; // bound by copy
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gh_3fkjx11j0zb3jm_f752skdn00000gn_T_main_5050a8_mi_0,global_var);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gh_3fkjx11j0zb3jm_f752skdn00000gn_T_main_5050a8_mi_1,global_staic_var);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gh_3fkjx11j0zb3jm_f752skdn00000gn_T_main_5050a8_mi_2,(*staic_var));

}

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[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        static int staic_var = 10;

        void (*myBlock)(int, int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &staic_var));

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_gh_3fkjx11j0zb3jm_f752skdn00000gn_T_main_5050a8_mi_3,((Class (*)(id, SEL))(void *)objc_msgSend)((id)myBlock, sel_registerName("class")));
        ((void (*)(__block_impl *, int, int))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock, 10, 10);
    }
    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

通过声明了全局变量、全局静态变量、局部静态变量三种变量,通过上代码表明:

  • 发现可以捕获的只有 &staic_var 并且以指针形式截获局部静态变量
  • 全局变量、全局静态变量直接用,所以在声明 block 后修改全局变量、全局静态变量是可以的
  • __main_block_func_0 在函数方法内,赋值指针表明,所以在声明 block后,修改 staic_var 局部静态变量的值是可以的

8.2 NSMallocBlock

在 ARC 情况下,编译器会根据情况自动将栈上的 block 复制到堆上,那么几种情况的 Block 的类型进行 copy?

  1. 匿名函数返回赋值给 block 变量
  2. 将 block 赋值给 __strong 指针时
  3. block 作为 Cocoa API 中方法名含有 usingBlock 的方法参数时
  4. block 作为 GCD API 的方法参数时

大多数情况都是 block 作为函数返回值时进行 copy 操作。 怎么判定是 block 类型是 NSMallocBlock? 需要两个条件:

  • 对匿名的 block 进行 copy(匿名函数 block 赋给 block 变量是一种情况)
  • 捕获到成员变量或者是 __block 声明的局部成员变量至少一种

下面就通过代码来表示一下:

代码语言:javascript
代码运行次数:0
运行
复制
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int var = 30; //局部变量
        __block int block_var = 40; //加block局部变量
        /*
            捕获局部变量、block局部变量至少一种
            匿名block赋给myBlock
        */
        void (^myBlock)(int, int) = ^(int a , int b) {
            NSLog(@"var:%d",var);
            NSLog(@"block_var:%d",block_var);
        };
        var = 31;
        block_var = 41;
        NSLog(@"blockType:%@",[myBlock class]);
        return 0;
   }
    return 0;
}

打印结果如下:

代码语言:javascript
代码运行次数:0
运行
复制
blockType:__NSMallocBlock__
var:30
block_var:41

打印结果表明:

  • block 的类型是 NSMallocBlock 类型
  • 成员变量在 block声明后去改 31,执行 block 发现值还是 30.
  • __block 在 block 声明后去改 41,执行 block 发现值变成 41.

那么为什么声明成员变量后改变不了,__block 声明的成员变量却可以?

下面我们通过底层转换成 c++ 代码来看一下为啥,因为上面已经写了 block底层的整体流程:

代码语言:javascript
代码运行次数:0
运行
复制
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

    int var = 30;
    __attribute__((__blocks__(byref))) __Block_byref_block_var_0 block_var = 
    {(void*)0,(__Block_byref_block_var_0 *)&block_var, 0, 
    sizeof(__Block_byref_block_var_0), 40};
    
    void (*myBlock)(int, int) = ((void (*)(int, int))&__main_block_impl_0((void *)
    __main_block_func_0, &__main_block_desc_0_DATA, var, 
    (__Block_byref_block_var_0 *)&block_var, 570425344));
    
    (block_var.__forwarding->block_var)++;
    
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gh_3fkjx11j0zb3jm_f752skdn00000gn_T_main_b19769_mi_5,
    ((Class (*)(id, SEL))(void *)objc_msgSend)((id)myBlock, sel_registerName("class")));
    
    ((void (*)(__block_impl *, int, int))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock, 10, 10);
    }
    return 0;
}

我们声明的成员变量没发生变化,声明 __block 变量变成了

代码语言:javascript
代码运行次数:0
运行
复制
__attribute__((__blocks__(byref))) __Block_byref_block_var_0 block_var = 
{(void*)0,(__Block_byref_block_var_0 *)&block_var, 0, 
sizeof(__Block_byref_block_var_0), 40};

int 类型变成 __Block_byref_block_var_0 类型。__Block_byref_block_var_0 是啥?下面慢慢分析,看一下 void (^myBlock)(int, int) = ^(int a , int b){} 底层c++ 代码:

代码语言:javascript
代码运行次数:0
运行
复制
void (*myBlock)(int, int) = ((void (*)(int, int))&__main_block_impl_0((void *)
__main_block_func_0, &__main_block_desc_0_DATA, var, 
(__Block_byref_block_var_0 *)&block_var, 570425344));

发现捕获的成员变量的值,捕获的 __block 变量类型变成了 &block_var 指针。 最大的变化就是 block_var 变量不再是 int 类型了,block_var 变成了一个指向 __Block_byref_block_var_0 结构体的指针,__Block_byref_block_var_0 结构如下:

代码语言:javascript
代码运行次数:0
运行
复制
struct __Block_byref_block_var_0 {
  void *__isa;
__Block_byref_block_var_0 *__forwarding;
 int __flags;
 int __size;
 int block_var;
};

结构里的结构可以看出:

  • 保存 int block_var 变量
  • 有一个指向 __Block_byref_count_0 实例的指针 __forwarding

再看一下 block 执行函数:

代码语言:javascript
代码运行次数:0
运行
复制
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int var;
  __Block_byref_block_var_0 *block_var; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_staic_var, int _var, __Block_byref_block_var_0 *_block_var, int flags=0) : staic_var(_staic_var), var(_var), block_var(_block_var->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {

    __Block_byref_block_var_0 *block_var = __cself->block_var; // bound by ref
    int var = __cself->var; // bound by copy
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gh_3fkjx11j0zb3jm_f752skdn00000gn_T_main_b19769_mi_3,
    var);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gh_3fkjx11j0zb3jm_f752skdn00000gn_T_main_b19769_mi_4,
    (block_var->__forwarding->block_var));

}
  1. 发现在 block 执行函数里重新声明的 int var 把 __cself(相当于__main_block_impl_0)里的 var 值赋值给它
  2. 发现在 block 执行函数里重新声明的 __Block_byref_block_var_0 * block_var,并把 __cself 的 block_var 地址给了它
  3. 打印 block_var 的值发现变成了 block_var->__forwarding->block_var 说明了结构体内 __forwarding 指针指向还是自己。

为什么要通过 __forwarding 指针完成对 count 变量的读写修改?

这样就保证无论是在栈上还是在堆上,都能通过都 __forwarding 指针找到在堆上创建的 block_var 这个 __main_block_func_0 结构体,通过结构内的 __forwarding 指针以完成对 block_var(第一个 block_var 是 __Block_byref_block_var_0 对象,第二个 block_var 是 int 类型变量)的访问和修改。

通过下面的图可以表明:

下面通过代码来验证一下:

代码语言:javascript
代码运行次数:0
运行
复制
__block int block_var = 10;
NSLog(@"block捕获前地址:%p",&block_var);
        
void (^myblock)(void) =  ^{
          
    NSLog(@"block_var:%d",block_var);
};
        
NSLog(@"block捕获后地址:%p",&block_var);
block_var = 20;
myblock();

打印的结果表明捕获前后的地址是变化的并且值改变了。

代码语言:javascript
代码运行次数:0
运行
复制
block捕获前地址:0x7ffeefbff4e8
block捕获后地址:0x102968608
block_var:20

通过以上可以表明:

  • __block 声明的变量就变成 __Block_byref_block_var_0 结构体对象
  • 在捕获前在栈上,捕获后相当于把 __block 变量 copy 到堆上
  • 捕获后,copy 到堆的结构体和捕获前栈的结构体都包含 __forwarding指针都指向堆结构体 __block 变量的地址

8.3 NSStackBlock

栈上的 block 随时会被销毁,受系统控制。怎么声明 NSStackBlock 类型?

  • 匿名声明的 block
  • 捕获局部变量或者 __block 局部变量至少一种
  • 没有进行 copy 操作
代码语言:javascript
代码运行次数:0
运行
复制
int var = 30; //局部变量
__block int block_var = 40; //加block局部变量

//没有捕获局部变量,所以是NSGlobalBlock类型
NSLog(@"Global Block:%@", [^{NSLog(@"Global Block");} class]);

//因为对其copy,说以是NSMallocBlock类型
NSLog(@"var Copy Block:%@", [[^{NSLog(@"Copy Block:%d",var);} copy] class]);
  
//虽然调用了局部变量,但是对其copy所以依然还是NSMallocBlock类型  
NSLog(@"block_var Copy Block:%@", [[^{NSLog(@"Copy Block:%d",block_var);} copy] class]);

//没有copy,并且只捕获局部变量,所以N是SStackBlock
NSLog(@"var Stack Block:%@", [^{NSLog(@"Stack Block:%d",var);} class]);
 
//没有copy,并且只捕获局部变量,所以N是SStackBlock       
NSLog(@"block_var Stack Block:%@", [^{NSLog(@"Copy Block:%d",block_var);} class]);

打印结果:

代码语言:javascript
代码运行次数:0
运行
复制
Global Block:__NSGlobalBlock__
var Copy Block:__NSMallocBlock__
block_var Copy Block:__NSMallocBlock__
var Stack Block:__NSStackBlock__
block_var Stack Block:__NSStackBlock__

9. 总结

通过这篇文章,作出如下总结:

  1. 全局变量、全局静态变量不被 block 捕获,局部变量、局部静态变量被 block 捕获
  2. 不被 block 捕获或者只捕获局部静态变量的 block 类型是 NSGlobalBlock
  3. 局部变量在 block 内传的是值,局部静态变量传的指针,全局(静态)变量都是直接用
  4. block 是栈类型,对其进行 copy 操作,block 就变成了 NSMallocBlock
  5. __block 声明的变量就变成 __Block_byref_block_var_0 结构体对象,在捕获前是放在栈上的
    1. 栈上 __block 的 __forwarding 指向本身
    2. 栈上 __block 复制到堆上后,栈上 block 的 __forwarding 指向堆上的 block,堆上 block 的 __forwarding 指向本身
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-01-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 网罗开发 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 前言
  • 2. block 是什么
    • 3. 生成对应的 main.cpp 文件
    • 4. 查看 block 内部结构
    • 5. block() 执行内部代码
    • 6. 验证 block 结构体类型
    • 7. 验证 block 是不是对象
  • 8. block 的三种类型基本概念
  • 1. 前言
  • 2. block 是什么
  • block 内部构成(以局部变量为例子来了解整个执行流程)
    • 3. 生成对应的 main.cpp 文件
    • 4. 查看 block 内部结构
    • 5. block() 执行内部代码
    • 6. 验证 block 结构体类型
    • 验证b lock 的本质是 __main_block_impl_0 结构体类型,通过模拟一下 block的内部代码:
    • 7. 验证 block 是不是对象
  • 8. block 的三种类型基本概念
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档