(二)iOS--Block浅谈,循环引用

前面我们说了weak和strong的一些作用,自然我们接下来就要聊一下block。Block在我们项目中绝对会用到,如果你没用到,请你到地球外面去好吗。

Block的分类

先来看一段代码

void (^block)(void)=^{
        NSLog(@"111");
    };
    block();
    NSLog(@"----:%@",block);
输出结果为:
2021-03-03 13:59:00.239359+0800 RuntimeDemo[21536:8193293] 111
2021-03-03 13:59:00.239400+0800 RuntimeDemo[21536:8193293] ----:<__NSGlobalBlock__: 0x100600038>
NSString *str=@"hello world";
   void (^firstblock)(void)=^{
       NSLog(@"字符串是:%@",str);
   };
   firstblock();
   NSLog(@"第二个测试字符串:%@",firstblock);
输出结果为:
2021-03-03 14:02:56.687806+0800 RuntimeDemo[21541:8194859] 字符串是:hello world
2021-03-03 14:02:56.687835+0800 RuntimeDemo[21541:8194859] 第二个测试字符串:<__NSMallocBlock__: 0x2832d0bd0>

从上内存的分布上block分为了:
NSGlobalBlock:静态全局
NSMallocBlock:堆block
NSStackBlock: 栈block

Block内部实现

我们使用终端输入clang -rewrite-objc main.m,然后再当前目录下就可以生成一个cpp文件。下面来看一下block内部的实现。
直接滑动到最后,找到了实现的testBlock,上代码。

void testBlock(){
    void (*lhrBlock)(void)=((void (*)())&__testBlock_block_impl_0((void *)__testBlock_block_func_0, &__testBlock_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)lhrBlock)->FuncPtr)((__block_impl *)lhrBlock);
}

这一下子,看的有点懵。我们一步一步的来解锁。
首先,是__testBlock_block_impl_0,这是个什么?
往上看,是不是发现了下面的这段代码

struct __testBlock_block_impl_0 {
  struct __block_impl impl;
  struct __testBlock_block_desc_0* Desc;
  __testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

__testBlock_block_impl_0是一个结构体,而在里面又包含了两个结构体__block_impl和_testBlock_block_desc_0*。

_testBlock_block_desc_0
static struct __testBlock_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __testBlock_block_desc_0_DATA = { 0, sizeof(struct __testBlock_block_impl_0)};

来看看它的成员变量的意义:
reserved:Block版本升级所需的预留区空间,在这里为0
Block_size:从单词意思上都清楚,是block的大小
__testBlock_block_desc_0_DATA 是__testBlock_block_desc_0的一个实例。

__block_impl

isa:指向一个类对象 我们上面说的block三种类型,就是这个指针所对应的对象类型
Flags:block 的负载信息(引用计数和类型信息),按位存储
FuncPtr:指向block执行的函数,也是就block大括号里面的东西。
既然明白了这两个结构体,我们在回过头来看最开始testBlock的那一堆东西。

 void (*lhrBlock)(void)=((void (*)())&__testBlock_block_impl_0((void *)__testBlock_block_func_0, &__testBlock_block_desc_0_DATA));

上面代码不就是定义block

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

这段代码不就是找到FuncPtr,然后执行。


Block函数和属性.png

一张图搞定,现在在看上面的函数是不是就明白了。就清楚调用的原理了
总结:block内部的实现就是通过两个结构体__block_impl和__testBlock_block_desc_0来实现的。先去定义block,然后通过__block_impl下的FuncPtr,调用执行。

截获变量值

我们下来执行下面一段代码

void testBlock(){
    int testnum=10;
    void (^lhrBlock)(void)=^{
        NSLog(@"当前输入数值为:%d",testnum);
    };
    testnum=30;
    lhrBlock();
}

按照从上到下的顺序执行,testnum=30在执行block之前,为毛线打印输出的是10.来看它内部是怎么实现的

struct __testBlock_block_impl_0 {
  struct __block_impl impl;
  struct __testBlock_block_desc_0* Desc;
  int testnum;
  __testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, int _testnum, int flags=0) : testnum(_testnum) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __testBlock_block_func_0(struct __testBlock_block_impl_0 *__cself) {
  int testnum = __cself->testnum; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_n__9j735w697q98_wgfxl0mlrfm0000gq_T_main_41c180_mi_0,testnum);
    }

static struct __testBlock_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __testBlock_block_desc_0_DATA = { 0, sizeof(struct __testBlock_block_impl_0)};
void testBlock(){
    int testnum=10;
    void (*lhrBlock)(void)=((void (*)())&__testBlock_block_impl_0((void *)__testBlock_block_func_0, &__testBlock_block_desc_0_DATA, testnum));
    testnum=30;
    ((void (*)(__block_impl *))((__block_impl *)lhrBlock)->FuncPtr)((__block_impl *)lhrBlock);
}

通过上面的代码发现,在void (lhrBlock)(void)=((void ()())&__testBlock_block_impl_0((void *)__testBlock_block_func_0, &__testBlock_block_desc_0_DATA, testnum));和__testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, int _testnum, int flags=0)中,_testnum是一个数值。我们在定义block之前,int testnum是10.这个数值已经传递到block当中了。之后在去改变testnum和block是半毛钱关系都没有。这就解释了为什么最后打印出来是10.
那我想改变testnum,让block里面也跟着改变。肯定要传入一个它的指针就可以解决。那要怎么做呢。

int testnum=10;

void testBlock(){
    //static int testnum=10;
//    __block int testnum=10;
    void (^lhrBlock)(void)=^{
        NSLog(@"当前输入数值为:%d",testnum);
    };
    testnum=30;
    lhrBlock();
}
static
firstblock_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int *_num, int flags=0) : num(_num) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }

在看num,这传递的是num的指针。所以在num的数值发生变化的时候,block中的num也是变化的。

全局变量
int num = 10;
struct __firstblock_block_impl_0 {
  struct __block_impl impl;
  struct __firstblock_block_desc_0* Desc;
  __firstblock_block_impl_0(void *fp, struct __firstblock_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __blockTest_block_func_0(struct __blockTest_block_impl_0 *__cself) {

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_04_xwbq8q6n0p1dmhhd6y51_vbc0000gp_T_main_1875c6_mi_0,num);
        num = 30;
    }

num并没有作为一个参数出现在block_func_0中,它直接就是全局变量,就可以直接访问

局部auto变量 捕获到block内部 值传递
局部static变量 捕获到block内部 指针传递
全局变量 捕获到block内部 直接访问

我们重点来看下__block
struct __Block_byref_testnum_0 {
  void *__isa;
__Block_byref_testnum_0 *__forwarding;
 int __flags;
 int __size;
 int testnum;
};

struct __testBlock_block_impl_0 {
  struct __block_impl impl;
  struct __testBlock_block_desc_0* Desc;
  __Block_byref_testnum_0 *testnum; // by ref
  __testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, __Block_byref_testnum_0 *_testnum, int flags=0) : testnum(_testnum->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __testBlock_block_func_0(struct __testBlock_block_impl_0 *__cself) {
  __Block_byref_testnum_0 *testnum = __cself->testnum; // bound by ref

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_n__9j735w697q98_wgfxl0mlrfm0000gq_T_main_b918cc_mi_0,(testnum->__forwarding->testnum));
    }
static void __testBlock_block_copy_0(struct __testBlock_block_impl_0*dst, struct __testBlock_block_impl_0*src) {_Block_object_assign((void*)&dst->testnum, (void*)src->testnum, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __testBlock_block_dispose_0(struct __testBlock_block_impl_0*src) {_Block_object_dispose((void*)src->testnum, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __testBlock_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __testBlock_block_impl_0*, struct __testBlock_block_impl_0*);
  void (*dispose)(struct __testBlock_block_impl_0*);
} __testBlock_block_desc_0_DATA = { 0, sizeof(struct __testBlock_block_impl_0), __testBlock_block_copy_0, __testBlock_block_dispose_0};
void testBlock(){

    __attribute__((__blocks__(byref))) __Block_byref_testnum_0 testnum = {(void*)0,(__Block_byref_testnum_0 *)&testnum, 0, sizeof(__Block_byref_testnum_0), 10};
    void (*lhrBlock)(void)=((void (*)())&__testBlock_block_impl_0((void *)__testBlock_block_func_0, &__testBlock_block_desc_0_DATA, (__Block_byref_testnum_0 *)&testnum, 570425344));
    (testnum.__forwarding->testnum)=30;
    ((void (*)(__block_impl *))((__block_impl *)lhrBlock)->FuncPtr)((__block_impl *)lhrBlock);
}

这么一大堆东西,扎心了。还是慢慢看吧。
__testBlock_block_impl_0多出来一个成员变量 __Block_byref_testnum_0 *testnum;,我们看到经过__block修饰的变量类型变成了结构体__Block_byref_testnum_0,__testBlock_block_impl_0多出来一个成员变量__Block_byref_testnum_0 *_testnum;,block捕获的是__Block_byref_testnum_0类型指针。
我们看到__Block_byref_testnum_0是一个结构体,并且有一个isa,因此我们可以知道它其实就是一个对象。同时还有一个__Block_byref_testnum_0 *类型的__forwarding和testnum,testnum我们能猜到就是用来保存变量的值,__forwarding还在懵逼中。

还多出两个方法__testBlock_block_copy_0和__testBlock_block_dispose_0

__testBlock_block_copy_0中调用的是_Block_object_assign,__block变量结构体实例从栈拷贝到堆
__testBlock_block_dispose_0中调用的是_Block_object_dispose,__block变量结构体实例引用计数为0时调用。
这两后面的block内存管理会用到。

__blockTest_block_desc_0

我们可以看到它多了两个回调函数指针copy和dispose,这两个指针会被赋值为__testBlock_block_copy_0和__testBlock_block_dispose_0。

简单的看了block分类,我们在来看block的循环应用问题

Block循环引用
截屏2021-03-03 下午2.38.50.png

就像上面的图中展示,block引用b之后,引用计数+1了。只有在A(block)释放掉的时候,B的引用计数才会-1;但是A什么时候释放呢?它是怎么造成循环引用的的?
先来看一个简单的例子

 self.lhrblock = ^{
        self.str=@"jieddddd";
    };
    self.lhrblock();
同时这个界面还有
-(void)dealloc{
    NSLog(@"销毁了");
}

在退出这个界面的时候,并没有调用dealloc这个方法,没有释放,这是为什么呢?当前界面只有一段代码,只能是它出现问题了。
这是因为block造成了循环引用,lhrblock引用了当前的self,按照我们前面说的,在lhrblock释放的时候,self是不是要realse。但是,lhrblock引用了self,self也引用了block。所以,两边就互相等,一直释放不掉。
生活中遇到这种情况,肯定要有一方去打破它吧。
下面是处理循环引用的方式
使用weak

  __weak __typeof(self) weakSelf = self;
    self.lhrblock = ^{
        weakSelf.str=@"jieddddd";
    };
    self.lhrblock();
打印出
 RuntimeDemo[21593:8213411] 销毁了

这说明调用了dealloc方法,block也被释放了。自然这个相互引用的问题也就不存在了

__block FirstViewController *vc=self;
    self.lhrblock = ^{
        vc.str=@"jieddddd";
        vc=nil;
    };
    self.lhrblock();

当然我们也可以使用__block来修饰, 要注意的是vc=nil;如果这里不设置为nil,它还是不会释放的。原因我们后续看block底层原理的时候就会明白。
下面我们来看第三种解决方法:

typedef void(^LHRfirstblock)(FirstViewController *vc);
self.lhrblock = ^(FirstViewController *vc) {
       vc.str=@"jjjj";
   };
   self.lhrblock(self);
4477+0800 RuntimeDemo[21605:8220341] 销毁了

既然我们要使用str属性,那我们所幸就直接把VC控制前都作为参数。这样最后block也是可以释放的
下面我们再来看一个多个层级的循环引用

 MYTableViewCell *cell=[tableView dequeueReusableCellWithIdentifier:@"123"];
   // __weak __typeof(self) weakSelf = self;
    cell.lhrblock = ^(NSString * _Nullable str) {
//        [weakSelf testwith:str];
        self.str=str;
    };
    return cell;

cell中的block方法,这么写系统不会报循环引用的错误,那么它循环引用了吗?我们发现dealloc方法是没有调用的,说明在这是发生了循环引用。我个人理解的是self强引用了cell,cell-->block,反过来在block中强引用了self,所以造成了循环引用。

Block的内存管理

我们首先来看看Block从栈中复制到堆,的整个过程变化


截屏2021-03-09 上午11.11.43.png

我们来详细解读描述一下上面的过程:
1.当block在栈上时,__block也同样是存储在栈上面的,__block她是被栈上的block持有的
2.当block被复制到堆上时,block要调用__block变量,但是它在栈上啊,超出栈上的block的作用范围了,怎么办?先把它copy到栈上不就解决了嘛,所以接下来就是copy函数内部调用_Block_object_assign函数,__block变量的存储就变成堆了,block函数就可以调用了。
3.这样复制到堆上,就引出释放的问题了。当堆上的block被释放了,就会调用block内部的dispose,dispose函数内部会调用_Block_object_dispose,堆上的__block被释放了

那么在一个__block变量被多个block函数持有时候,过程又是什么样的?
截屏2021-03-09 上午11.24.09.png

1.当多个栈上的block使用栈上的__block变量,__block变量被栈上的多个block函数持有。
2.当block0被复制到堆上是,按照上面讲的,也要把__block复制到堆上,但是现在block1在栈上持有__block,这不是搞事情嘛,假如堆上的__block变量变化,栈上的__block该怎么办呢?原栈上__block变量的__forwarding指向拷贝到堆上之后的__block变量。
3.当block1也被复制到堆上时,堆上的__block被block0和block1持有,引用计数+1.
4 当堆上的block都被释放,__block变量结构体引用计数为0,调用_Block_object_dispose,堆上的__block被释放。

看视频,看其他博客,最后自己理解,总结出了点东西。

你可能感兴趣的:((二)iOS--Block浅谈,循环引用)