02 - Block(基础篇)

block,一个用的熟的不能再熟的东东。但是,被问起block是怎么实现的,为什么要使用___weak,为什么会造成循环引用,__block怎么用,block分为几种。。。我就傻眼了。这次,就好好来分析一下,从本质出发~

1 - Block的声明以及实现

首先block的申明 (返回数据类型)(^block名称)(参数);
如下代码:

@property (copy , nonatomic) void(^blockName)(void);
// 或者如下
void(^tempHandle)(void);

block的实现 ^返回数据类型(参数){具体部分}
如下代码:

^int(int b) {
            return 1;
  };
// 省略返回值和参数代码如下
^ {
            return 1;
  };

2 - Block的本质

根据之前我们学过的方法,我们先创建一个简单的包含block的代码,如下。

#import 

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        bool(^gloadBlock)(int a) = ^bool(int b){
            NSLog(@"--- block");
            return false;
        };
        
        NSLog(@"%d",gloadBlock(1));
    }
    return 0;
}

使用命令指令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp得到对应的cpp文件,main.cpp。
main.cpp中包含了以下代码


关于main函数如下:

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

        bool(*gloadBlock)(int a) = ((bool (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_swf2m0ds52l_g_ly5nz8vxmw0000gn_T_main_24b9e7_mi_1,((bool (*)(__block_impl *, int))((__block_impl *)gloadBlock)->FuncPtr)((__block_impl *)gloadBlock, 1));
    }
    return 0;
}

关于__main_block_impl_0的数据结构:

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_impl的数据结构:

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

关于__main_block_desc_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)};

来总结一下上面的结论。
block,就是一个结构体指针(也是一个oc的对象),它的结构体包含有两个属性implDesc
impl是一个结构体__block_impl,它包含有4个属性,如下表格:

属性名 内容
isa 一个isa指针,关于isa指针,参照第一篇文章
Flags FIXME: 这里未完成
Reserved FIXME: 这里未完成
FuncPtr block包含函数调用以及函数调用环境

3 - Block的变量捕获

我们再来看下面的这个block代码:

#import 

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        int intA = 123;
        bool(^gloadBlock)(int a) = ^bool(int b){
            NSLog(@"--- %d",intA);
            return false;
        };
        intA = 321;

        NSLog(@"%d",gloadBlock(1));
    }
    return 0;
}

对于这个block,输出结果是

2021-02-08 17:20:49.357112+0800 Block_01[29634:735842] --- 123
2021-02-08 17:20:49.357532+0800 Block_01[29634:735842] 0
Program ended with exit code: 0

这个结果,也许和你想的不太一样。为什么不是--- 321
再比较下下面的代码:


int intA;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        intA = 123;
        bool(^gloadBlock)(int a) = ^bool(int b){
            NSLog(@"--- %d",intA);
            return false;
        };
        intA = 321;

        NSLog(@"%d",gloadBlock(1));
    }
    return 0;
}

输出结果是

2021-02-08 17:25:36.922729+0800 Block_01[29653:738966] --- 321
2021-02-08 17:25:36.923093+0800 Block_01[29653:738966] 0
Program ended with exit code: 0

对于这样的问题。我们应该分析下底层代码来找寻答案。


使用命令导出c++的代码分别如下:

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

        int intA = 123;
        bool(*gloadBlock)(int a) = ((bool (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, intA));
        intA = 321;

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_swf2m0ds52l_g_ly5nz8vxmw0000gn_T_main_470d12_mi_1,((bool (*)(__block_impl *, int))((__block_impl *)gloadBlock)->FuncPtr)((__block_impl *)gloadBlock, 1));
    }
    return 0;
}
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        intA = 123;
        bool(*gloadBlock)(int a) = ((bool (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        intA = 321;

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_swf2m0ds52l_g_ly5nz8vxmw0000gn_T_main_aec0d3_mi_1,((bool (*)(__block_impl *, int))((__block_impl *)gloadBlock)->FuncPtr)((__block_impl *)gloadBlock, 1));
    }
    return 0;
}

注意这句代码:

        bool(*gloadBlock)(int a) = ((bool (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, intA));

__main_block_impl_0构造函数后面多了一个参数intA

再这个结构体__main_block_impl_0

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int intA;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _intA, int flags=0) : intA(_intA) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

该结构体中多出了 int intA这个属性,并且构造函数intA(_intA)可知,再创建这个block的时候,将在内部创建一个intA属性,将block外部intA的值传入结构体中。所以,在block之外改变intA的值,并不会改变block内部的intA的值。

而将intA放入全局变量的话,block中并不会存在intA属性。


这就是block的变量捕获~

我们先来看一下block的变量捕获。

变量类型 是否捕获 访问方式
局部变量(auto) 值传递
局部变量(static) 指针传递
全局变量 直接访问

我们来校验一下上面的结论,运行以下代码:

#import 

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        static int intA = 123;
        auto int intB = 123;
        bool(^gloadBlock)(int a) = ^bool(int b){
            NSLog(@"--- %d,%d",intA,intB);
            return false;
        };
        intA = 321;
        intB = 321;

        NSLog(@"%d",gloadBlock(1));
    }
    return 0;
}

输出结果:

--- 321,123
0

我们再来看一下转换成c++的代码吧。

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

        static int intA = 123;
        auto int intB = 123;
        bool(*gloadBlock)(int a) = ((bool (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &intA, intB));
        intA = 321;
        intB = 321;

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_swf2m0ds52l_g_ly5nz8vxmw0000gn_T_main_dfee55_mi_1,((bool (*)(__block_impl *, int))((__block_impl *)gloadBlock)->FuncPtr)((__block_impl *)gloadBlock, 1));
    }
    return 0;
}

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *intA;
  int intB;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_intA, int _intB, int flags=0) : intA(_intA), intB(_intB) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

当局部变量是auto修饰的话(默认为auto),block会进行值捕获。当局部变量是static修饰的话,block会进行指针捕获。self属于auto修饰的局部变量


4 - Block的类型

先看一下下面的代码:

#import 

void (^gloadBlockC)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        static int intA = 123;
        auto int intB = 123;
        
        // GlobalBlock
        void(^globalBlock)(int a) = ^void(int b){
            NSLog(@"--- %d,",b);
        };
        void(^globalBlockB)(void) = ^void(){
            NSLog(@"--- %d,",intA);
        };
        gloadBlockC = ^void(){
            
        };

        NSLog(@"%@",[globalBlock class]);
        NSLog(@"%@",[globalBlockB class]);
        NSLog(@"%@",[gloadBlockC class]);
        NSLog(@"%@",[^void(){
        } class]);
        
        // StackBlock
        NSLog(@"%@",[^void(){
            NSLog(@"%d",intB);
        } class]);

        // MallocBlock
        void(^mallocBlock)(void) = ^void(){
            NSLog(@"--- %d,",intB);
        };
        NSLog(@"%@",[mallocBlock class]);
    }
    return 0;
}

在ARC环境下运行结果为:

NSGlobalBlock
NSGlobalBlock
NSGlobalBlock
NSGlobalBlock
NSStackBlock
NSMallocBlock

在MRC环境下运行结果为:

NSGlobalBlock
NSGlobalBlock
NSGlobalBlock
NSGlobalBlock
NSStackBlock
NSStackBlock

所以,block有3种类型:global、stack、malloc。
为什么第六个block类型在ARC和MRC会不一样?经过测试验证,将__NSStackBlock__做copy后,得到的就是一个__NSMallocBlock__.所以,应该是ARC环境下,NSStackBlock的赋值会自动加入copy操作造成的。

我们总结一下:

block类型 产生的环境 存储区域 copy之后的效果
GlobalBlock block没有访问auto变量 数据区域 类型不变
StackBlock block访问了auto变量 从栈赋值到堆
MallocBlock StackBlock进行了copy 引用计数+1

经查询资料,补充一个知识点:

在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况:

  • block作为函数返回值时
  • 将block赋值给__strong指针时
  • block作为Cocoa API中方法名含有usingBlock的方法参数时
  • block作为GCD API的方法参数时

为了自己来管理block的释放,我们应该将属性的block放入堆中。所以,在ARC下,利用上述第二条特性,block用strong或者copy来修饰。而在MRC下,block就用copy来修饰。


你可能感兴趣的:(02 - Block(基础篇))