让我们扒一扒block的裤子

block不管是在我们开发中,还是面试中,都是高频出现的,如果我们不跳出来看它,而是沉浸在API或者单纯的注意一些问题,是很难彻底理解他的。

首先,block是什么?
block本质上就是一个对象,里边封装了函数调用以及函数调用的环境,通俗的理解,block中的所有代码,就是一个函数执行的过程,只不过在执行过程中,会有一些额外需要我们注意的点,这些点就是函数调用环境。

static NSString * b = @"10";
  void(^block)(void) = ^{
       b = @"20";
  };
  block();
基本的一些概念就过了

在上边的block执行过程中,函数调用可以理解就一句代码,给静态变量附一个新的值;另外一部分,函数调用环境是怎么理解的呢,通俗点的讲,就是怎么维护和使用外部的这些变量(b),block是一个独立的代码块,可以在任何时候执行,假如函数调用完了,调用栈都收回了,block怎么办,block引用的外部变量怎么办。解决了这些问题,就等于实现了block,彻底理解了block。

下边是block的cpp(^ ^)代码,容易理解一些

这就是上边的block转换成c++的代码
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSString **b;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSString **_b, int flags=0) : b(_b) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

下边是创建这个对象的代码
 void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &b, 570425344));

精简一下(伪代码)
block= __main_block_impl_0(__main_block_func_0, __main_block_desc_0_DATA, &b, 570425344));
传入了一个函数指针,和一个结构体指针,来创建上边的block对象,函数指针就是block要调用的函数,__main_block_desc_0_DATA结构体对象,根据不同的外部变量,对block有不同的初始化

block就是这么一个东西,然后就是考虑函数执行环境,也就是外部变量和自己保存的问题了。
对于外部变量,分为三种,静态变量、全局变量、auto变量(函数内的局部变量)。

那我们从设计的角度来看:我要使用外部变量,还要在不同的时候使用(包括当前函数调用栈销毁的时候),那我怎么使用这三种变量呢,直接给结论
1.全局变量,存在于Mach-o文件的_DATA区,通常理解的gloab区,他是不会随着函数栈销毁而销毁的,所以我们在程序任意地方可以随时拿来使用,那么对待这种变量,不需要做任何处理

2.static 修饰的静态变量,也是贯穿程序的一种变量,但是static有局部静态变量和全局静态变量,在用这种变量的时候,是采用指针传递的方式,上边的例子就是static的形式,我们可以看到,在block中有一个指向指针的指针变量,这个地方留一个问题后边回答,就是block改变外部引用变量的值问题,涉及到__block修饰问题。

3.还有就是普通的auto变量,这种直接就是值传递,什么叫值传递,就是在传递的时候,直接将值拷贝一份,传入到里边,如果我们使用外部的 int a类型的变量,就会拷贝一份a的副本在block中,如果是对象类型的变量,就会拷贝一份他的指针放到block中,这个指针的指向和外部指针的指向是同一块内存区域

完成上述操作,我们就可以在任何时候使用外部传入的变量,在block中这叫做捕获操作。

捕获以后是不是还有其他的问题,我们拷贝一份副本放到block中,我在block中改变这个副本,那么外部变量根本不会变啊,给写代码的人造成困扰。
这个时候__block上场,还是讲原理。


全局变量:不需要任何操作,因为我们访问和操作的时候都是同一个变量
static静态变量:我们接收到了传递进来的指针变量,那么可以直接通过 * p = value的方式,直接改变指针对象的指向。
auto变量:值传递是不行的,所以也要改成指针传递的方式,__block帮我们做了这个操作,通过__block修饰的对象,会生成一个新的对象,这个新的对象会对之前的变量进行引用和操作,我们的block会保存这个新生成对象的指针。有点绕。看代码:



通过__block修饰后新生成的对象
struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age;
};

我们自己的block
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int no;
  NSObject *__weak weakObject;
  __Block_byref_age_0 *age; // by ref  通过指针的方式
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _no, NSObject *__weak _weakObject, __Block_byref_age_0 *_age, int flags=0) : no(_no), weakObject(_weakObject), age(_age->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

其实还是要跳出来看,__block解决的问题其实就是不同指针变量的问题,之前的时候,值捕获,将一个指针变量捕获进来(也就是生成了一个新的指针变量),那么改变这个新的指针变量的指向,与外部无关,现在,我们统一操作__block新生成的对象,通过他来统一改变值,(block外部age再次进行赋值,起始也是操作的这个新生成的对象)。


以上解释都是外部变量的问题,其实还有block自己的问题,当函数调用栈销毁了,block如果也是在这个栈空间,也就被销毁了,所以引出了
NSGlobalBlock
NSStackBlock
NSMallocBlock
这三种block

在MRC时代,区分这三种block,就看有没有引用auto变量和有没有copy操作,如果引用了auto变量,就在stack上,如果stack的block用copy修饰或者进行了copy操作,那么就会到了堆上,还是和引用变量那么理解,这个block,如果我们想让他在以后执行,那么就要把它保存下来,不能调用的时候已经被销毁了。
ARC环境下,系统又做了很多优化,我们将block赋值给一个block的时候、只要引用auto变量、都会被拷贝到堆上

* 作为函数返回值的时候
* 赋值给__strong 指针的时候。(并且访问auto对象)
* block作为Cocoa API 方法参数的时候,含有usingBlock的方法参数。
 NSArray * arr;
    [arr enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    }];
* block作为GCD参数的时候。

自己在哪的问题解决了,还有内存管理的问题,这也是面试经常问到的问题,从上述原理上来说:

iOS中,内存管理都是通过引用计数来管理的,在上述讲过的问题中,我们列举一下,哪些对象的内存管理是需要我们了解。
aoto变量的内存管理
__block修饰后生成的新的对象的内存管理

先说结论,block中的内存管理是通过

__main_block_copy_0
__main_block_dispose_0
这两个函数操作的

栈上的block不需要也不会对变量进行强引用,只有block被拷贝到堆上才会。

  1. 先看auto变量,我们可以通过__weak, __strong 修饰,来告诉block,是否对这个变量进行强引用(也就是retainCount + 1),默认是强引用,然后等block销毁,对外部对象也release一次,这样对象正常释放,但是如果有循环引用的情况,可能就需要__weak修饰了(但是也要注意block嵌套的问题,这个放到最后)。

2.通过__block修饰的对象,我们的block对他都是强引用,然后新生成的对象又对之前的auto变量进行内存管理,管理的原则同第一条。

其他:

1.__forwarding指针的问题,通过__block修饰生成的新的对象,里边会有一个这样的指针指向自己,原因就是,block在拷贝的时候,__block修饰对象,也会被拷贝到堆上,这时候__forwarding指针指向的就是堆上的值,一般取值过程_age - > __forwarding - > age
2 block嵌套问题,防止变量比较早的销毁,需要在block内部使用__strong,对变量强引用,等内部大括号执行完,再release一次,下边是实例代码

- (void)doBlock{
    Model *model = [Model new];
   
    __weak typeof(self) weakSelf = self;
    model.dodoBlock = ^(NSString *title) {

        __strong typeof(self) strongSelf = weakSelf;
        strongSelf.text = title;   
    };
    
    self.model = model;
}

其他细节不断补充...

你可能感兴趣的:(让我们扒一扒block的裤子)