Block的原理分析

682 阅读5分钟

我正在参与掘金创作者训练营第4期,点击了解活动详情,一起学习吧!

前言

Block是我们项目经常用到的地方,今天我们就来学习一下关于 Block 的原理,__block 的作用,Block 的循环引用问题。让我们更深刻的了解下 Block 的本质。

Block的类型

全局Block

__NSGlobalBlock__ :全局 Block,位于全局区,且在 block 内部不使用外部变量,或者只使用静态变量或者全局变量。

    static int a = 20;

    void (^gloalblock)(void) = ^{

        NSLog(@"gloalblock: - %d",a);

    };

    gloalblock();
    NSLog(@"%@",gloalblock);

打印如下;

<__NSGlobalBlock__: 0x10bf850b8>

堆Block

__NSMallocBlock__ :堆区 Block ,在 block 内部使用外部变量或者 OC 属性,并且赋值给强引用或者 copy 修饰的变量。

    int b = 10;

    void (^mallocBlock)(void) = ^{

       NSLog(@"mallocBlock - %d",b);

    };

    mallocBlock();

    NSLog(@"%@", mallocBlock);

打印如下:

<__NSMallocBlock__: 0x600000c50210>

栈Block

__NSStackBlock__ :- 与 MallocBlock 一样,可以在内部使用局部变量或者 OC 属性,但是不能赋值给强引用或者 Copy 修饰的变量。

    void (^__weak stackBlock)(void) = ^{
        NSLog(@"stackBlock----%d",b);
    };
    stackBlock();
    NSLog(@"%@", stackBlock);

打印如下:

<__NSStackBlock__: 0x7ff7b3f7a260>

这里只是演示而已,一般我们不这么写。像上面的例子堆Block,在 block 没有 copy 之前,是栈 Block,拷贝之后就变成了堆 Block。

Block底层分析

我们先在main.m里面创建1个block。

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

然后在终端用 clang 生成 main.app 文件。使用方法:clang -rewrite-objc main.m

然后我们打开main.cpp

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; 
        int a = 10;
        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;

}

我们可以发现block 底层会被编译成一个结构体类型 __main_block_impl_0

struct __main_block_impl_0 {

  struct __block_impl impl;

  struct __main_block_desc_0* Desc;

  int 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;

  }
};

其中__block_impl 的结构如下:

struct __block_impl {

  void *isa;

  int Flags;

  int Reserved;

  void *FuncPtr;

};

先分析这一段:

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

从这里看到是在底层block的类型__main_block_impl_0结构体,通过其同名构造函数创建,第一个传入的是__main_block_func_0,用fp表示,传入到impl.FuncPtr = fp中,这个是block的函数代码块。

第二个传的是__main_block_desc_0_DATA,存放在Desc = desc中,是描述信息的函数地址;

第三个直接保存a,可以看到是值拷贝。

那这里的重点就是:block捕获外界变量时,在内部会自动生成同一个属性来保存。如果是静态局部变量,则是指针拷贝,如果是全局变量,则直接访问。

__block

我们先改一下代码。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int a = 10;
        void (^ block)(void) = ^{
            a++;
            NSLog(@"----%d",a);
         };
         block();
    }
    return 0;
}

继续生成main.cpp文件。

struct __Block_byref_a_0 {

  void *__isa;

__Block_byref_a_0 *__forwarding;

 int __flags;

 int __size;

 int a;

};

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;

  }
};

可以看到是,这么我们存的是结构体指针__Block_byref_a_0。,a(_a->__forwarding)&a,也就是说对象 a 的地址,和结构体指针__Block_byref_a_0是一样的。

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)++;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_sk_t102mr_53p70tshnj46v9k880000gp_T_main_760d82_mi_0,(a->__forwarding->a));
         }

这里是通过指针拷贝,此时我们的对象a__cself->a指向的是同一片空间区域。这时候我们进行(a->__forwarding->a)++操作,相当于外面的对象a也进行了++操作。

这里的重点就是:block捕获外界变量时,将变量生成的结构体对象的指针地址传递给block,block内部就可以对外界变量进行操作。

Block的3层copy

只有 __block 修饰的对象,才有3层拷贝。

  1. 通过_Block_copy,将栈区的block拷贝一份到堆区。

  2. __block修饰的对象,通过_Block_byref_copy方法,把对象拷贝为Block_byref结构体类型。

  3. 通过_Block_object_assign方法,对__block修饰的当前变量的拷贝。

循环引用

循环引用:对象之间互相持有,形成一个闭环,导致谁也无法正确释放。

20220223214123.jpg

这里 self 持有 block ,block 又持有着 self ,互相强引用,所以提示循环引用。

__weakself弱引用

- (void)test {

    NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));

    __weak typeof(self) weakself = self;

    NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));

    self.block = ^(void){
        weakself.str = @"你好";
        NSLog(@"%@",weakself.str);
    };

    self.block();
}

打印结果:

20220223214951.jpg

分析:进来此页面后,先打印 2 个 6 ,证明了 weakself 并没有对 self 的引用计数增加 1 , self 持有 block ,block 持有的是 weakself 指针,weakself 并没有强引用 self ,一旦 self 释放后,block 也就会释放了。

换一个例子:

- (void)test {
    NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));

    __weak typeof(self) weakself = self;

    NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));

    self.block = ^(void){
        weakself.str = @"你好";
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

            NSLog(@"%@",weakself.str);

        });
        NSLog(@"%@",weakself.str);
    };

    self.block();

}

直接看打印结果吧:

20220223215814.jpg

和你们想的是一样吗,进来页面后,又马上离开,这时候dispatch_after 还没执行完,这时候调用 weakself , weakself 已经被释放了,所以打印为 null

那我们就添加一个强引用吧。

- (void)test {    
    __weak typeof(self) weakself = self;

    self.str = @"你好";

    self.block = ^(void){

        __strong typeof(self) strongself = weakself;

        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

            NSLog(@"%@",strongself.str);

        });

        NSLog(@"%@",strongself.str);

    };

    self.block();
}

打印一下结果:

20220223220715.jpg

可以看到,我们退出后, 2 秒后依然正常输出,再 dellac 。这是因为我们 strongself 是强引用 self ,会对 self 引用计数 +1 , self 就不能正常释放,等走完 dispatch_after 后,因为 strongself 是局部变量,走完后就会被回收,这时候 self 就可以正常释放了。

__block释放

我们可以添加__block来操作。用 __block 修饰,然后 block 调用完后,置为 nil ,也可以打破循环引用。

- (void)test
{
    __block BViewController*vc = self;

    self.block = ^(void){

        NSLog(@"%@",vc.str);

        vc = nil;
    };

    self.block();

vc 持有self, self 持有 block ,block 持有 vc ,这时候调用完 vc 置为空,就可以打破循环。所以self.block()是一定要执行的,不然 self 被 vc 持有,就释放不了 self 了。

最后

Block的 分析就到这里了,后面有时间,再写一下 weak,为什么 delegate 要用 weak 修饰呢,用 assign 修饰会怎样?

参考资料