前言
整理一些概念
环境
Xcode 8.2.1
macOS Sierra 10.12.3
是什么
Block objects are a C-level syntactic and runtime feature. They are similar to standard C functions, but in addition to executable code they may also contain variable bindings to automatic (stack) or managed (heap) memory. A block can therefore maintain a set of state (data) that it can use to impact behavior when executed.
Block是个C语言级别的语法与运行时特性。它和函数类似,能执行代码,但更重要的是能绑定堆栈中的变量,通过设置这些变量来改变代码的行为。
用在哪
You can use blocks to compose function expressions that can be passed to API, optionally stored, and used by multiple threads. Blocks are particularly useful as a callback because the block carries both the code to be executed on callback and the data needed during that execution.
Block常被用在多线程中,在应用开发时,大多数情况下不能因为网络请求而阻塞与用户的交互,待响应返回后,又需要根据响应的结果对页面进行调整。在这种场景下,可以让主线程可以提供一个Block到子线程。
优点
- 主线程:实现异步,无需关注网络请求的细节部分,只要考虑请求结果对界面的影响,预先做好处理方式。
- 子线程:负责网络请求,它清楚网络请求何时完成,由它来通知主线程进行页面刷新操作是比较合适的。
而它们的联结点就是Block,主线程提供Block,子线程负责在合适时机调用Block
如果没有Block
因为Block和函数类似,用函数来实现回调功能,同时因为OC是面向对象的编程语言,为了能与其他方法交互,更贴近现实情形,写了如下代码
1 | // 主线程 .m 函数定义 |
如果用Block方式改写,可以将函数的定义与函数参数的传递结合在一起,和匿名函数差不多,因为主线程中并不关心函数的名称,主要还是其中执行的代码,子线程中是要属性引用来执行,还是直接执行,那是子线程需要考虑的事情。而httpResponse是子线程中的变量,被传递到主线程的函数中进行执行了。因此Block和通知,代理等一样都具有传递数据的能力。
这里所说的子线程和主线程只是为了区分文件,不是说有什么主线程的.m。
怎么写
- int是返回值
- ^是声明block的语法
- myBlock是block*变量的名称,它的值为{return num * multiplier;}*
- int num是参数名
- {return num \ multiplier;}是block*的内容
Xcode的block代码块提示为:inlineBlock
作属性
1 | @property (nonatomic,copy) void (^responser)(NSString *res); |
如果觉得不好看,可以typedef定义一个别名类型
1 | typedef void (^Responser)(NSString *); |
作参数
1 | - (void)setReq:(void (^)(NSString *))req { |
1 | - (void)setReq:(Responser)req { |
作返回值
1 | - (void (^)(NSString *))req { |
1 | - (Responser)req { |
Block的类型
属性:是OC的一项特性,用于封装对象中的数据。
- 原子性([no]atomatic)
- 读写权限(readwrite/readonly)
- 内存管理语义(retain,copy,assign,weak,strong,unsafe_unretained)
- 方法名(getter,setter)
strong:为该种属性设置新值时,设置方法会先保留新值并释放旧值,然后再将新值设置上去
copy:与strong类似,但是不保留新值,而是将其”拷贝”。当属性类型为NSString时,常用该特质来保护其封装性
详情可以参考书籍Effective Objective C 2.0 第二章第6条
通过这儿的5道题可知
Block可根据存储的区域分为3种类型:NSGlobalBlock,NSStackBlock,NSMallocBlock
1 | typedef void (^dBlock)(); |
输出结果为
MRC
ARC
目前结论:
不绑定变量时,MRC和ARC*环境下都是NSGlobalBlock类型。
绑定非对象变量的情况下,被绑定的变量都是传值的,在MRC环境下,创建Block默认会存储在栈中,即为NSStackBlock类型,生命周期为函数或方法体内有效,当使用copy修饰的属性对其进行引用或对Block调用copy + autorelease时,将会拷贝一个新的Block到堆中,即为NSMallocBlock类型,生命周期将会被延长。在ARC环境下,~~创建Block默认存储在堆中,即为NSMallocBlock类型~~,当使用copy*修饰的属性对其接收时,因为内存地址没变,所以只进行了浅拷贝。
但是,根据谈Objective-C block的实现中的评论,在ARC环境下照着操作后,输出确实如此。因此又进行了以下的代码测试
1 | id arr; |
输出结果:
不论Block*绑定的是对象还是非对象数据类型,结果都是一样的,直接输出都是NSStackBlock类型的,但是当进行赋值操作后,在上面的情况下,用b3去接收Block类型时,ARC发现这个Block的生命周期变了,在输出后,依然应该存在,所以对其进行拷贝一份到堆中,因此b3类型变为NSMallocBlock*。
通过b3执行输出的地址,也不是原来的地址了,因为非对象数据类型是传值的,那么如果换成对象,会有不同吗?再分别写了如下测试代码:
1 | NSString *d2 = @"123"; |
MRC环境下输出
ARC环境下输出
可以得到结果,对于对象的绑定在MRC和ARC都只是进行了浅拷贝
现在绑定的是局部变量,如果是对象的属性,会有变化吗?
进行如下代码测试:
1 | @property (nonatomic,strong) NSArray *dBTestArr; |
MRC环境输出
ARC环境输出
同样也是对变量进行浅拷贝。
如果是静态变量呢,会有变化吗?
代码
1 | static NSArray *dBTestArr2; |
MRC环境输出
ARC环境输出
变量是浅拷贝的,同时Block*统一都是NSGlobalBlock*,这也很好理解,因为静态变量在内存中是分配在全局区的。
OC没有类属性,但是有关联属性,如果关联到类对象上,用Block进行绑定,会怎么样呢?
代码
1 | #import <objc/message.h> |
MRC环境
ARC环境
同样也是对变量浅拷贝,MRC*下是NSStackBlock,ARC下是NSMallocBlock*
总结一下,大概如下表:
绑定变量类型 | MRC | ARC |
---|---|---|
不绑定 | NSGlobalBlock | NSGlobalBlock |
局部变量 | NSStackBlock,copy*属性赋值修饰拷贝一个NSMallocBlock*对象,非对象传值,对象传址 | NSStackBlock,赋值后拷贝一个为NSMallocBlock对象,非对象传值,对象传址 |
属性 | NSStackBlock,非对象传值,对象传址 | NSStackBlock,赋值后拷贝一个为NSMallocBlock对象,非对象传值,对象传址 |
静态变量 | NSGlobalBlock,变量地址 | NSGlobalBlock,变量地址 |
关联对象 | NSStackBlock,对象传址 | NSMallocBlock,对象传址 |
到这里,先停一下,说明两个问题
在5道练习题中的Example B有
1 | void exampleB_addBlockToArray(NSMutableArray *array) { |
能把Block添加到数组中,那么Block应该是一个对象吧。
在JaviSoto/iOS10-Runtime-Headers工程中,查找发现Block对象的继承关系为
NSStackBlock => __NSStackBlock => NSBlock => NSObject
NSGlobalBlock => __NSGlobalBlock => NSBlock => NSObject
NSMallocBlock => __NSMallocBlock => NSBlock => NSObject
可知OC中的Block确实是一个对象
还有一个小问题就是存在的到底是NSConcreteGlobalBlock,NSConcreteStackBlock,还是NSMallocBlock这种类型的?
在13年发表的,猜测可能苹果对Block应该进行修改了,又根据clang-do
又觉得偏向于NSConcreteGlobalBlock,我暂时认为在底层实际上是NSConcreteGlobalBlock这种的,然后在OC*上表现为NSMallocBlock*
__Block
1 | NSString *d2 = @"123"; |
以上代码将会报Variable is not assignable (missing __block type specifier)
为什么呢?
同样根据clang-doc,里面有这样的说明
因为如果变量不用__block修饰,那么默认在拷贝时,会添加const进行修饰
Block的循环引用
更新: 170402
在 5个动画,理解常用垃圾回收算法的执行过程 中有这样一段话
引用计数算法存在很多问题。最大的一个问题是,它无法解决循环引用问题。这种情况很常见,父对象和反向引用会形成引用环。
原来的
先构造一个简单的循环引用
1 | @property (nonatomic,copy) dBlock dBCycle; |
正常情况下,self是VC,被导航控制器的viewControllers属性持有,而self持有dBCycle这个Block属性,当pop控制器后,self的引用计数为0,被释放,然后dBCycle计数也为0,也能被顺利释放
在上面的循环引用代码中,当pop控制器后,self被dBCycle属性持有,所以不能被回收,dBCycle指向的Block的内存地址,在执行完Block后,因为它的引用计数还是至少为1(self持有),所以也不会被释放,有点类似死锁的概念,双方互相依赖对象。dealloc方法不会被调用。
知道原理之后,关注一下AFNetworking中的success回调Block
进入
1 | - (nullable NSURLSessionDataTask *)GET:(NSString *)URLString |
再进入
1 | - (NSURLSessionDataTask *)GET:(NSString *)URLString |
1 | - (NSURLSessionDataTask *)dataTaskWithHTTPMethod:(NSString *)method |
1 | dataTask = [self dataTaskWithRequest:request |
success回调的Block作参数传递到completionHander中
1 | - (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request |
传递到
1 | - (void)addDelegateForDataTask:(NSURLSessionDataTask *)dataTask |
delegate.completionHandler这block持有了我们传递的success回调的Block
delegate被self(AFHTTPSessionManager)对象的mutableTaskDelegatesKeyedByTaskIdentifier属性持有
大致理解如下
AFHTTPSessionManager_Obj.mutableTaskDelegatesKeyedByTaskIdentifier.delegate.completionHandler = ^ {
succcess()
}
若你在success()中使用了VC的self,因为
1 | + (instancetype)manager { |
AFHTTPSessionManager_Obj等价于局部变量,当方法执行完后,变量会被释放,紧接着Block执行完后,会被回收,对VC的self持有会被释放,当pop后,self对象也会被释放。