虽然之前有过做iOS的开发,但是并不熟练,有很多问题没有搞清楚,今天遇到循环引用问题,就研究了一下,搞明白了很多以前模糊的问题。
环境
- ARC
- Xcode 7
引用方式
我们一般使用的引用方式有strong
、weak
、assign
、copy
,它们的区别如下。
strong
: 强引用,会导致引用计数器+1。
weak
: 弱引用,不会增加引用计数器,当引用计数器为0的时候会置为nil,防止BAD_ACCESS
。
assign
: 和weak
类似,一般用于基本数据类型,引用计数器为0时不会置为nil,如果对引用类型使用assign
容易引发BAD_ACCESS
。
copy
: 复制一份引用计数器为1的副本。
循环引用
简单来说,循环引用就是你持有我,我持有你,你和我的引用计数都是1,谁都无法释放。这种情况一般在delegate
上出现,解决办法就是使用weak
,比如UITableView
的delegate
就是weak
。
@property (nonatomic, weak, nullable) id <UITableViewDelegate> delegate;
一般我们会将ViewController
实现UITableViewDelegate
协议,我们会写类似下面这样的代码。
self.tableView.delegate = self;
如果UITableView
的delegate
是strong
,就会造成循环引用,self
持有tableView
,tableView
持有self
。
我的发现
既然用weak
就可以解决问题,那么我们自己写这种dalegate
的时候都用weak
好了,我之前就是这么认为的,之前不太懂背后这些引用balabala的东西,以为就用weak
就万事大吉了,然而遇到一个bug让我发现自己太年轻,我用命令行程序简单模拟一下当时的�情况。
有一个Task
类,用来处理一个耗时任务,它有一个delegate
属性,用于任务结束后回调给调用者告知任务执行完成了,这里delegate
使用了weak
修饰。Task
的start
方法就是开启一个子线程,去执行一个耗时任务,这里模拟就sleep了两秒钟,然后调用delegate
任务完成。
@protocol TaskDelegate <NSObject>
- (void)taskCompleted;
@end
@interface Task : NSObject
@property (nonatomic, weak) id<TaskDelegate> delegate;
- (void)start;
@end
@implementation Task
- (void)start {
// 开启子线程执行耗时任务
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[thread start];
}
- (void)run{
// 模拟耗时任务,sleep两秒钟
[NSThread sleepForTimeInterval:2];
// 任务执行完毕,调用delegate任务完成
[self.delegate taskCompleted];
}
@end
这里还会牵扯到另外一个对象,我这里给类的命名叫Handler
,它实现了TaskDelegate
协议,它的handle
方法会调用Task
,并处理Task
执行完成后的结果。
@interface Handler : NSObject <TaskDelegate>
@property (nonatomic, strong) Task *task;
- (void) handle;
@end
@implementation Handler
- (instancetype)init
{
self = [super init];
if (self) {
// 创建Task
_task = [[Task alloc] init];
_task.delegate = self;
}
return self;
}
- (void)handle {
// 启动Task
[_task start];
}
- (void)taskCompleted {
// Task执行完毕回调
NSLog(@"task completed.");
}
@end
Handler
在taskCompleted
方法中打印一行日志,算是在Task
执行完成之后的处理。
OK,我们在main
方法中这样使用。
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 创建Handler
Handler *handler = [[Handler alloc] init];
// 执行handler方法
[handler handle];
}
[NSThread sleepForTimeInterval:5];
return 0;
}
因为Task
中是在子线程执行,这里sleep了5秒确保Task
执行完毕。
我想象中的输出应该是这样的。
task completed.
Program ended with exit code: 0
运行之后会在2秒后输出task completed
,最后输出exit
表示程序执行结束。但实际上是这样的。
Program ended with exit code: 0
WTF!只看到了exit
。
当我查看了一下相关资料,问问了朋友之后,确定了原因,因为Task
的delegate
是weak
,也就是不会对handler
的引用计数+1,确实是这样,因为不是为了避免循环引用嘛,但是,handler
是一个局部变量,当@autoreleasepool
执行完之后,handler
会被release
,然而其他地方也没有对handler
引用,那么就*了狗了,handler
直接被回收了,当Task
执行完回调deleagte
的时候是对一个nill
的调用,当然OC里不会有空指针哈,所以就没有反应。这里回到刚开始说引用类型那里,weak
会在引用计数器为0的时候置为nil,而assign
不会,也就是如果换成assign
那么会出现BAD_ACCESS
,这个我测试确实如此。回过来,怎么解决这个问题,既然weak
不好使,我就索性换成strong
试一下吧。
@interface Task : NSObject
@property (nonatomic, strong) id<TaskDelegate> delegate;
@end
OK,我们在运行一下,输出如下。
task completed.
Program ended with exit code: 0
这下输出正常了,但是循环引用问题呢,为了测试我在dealloc
中加了一行日志。
// Task 中
- (void)dealloc {
NSLog(@"�task dealloc");
}
// Handler 中
- (void)dealloc {
NSLog(@"handler dealloc");
}
运行之后输入结果同上,只有task completed
,并没有输出dealloc
的日志,说明这里确实循环引用了,导致谁都释放不了,为了确定是strong
导致了,我更改为weak
跑了一遍,输入如下。
handler dealloc
task dealloc
Program ended with exit code: 0
dealloc
确实打印出来了,task completed
没有打印出来,这里有个地方要注意,handler dealloc
打印的很快,也就是在@autoreleasepool
结束后就执行了,task dealloc
会在2秒后执行,因为子线程会持有task
,所以子线程执行完毕之后才会release
。
解决方法
我想到了两种方法解决。
- 用
weak
,让别人持有handler
,不要搞成局部的。 - 用
strong
,会循环引用,当Task
执行完毕后我手动置为nil
,打破循环引用,也就是在Handler
中的taskCompleted
中讲任何一方的置为nil
即可。
我在项目中选择了第2中方式,因为如果让别人持有handler
,那么handler
就依赖于别人的生命周期,而别人的生命周期可能会很长,我想让它干完活就滚,所以第一种不太适合。
首先Task
的delegate
是strong
修饰的。
@interface Task : NSObject
@property (nonatomic, strong) id<TaskDelegate> delegate;
@end
然后在Handler
的taskCompleted
中加入手动置为nil
。
- (void)taskCompleted {
NSLog(@"task completed.");
// _task.delegate = nil;
_task = nil;
}
这里可以把_task
置为nil
,也可以把_task
的delegate
也就是handler
置为nil
。
OK,最后运行结果如下。
task completed.
handler dealloc
task dealloc
Program ended with exit code: 0
最后,完美解决了这个问题,我对OC中的引用相关概念有多了一点理解。
如有任何错误,希望能指出。