学计算机的那个

不是我觉到、悟到,你给不了我,给了也拿不住;只有我觉到、悟到,才有可能做到,能做到的才是我的.

0%

Block详解

Block的使用

是什么?

块,封装了函数调用以及调用环境的OC对象

Block底层数据结构

Block本质上也是一个OC对象,它内部也有个isa指针
Block是封装了函数调用以及调用环境的OC对象
Block底层数据结构如下图所示:

Block变量捕获机制

为了保证block内部能够正常访问外部变量,block有个变量捕获机制。

  1. 对于全局变量,不会捕获到block内部,访问方式为直接访问
  2. 对于auto类型的局部变量,会捕获到block内部,block内部会自动生成一个成员变量,用来存储这个变量的值,访问方式为值传递。
  3. 对于static类型的局部变量,会捕获到block内部,block内部会自动生成一个成员变量,用来存储这个变量的地址,访问方式为指针传递
  4. 对于对象类型的局部变量,block会连同它的所有权修饰符一起捕获

为什么局部变量需要捕获,全局变量不用捕获呢?

  1. 作用域的原因,全局变量哪里都可以直接访问,所有不用捕获
  2. 局部变量,外部不能直接访问,所以需要捕获
  3. auto类型的局部变量可能会被销毁,其内存会消失,block将来执行代码的时候不可能再去访问那块内存,所以捕获其值
  4. static变量会一直保存在内存中,所以捕获其地址即可

对象类型的auto变量

当block内部访问了对象类型的auto变量时

如果block是在堆上,将不会对auto变量产生强引用

如果block被拷贝到堆上
① block 内部的 desc 结构体会新增两个函数:
copy__main_block_copy_0,函数名命名规范同__main_block_impl_0
dispose(__main_block_dispose_0)
② 会调用 block 内部的 copy 函数
③ copy 函数内部会调用_Block_object_assign函数
_Block_object_assign函数会根据 auto 变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用

如果block从堆上移除
① 会调用 block 内部的 dispose 函数
② dispose 函数内部会调用_Block_object_dispose函数
_Block_object_dispose函数会自动释放引用的 auto 变量(release)

__block

__block修饰符

  1. __block 可以用于解决 block 内部无法修改 auto 变量值的问题;
  2. __block 不能修饰全局变量、静态变量;
  3. 编译器会将 __block 变量包装成一个对象(struct __Block_byref_age_0(byref:按地址传递));
  4. __block 修饰不会修改变量的性质,它还是 auto 变量;
  5. 一般情况下,对被捕获变量进行赋值(赋值!=使用)操作需要添加 __block 修饰符。比如给数组添加或者删除对象,就不用加 __block 修饰;
  6. 在 MRC 下使用 __block 修饰对象类型,在 block 内部不会对该对象进行 retain 操作,所以在 MRC 环境下可以通过 __block 解决循环引用的问题。

__block的__forwarding指针

__block__forwarding指针存在的意义?

为什么要通过 age 结构体里的__forwarding指针拿到 age 变量的值,而不直接 age 结构体拿到 age 变量的值呢?

__block__forwarding是指向自己本身的指针, 为了不论在任何内存位置,都可以顺利的访问同一个 __block 变量。

  1. block 对象 copy 到堆上时,内部的 __block 变量也会 copy 到堆上去。为了防止 age 的值赋值给栈上的 __block 变量,就使用了__forwarding
  2. __block 变量在栈上的时候,__block 变量的结构体中的__forwarding指针指向自己,这样通过__forwarding取到结构体中的 age 给它赋值没有问题;
  3. __block 变量 copy 到堆上后,栈上的__forwarding指针会指向 copy 到堆上的 _block 变量结构体,而堆上的__forwarding指向自己;

这样不管我们访问的是栈上还是堆上的 __block 变量结构体,只要是通过__forwarding指针访问,都是访问到堆上的 __block 变量结构体;给 age 赋值,就肯定会赋值给堆上的那个 __block 变量中的 age

对象类型的auto变量、__block变量内存管理区别

当 block 在栈上时,对它们都不会产生强引用
当 block 拷贝到堆上时,都会通过 copy 函数来处理它们

当 block 从堆上移除时,都会通过 dispose 函数来释放它们

被__block修饰的对象类型

  • __block 变量在栈上时,不会对指向的对象产生强引用
  • __block 变量被 copy 到堆时
    __Block_byref_object_0__block 变量内部会新增两个函数:
    copy(__Block_byref_id_object_copy)
    dispose(__Block_byref_id_object_dispose)
    ② 会调用 __block 变量内部的 copy 函数
    copy 函数内部会调用_Block_object_assign函数
    _Block_object_assign函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于 ARC 时会 retain,MRC 时不会 retain)
  • 如果 __block 变量从堆上移除
    ① 会调用 __block 变量内部的 dispose 函数
    ② dispose 函数内部会调用_Block_object_dispose函数
    _Block_object_dispose函数会自动释放指向的对象(release)

Block类型

block 有 3 种类型,可以通过调用 class 方法或者 isa 指针 查看具体类型,最终都是继承自 NSBlock 类型。

每一种类型的 block 调用 copy 后的结果如下所示:

几个问题

Q:block在给 NSMutableArray 添加或移除对象,需不需要添加 __block?

不需要。

Q:self 会不会捕获到 block 内部?

会捕获。 OC 方法都有两个隐式参数,方法调用者self和方法名_cmd。 参数也是一种局部变量。

Q:_name 会不会捕获到 block 内部?

会捕获。不是将_name变量进行捕获,而是直接将self捕获到 block 内部,因为_name是 Person 类的成员变量,_name来自当前的对象/方法调用者self(self->_name)。

如果使用self.name即调用self的getter方法,即给self对象发送一条消息,那还是要访问到self。self是局部变量,不是全局变量,所以self会捕获到 block 内部。

Q:__block 修饰符使用注意点:

在 MRC 下使用 __block 修饰对象类型,在 block 内部不会对该对象进行 retain 操作,所以在 MRC 环境下可以通过 __block 解决循环引用的问题。

Q:解决在 block 内部通过弱指针访问对象成员时编译器报错的问题:

1
2
3
4
5
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf; //避免执行过程中提前释放
NSLog(@"%d",strongSelf->age);
};

Q:以下代码有问题吗?

1
2
3
4
__block id weakSelf = self;
self.block = ^{
NSLog(@"%p",weakSelf);
};

在 MRC 下,不会产生循环引用;
在 ARC 下,会产生循环引用,导致内存泄漏,解决方案如下。

1
2
3
4
5
6
__block id weakSelf = self;
self.block = ^{
NSLog(@"%p",weakSelf);
weakSelf = nil;
};
self.block();

缺点:必须要调用 block,而且 block 里要将指针置为 nil。如果一直不调用 block,对象就会一直保存在内存中,造成内存泄漏。

参考

OC 底层探索 - Block 详解