weak-strong dance 简介
使用 Block 时可以通过__weak来避免循环引用已经是众所周知的事情:
// OCClass.m
__weak typeof(self) weakSelf = self;
self.handler = ^{
[weakSelf copy];
};
这时handler持有 Block 对象,而 Block 对象虽然捕获了weakSelf,延长了weakSelf这个局部变量的生命周期,但weakSelf是附有__weak修饰符的变量,它并不会持有对象,一旦它指向的对象被废弃了,它将自动被赋值为nil。在多线程情况下,可能weakSelf指向的对象会在 Block 执行前被废弃,这在上例中无伤大雅,只会输出Self is nil,但在有些情况下(譬如weakSelf作为 KVO 的观察者被移除时)就会导致 crash。这时可以在 Block 内部再持有一次weakSelf指向的对象,延长该对象的生命周期,保证在block执行期间,weakSelf指向的对象不被释放,这就是所谓的 weak-strong dance:
__weak typeof(self) weakSelf = self;
self.handler = ^{
typeof(weakSelf) strongSelf = weakSelf;
[strongSelf.obserable removeObserver:strongSelf
forKeyPath:kObservableProperty];
};
typeof(weakSelf) strongSelf = weakSelf
这一句等于__strong typeof(weakSelf) strongSelf = weakSelf
,在 ARC 模式下,id 类型和 OC 对象类型默认的所有权修饰符就是__strong
,所以是可以省略的。
问题
上面就是对 weak-strong dance 的扫盲级描述。不知道大家怎么想,反正我刚听说这个东西的时候,是有几个疑惑的:
1、self指向的对象已经被废弃的情况下,_handler成员变量也不存在了,在 ARC 下会自动释放它指向的 Block 对象,这个时候 Block 对象应该已经没有被变量所持有了,它的引用计数应该已经为0了,它应该被废弃了啊,为什么它还能继续存在并执行?
2、本来在 Block 内部使用weakSelf就是为了让 Block 对象不持有self指向的对象,那在 Block 内部又把weakSelf赋给strongSelf不就又持有self对象了么?又循环引用了?要解决以上疑惑,需要对 ARC、Block、GCD 这些概念有比较深入的了解,主要是要清楚 Block 的实现原理。
3、weak-strong dance它真的能解决在多线程下,可能 weakSelf 指向的对象会在 Block 执行前被废弃而导致的问题吗?
Block 是 C 语言的扩展功能,支持 Block 的编译器会把含有 Block 的代码转换成一般的 C 代码执行。之前我一直有用到“Block 对象”这个词,因为一个 Block 实例就是一个含有“isa”指针的结构体,跟一般的 OC 对象的结构是一样的:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __xx_block_impl_x {
struct __block_impl impl;
// ...
};
所以跟一般的 OC 对象一样,这个isa指针也指向该 Block 实例的类型结构体(类对象,也有叫单件类的),Block 有三种类型:
_NSConcreteStackBlock
_NSConcreteGlobalBlock
_NSConcreteMallocBlock
这三种 Block 类的实例设置在不同的内存区域,_NSConcreteStackBlock 的实例设置在 stack 上,_NSConcreteGlobalBlock 的实例设置在 data segment(一般用来放置已初始化的全局变量),_NSConcreteMallocBlock 的实例设置在 heap。如果 Block 在记述全局变量的地方被设置或者 Block 没有捕获外部变量,那就生成一个 _NSConcreteGlobalBlock 实例。其它情况都会生成一个 _NSConcreteStackBlock 实例,也就是说,它是在栈上的,所以一旦它所属的变量超出了变量作用域,该 Block 就被废弃了。而当发生以下任一情况时:
1、手动调用 Block 的实例方法copy
2、Block 作为函数返回值返回
3、将 Block 赋值给附有__strong修饰符的成员变量
4、在方法名中含有usingBlock的 Cocoa 框架方法或 GCD 的 API 中传递 Block
如果此时 Block 在栈上,那就复制一份到堆上,并将复制得到的 Block 实例的isa指针设为 _NSConcreteMallocBlock:
imply.isa = &__NSConcreteMallocBlock;
而如果此时 Block 已经在堆上,那就把该 Block 的引用计数加1。
解答疑惑一
说到这里,已经可以回答上文的第一个疑惑了。把 Block 赋值给self.handler的时候,在栈上生成的 Block 被复制了一份,放到堆上,并被_handler持有。而之后如果你把这个 Block 当作 GCD 参数使用(比较常见的需要使用 weak-strong dance 的情况),GCD 函数内部会把该 Block 再 copy 一遍,而此时 Block 已经在堆上,则该 Block 的引用计数加1。所以此时 Block 的引用计数是大于1的,即使self对象被废弃(譬如执行了退出当前页面之类的操作),Block 会被 release 一次,但它的引用计数仍然大于0,故而不会被废弃。
捕获对象变量
Block 捕获外部变量其实可分为三种情况:
1、捕获变量的瞬时值
2、捕获__block变量
3、捕获对象
前两种情况跟今天的主题关系不大,先按下不表。第三种情况,也就是本文所举例子的情况,如果不用__weak,而是直接捕获self的话,代码大概是这个样子:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __xx_block_impl_y {
struct __block_impl impl;
OCClass *occlass; // 对象型变量不能作为 C 语言结构体成员,可能还需要做一些类型转换,而且真实生成的代码并不一定叫 occlass,领会精神……
// ...
};
static void __xx_block_func_y(struct __xx_block_impl_y *__cself) {
OCClass *occlass = __cself -> occlass;
// ...
}
也就是说,表示 Block 实例的结构体中会多出一个OCClass类型的成员变量,它会在结构体初始化时被赋值。而结构体中的函数指针void *FuncPtr显然是用来存放真正的 Block 操作的,它会在结构体初始化的时候被赋值为__xx_block_func_y,__xx_block_func_y以表示 Block 对象的结构体实例为参数,从而得到occlass这个对象(即被捕获的self)。显然,这里会导致循环引用,而使用了__weak之后,表示 Block 对象的结构体中的成员变量occlass也将附有__weak修饰符:
__weak OCClass *occlass;
顺便说一下,__weak修饰的变量不会持有对象,它用一张 weak 表(类似于引用计数表的散列表)来管理对象和变量。赋值的时候它会以赋值对象的地址作为 key,变量的地址为 value,注册到 weak 表中。一旦该对象被废弃,就通过对象地址在 weak 表中找到变量的地址,赋值为 nil(可能是通过对象的析构函数),然后将该条记录从 weak 表中删除。
那当我们使用 weak-strong dance 的时候是怎么个情况呢,会再次持有对象从而造成循环引用么?代码大致如下:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __xx_block_impl_y {
struct __block_impl impl;
__weak OCClass *occlass;
// ...
};
static void __xx_block_func_y(struct __xx_block_impl_y *__cself) {
OCClass *occlass = __cself -> occlass;
// ...
}
解答疑惑二
__weak是个神奇的东西,每次使用__weak变量的时候,都会取出该变量指向的对象并 retain,然后将该对象注册到 autoreleasepool 中。通过上述代码我们可以发现,在__xx_block_func_y中,局部变量occlass会持有捕获的对象,然后对象会被注册到 autoreleasepool。这是延长对象生命周期的关键,但这不会造成循环引用,当函数执行结束,变量occlass超出作用域,过一会儿(一般一次 RunLoop 之后),对象就被释放了。所以 weak-strong dance 的行为非常符合预期:延长捕获对象的生命周期,一旦 Block 执行完,对象被释放,而 Block 也会被释放(如果被 GCD 之类的 API copy 过一次增加了引用计数,那最终也会被 GCD 释放)。
额外好处
上文说了每使用一次_weak变量就会把对象注册到 autoreleasepool 中,所以如果短时间内大量使用_weak变量的话,会导致注册到 autoreleasepool 中的对象大量增加,占用一定内存。而 weak-strong dance 恰好无意中解决了这个隐患,在执行 Block 时,把_weak变量(weakSelf)赋值给一个临时变量(strongSelf),之后一直都使用这个临时变量,所以_weak变量只使用了一次,也就只有一个对象注册到 autoreleasepool 中。
解答疑惑三
先给出结论:在多线程下,如果weakSelf 指向的对象在 Block 执行前被废弃,Weak-Strong-Dance不能帮上任何忙!
通过解答疑惑二,我们知道block捕获外部__weak修饰的对象时,block内部实现代码如下:
static void __xx_block_func_y(struct __xx_block_impl_y *__cself) {
((id (*)(id, SEL))(void *)objc_msgSend)((id)__cself->weakSelf, sel_registerName("copy"));
}
那么如果用了Weak-Strong-Dance呢?
self.block = ^{
__strong typeof(self) strongSelf = weakSelf;
[strongSelf copy];
};
看看clang改写后会有什么区别:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __xx_block_impl_y {
struct __block_impl impl;
__weak OCClass *occlass;
// ...
};
static void __xx_block_func_y(struct __xx_block_impl_y *__cself) {
OCClass *const __weak weakSelf = __cself->weakSelf; // bound by copy
__attribute__((objc_ownership(strong))) typeof(self) strongSelf = weakSelf;
((id (*)(id, SEL))(void *)objc_msgSend)((id)strongSelf, sel_registerName("copy"));
}
区别在于在 block 内多了这么一行代码
__attribute__((objc_ownership(strong))) typeof(self) strongSelf = weakSelf;
所以持有 self 的行为是在 block 执行的时候才发生的!
回过头来看看问题:它真的能解决在多线程下,可能 weakSelf 指向的对象会在 Block 执行前被废弃而导致的问题吗?
在执行前就废弃,到了执行的时候,weakSelf 已经是 nil 了,此时执行 __strong typeof(self) strongSelf = weakSelf;根本没意义吧。
所以如果需要在block中remove KVO的监听,Weak-Strong-Dance不能帮上任何忙!。只要在执行__strong typeof(self) strongSelf = weakSelf;
前,对象在其他线程被废弃了,该crash还是继续crash。
总结
Weak-Strong-Dance并不能保证 block所引用对象的释放时机在执行之后, 更安全的做法应该是在 block 内部使用 strongSelf 时进行 nil检测,这样可以避免上述情况。