浅谈系列-block如何工作的

322 阅读5分钟

导语:今天我们探讨一下block,到底是如何工作的,有些东西可能比较不好理解,但是我认为,清楚几个重点就行了,首先block怎么使用(大家都用过,就不多说),变量如何捕获的,block如何修改外部变量,如何不形成循环引用基本就可以了。有兴趣当然可以深入了解。另外本人知识储备有限,如果有错漏之处,请一定要指正,非常感谢。

block定义

  • block本质上就是OC对象

  • block是封装了函数实现以及函数调用环境的OC对象,本身也是带有isa指针的

block底层结构图如下:

Block_layoutBlock_descriptor
(void*) isa(unsighed long int) reserved
(int) flags(unsighed long int) size
(int) reserved(void *(void *)) copy
(void *(void *, ...)) invoke(void *(void *)) dispose
(strut Block_descriptor *) descriptordescriptor -> Block_descriptor
variables

block变量捕获

  • block内部如果要使用局部变量,首先需要捕获局部变量
block 直接引用block变量捕获
全局变量auto修饰的局部变量(值捕获,内部创建一个新的局部变量)
函数的参数 (包括self,_cmd这两个隐藏参数)
static 修饰的局部变量(捕获指针)

不加修饰符的局部变量,默认就是auto修饰。

方法中的block如果使用了self,或者函数的其他参数,block是会捕获这些变量的。

如果是auto修饰的局部变量,block会直接捕获局部变量的值,在block内部会直接生成一个新的同名变量储存这个局部变量

如果是 static 修饰的局部变量,block会捕获此布局变量的指针

如果是全局变量,无论是不是static修饰的,都不会捕获,直接引用

  • 为什么不直接捕获auto修饰的局部变量的指针呢?

是因为auto修饰的局部变量出了作用域就会销毁,有可能在此局部变量销毁之后调用block,如果直接捕获指针,就会产生野指针错误。而static修饰的局部变量属于静态变量,出了作用域只是不能直接访问,不会销毁,可以通过指针访问。


block类型

block类型存储位置
__NSGlobalBlock__存在数据段区
__NSStackBlock__存在栈区(容易被销毁,要保住copy到堆区)
__NSMallocBlock__存在堆区

__NSGlobalBlock__: 没有捕获auto修饰的局部变量的block就是NSGlobalBlock类型(包括捕获了static修饰的局部变量的block也属于NSGlobalBlock)

__NSStackBlock__:捕获auto修饰的局部变量的block就是NSStackBlock类型

__NSMallocBlock____NSStackBlock__的block调用copy就会生成 __NSMallocBlock__类型的block

block的copy

  • 在ARC自动管理内存模式中,只要有强指针指向block,会自动调用copy操作
  • block作为函数返回值的时候也会调换用copy操作
  • block作为Cocoa API中方法名含有usingBlock的方法参数时
  • block作为GCD API的方法参数时
//MRC下block属性的建议写法
@property (copy, nonatomic) void (^block)(void);

//ARC下block属性的建议写法
@property (strong, nonatomic) void (^block1)(void);
@property (copy, nonatomic) void (^block2)(void);

对象类型的auto变量

  • 如果block是在栈上,无论某个对象的auto变量是否是强指针,block也不会产生强引用(因为block随时会被销毁).

  • 如果block被copy到了堆上

  1. 会调用block内部的copy函数
  2. copy函数内部会调用_Block_object_assign函数
  3. _Block_object_assign函数会根据auto变量的修饰符(__strong__weak__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用

总之,block内部会根据auto变量的修饰符做出反应,形成强引用或者弱引用

  • 被copy的block内部会新增copy,dispose两个函数

copy 栈上的函数copy到堆上

dispose 堆上的block被废弃时调用


__block修饰符

__block修饰变量

block内部默认无法修改外面的局部变量的。

只有被__block修饰的局部变量就可以在block内部修改。

__block修饰符只能用来修饰auto修饰的变量。

  • __block的改值原理是:经过__block修饰的auto变量,编译器会把这个变量包装成一个对象(之前的auto变量会成为这个对象中的一个成员变量),通过地址指针修改变量值。

  • 内存管理: 原则上跟对象类型的auto的内存管理一样,如果block在栈上,不会对__block修饰的变量强引用,如果block被copy到堆上,会对__block修饰的变量强引用。

  • 当block在堆上被移除时,会调用block内部的

dispose->_Block_object_dispose->自动释放__block修饰的变量

总结:通过__block修饰的变量可以看做一个对象,因此可以通过指针来修改变量值。

__block修饰对象

  • 如果__block变量在栈上,不会对指向的对象产生强引用。

  • __block变量被copy到堆时

会调用__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循环引用的问题。

最经典的循环引用就是,block作为一个属性被对象强引用,而block内部又使用了self,block又强引用了对象。

通常使用 __weak (__weak typeof(self) weakSelf = self),__unsafe_unretained来解决这个问题。

其实我们还可以使用__block来解决循环引用的问题,不过一定要调用block。(知道__block也有这个作用就好,开发中应该使用__weak)。

循环引用这个问题,网上非常多资料,在此就不多说了。

  • 有些资料是引用了网上的,如果有不明白或者有误可以直接指出,我们一起探讨,感谢。

浅谈系列-OC对象创建出来到底是怎么样的呢

浅谈系列-OC方法调用到底是个什么流程

浅谈系列 - KVO&KVC到底是怎么样实现的

浅谈系列-分类的结构和加载时机

浅谈系列-load 和 initialize的调用时机和实际运用