Block之回炉学习

353 阅读6分钟

block分类

三种分类:NSGlobalBlock、NSMallocBlock、NSStackBlock

全局NSGlobalBlock

void (^block)(void)  = ^{
    NSLog(@"test block");
};
block();
NSLog(@"%@", block);

堆NSMallocBlock

int a = 10;
void (^block)(void)  = ^{
    NSLog(@"test block - %d", a);
};
block();
NSLog(@"%@", block);

栈NSStackBlock

int a = 10;
NSLog(@"%@", ^{
    NSLog(@"123 - %d", a);
});

带参数的block

还有一种情况,带参数的block,也是全局block

void (^block1)(int) = ^(int a){
    NSLog(@"123 - %d", a);
};
int a = 777
block1(a);
NSLog(@"%@", block1);

block分类小总结

block不捕获自动变量的情况下,是全局block类型。捕获自动变量,变成非全局类型。栈类型的block其实是一个中间产物,当定义一个block变量去接受block时,系统底层会自动将栈block转换成堆block。其实很好理解,如果不转换到堆区,栈区中的数据会在函数作用域结束后,被系统回收掉。所以放到堆区,才会更安全并且有回调的效果。

还有惊喜:系统级别的另外三种block

除了上面我们在平时开发中会遇到的3种block外,还有3种系统底层代码使用的block,分别是 _NSConcreteAutoBlock、 _NSConcreteFinalizingBlock、 _NSConcreteWeakBlockVariable

在libclosure源码中可以看到,源码libclosure-73中的data.c文件中可以看到:

这三种在目前我个人经历中,是没有使用到。所以本人不敢断定,我们普通开发者就不用。所以如果遇到block有几种分类这样的问题,应该回答:

block的分类有六种,我们通常使用的3种+系统底层3种

循环引用

举个简单的例子:

@property (copy, nonatomic) NSString *str;
@property (copy, nonatomic) TestBlock t_block;

self.str = @"hello, block!";
self.t_block = ^{
    NSLog(@"%@", self.str);
};

这是一个简单的循环引用示例,self持有block,block中持有self,行成一个self-block-self的循环链。xcode也有相关提示,本人xcode环境:11.4.1

提示内容:Capturing 'self' strongly in this block is likely to lead to a retain cycle。意思是:在block中捕获到一个强引用的‘self’可能会引起循环引用。

解决方法

解决方法是破坏这层循环链条,可以用以下三种方式,

  1. __weak
//持有关系:weakself-self-block-weakSelf
__weak typeof(self) weakSelf = self;
self.t_block = ^{
    NSLog(@"%@", weakSelf.str);
};

__weak修饰的变量,系统arc会自动帮助开发这来管理相关内存,所以会打破循环链。

  1. 使用中间者
//持有关系:vc-self-block-vc=nil
__block ViewController *vc = self;
self.t_block = ^{
    NSLog(@"%@", vc.str);
    vc = nil;
};

相当于手动断开循环链,在block中强制给中间者赋值nil。

  1. block传参
//持有关系:self-block
self.t_block = ^(ViewController *vc){
   NSLog(@"%@", vc.str);
};
self.t_block(self);

这种方式没有形成循环链。

block分析

问题:

1.block本质是什么

上面的代码例子中,block是可以用%@来打印,这个可以证明:block是一个对象,而OC中对象都是结构体。所以block的本质应该是结构体,接下来我们来验证一下。我们用一个简单的例子来说明一下,创建一个block.c的文件,内容是:

#include "stdio.h"
int main(){
    void(^block)(void) = ^{
        printf("123");
    };
    
    block();
    return 0;
}

使用终端命令clang,将c代码转换成cpp代码:

clang -rewrite-objc block.c

执行命令后会在block.c文件的目录下生成一个block.cpp文件,双击打开文件,文件内容比较多,没关系,直接翻到最下面的main函数,cpp代码中的类型转化很多,看起来不是很友好,可以参考c代码对应的看:

int main(){

    /*
    void(^block)(void) = ^{
        printf("123");
    };
     */
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    
//    block();
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    
    
    return 0;
}

__main_block_impl_0就是block的对应代码类型,'main'代表是哪个函数中的block;后面的数字'0',代表'main'函数中的第几个block的下标(苹果程序员写代码也挺草率的)。cpp代码中可以找到这个类型是如何定义的:

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的本质就是结构体

2.为什么需要block()

我们继续来看cpp文件main函数中block的定义代码

void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

__main_block_impl_0是结构体中定义的构造方法,它需要两个参数,第一个参数是 __main_block_func_0,第二个参数是 __main_block_desc_0_DATA

先来看看第一个参数 __main_block_func_0,它的类型是 **(void *)**说明是一个函数指针,我们来看看他的函数实现:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    printf("123");
}

这就是block内容。而且参考结构体中构造方法的实现

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;//保存函数指针,该函数内容就是block中的内容
    Desc = desc;
  }
};

改指针会保留在impl.FuncPtr中。我们参考main函数中block()的对应代码:

((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

这明显就是函数调用的地方。此处说明block()是函数调用的作用

#include "stdio.h"
int main(){
    void(^block)(void) = ^{//函数声明
        printf("123");
    };
    
    block();//函数调用
    return 0;
}

3.block如何自动捕获外部变量

修改一下block.c文件,增加一个变量a

#include "stdio.h"
int main(){
    int a = 10;
    void(^block)(void) = ^{
        printf("123 - %d", a);
    };
    
    block();
    return 0;
}

执行clang命令生成cpp文件,查看cpp文件中的main函数

int main(){
    //外部变量a
    int a = 10;
    //构造方法多了一个参数a
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));

    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}

查看一下**__main_block_impl_0**结构体

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;//多了一个成员a
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {//构造函数多了一个参数
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

再看看指针函数的实现,注意:此处注释‘bound by copy’是自动生成的

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int a = __cself->a; // bound by copy
    printf("123 - %d", a);
}

这些代码证明,block自动获取外部变量,会在结构体中以值拷贝的方式生成一个成员变量,用于block内部。这也证明了该变量,在block内部修改的是不会影响到外面的。

4.__block修饰的变量为什么可以在block内部修改

同理我们修改一下block.c文件

int main(){
    __block int a = 10;//使用__block修饰变量
    void(^block)(void) = ^{
        a++;
        printf("123 - %d", a);
    };
    
    block();
    return 0;
}

看看生成cpp文件的main函数

int main(){
    __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));

    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}

多了一行似曾相识的代码

__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};

__Block_byref_a_0类型的变量a的赋值,看到赋值等候右侧是被{}包裹着的,那这个类型肯定也是个结构体了

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

再看看block结构体和函数指针的实现,注意'by ref'和'bound by ref'注释是自动生成的,看来苹果的程序员也挺严谨的,哈哈

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__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_a_0 *a = __cself->a; // bound by ref

        (a->__forwarding->a)++;
        printf("123 - %d", (a->__forwarding->a));
    }

其实看到上面的注释应该了解到了,使用__block修饰的变量,是引用的方式放到block中,所以可以实现block内部修改变量的目的。