使用 block 的小技巧和注意事项

时间:2021-06-15 06:31:30
个人比较喜欢使用Block,因为要写个规范的代理就要规定协议方法,觉得略麻烦,所以我很少使用代理;但是使用Block时必须要注意它带来的副作用:
  • 循环引用;
  • 诈尸;

下面解释下 block 引起的循环引用,这个是很好理解的,因为我们应该都是用过代理,都知道把 delegate 描述为 retain(strong)会引起循环引用,block 之所以会引起是因为 block 捕获环境变量引起的, 这一特性有的时候能给编程带来很大的便利性,当然也需要注意带来的副作用,在 block 内部使用到的对象捕获后会被 retain,基本数据类型会进行值拷贝(不是地址拷贝,因此只读哦!),因此如果被捕获的对象持有 block 的话,那么就会造成循环引用!经常发生的场景是一个控制器持有(或者间接持有)(copy)了 block ,并且 block 捕获了该控制器对象 self;

@interface A : NSObject

@property (copy, nonatomic) TypeBlock succBlock;

@end

@interface B : NSObject

@property (retain, nonatomic) A a;

@end

//B.m
- (void)testRetainCycle
{
self.a.succBlock = ^(id obj){
[self handleSuccess:obj];
};
}

如果不是 ARC 编译的话,需要定义一个弱化的指针:

__block B *this = self

因此改为:

- (void)testRetainCycle
{
__block B *this = self
self.a.succBlock = ^(id obj){
[this handleSuccess:obj];
};
}

我比较懒,这种写法每次都要知道类名,我感到不爽,我想要一个通用的写法,无意间找到了 typeof ,所以改写下:

__block typeof(self) this = self;  

[self methodThatTakesABlock:^ {

[this handelSomthing];
}

PS:另外也有人使用 __typeof 的,应该和 typeof 没啥大的区别;在 JAVA 里没有 self ,而是使用 this 关键字表示当前,我是之前学的是 JAVA ,因此沿用了 JAVA 的写法,但是如果你的工程使用了 c++的话,最好换个名字,因为 this 是 c++ 的关键字!

在 ARC 编译环境下 __block 就没用了,需要改为 __weak ! 因此 ARC 下可以这样写:

    __weak __typeof(self)weakSelf = self;

还有人搞了个 weak strong dance 呢:

    __weak __typeof(self)weakSelf = self;
StatusBlock callback = ^(Status status) {
__strong __typeof(weakSelf)self = weakSelf;
self.status = status;
};

这里的技巧是:block内部使用了 __strong 来保证进入block内后 self 不会被释放,并且这里重写定义了 self 就会覆盖掉 block 块捕获之外的 self ,因此在 block 内可继续使用 self,并且不会循环引用!



下面再说下所谓的 诈尸 是什么意思,看下面的场景,我觉得没有场景那是在幻想,毫无意义可言,下面是我在实际编程中遇到的问题,下面简单说下:

//DownloadManger.m
- (void)downTheVideo:(NSString *)vid
{
//鉴定会员权限
__weak __typeof(self)weakSelf = self;
[VipStatus isVip:^(Bool vip){
__strong __typeof(weakSelf)self = weakSelf;
[self startDownloadTheVideo:vid];
};
}

DownloadManger 类调用了 VipStatus 类的 isVip 方法,该方法使用了 block 回调结果,这里应该没什么问题的,这样写是对的,但是

DownloadManger 和 VipStatus 都是单例类,并且 isVip 方法是需要发送网络请求的,所以这个是异步回调的,DownloadManger 维持了下载的状态机,有可能用户停止了下载之后,这个 block 才被唤醒,这时问题就来了,明明用户已经停止了,但是这个 block 诈尸一样开始运行了,结果导致任务继续下载!

我的解决办法是给 DownloadManger 添加一个记录状态的 int 属性,充分利用 block 捕获的特性,当状态机发生变化就改变这个 int 属性,block 回调的时候就判断捕获的 int 值和当前状态机的是否一致,不一致就不再执行!

//DownloadManger.m
- (void)downTheVideo:(NSString *)vid
{
//鉴定会员权限
int currentStatus = self.currentStatus;
__weak __typeof(self)weakSelf = self;
[VipStatus isVip:^(Bool vip){
__strong __typeof(weakSelf)self = weakSelf;
//检查状态机;
if(currentStatus != self.currentStatus) {
return;//block 大爷快躺下休息吧!
}
[self startDownloadTheVideo:vid];
};
}

- (void)pauseAllDownloadTask
{

//改变状态机,防止 block 诈尸!
self.currentStatus ++;
...
}

关于 block 的坑就先填到这里啦,有问题请指正…