起因
Bugly 上出现了一个崩溃日志 SIGSEGV/SEGV_ACCERR。
分析
一个内存非法引用问题,看了下堆栈,崩溃时最后一行代码是:
self.lastBestNode.focused = NO;
怎么属性访问还能出现非法内存呢?非法内存访问最常见的就是访问已经释放了的对象的指针,看上面这句话其实是两个调用:
node = self.lastBestNode;
node.focused = NO;
于是先检查了一下 lastBestNode
和 focused
这两个属性的定义情况:
focused
属性比较简单,就是一个 BOOL 属性,直接用的 @synthesize focused;
生成 getter 和 setter,应该不会有什么问题。
@property (assign, nonatomic) BOOL focused;
lastBestNode
有点不寻常了,它是在 category 中定义的,使用了 runtime 中的 objc_getAssociatedObject
和 objc_setAssociatedObject
。
- (SCNNode<RadarObjectNode> *)lastBestNode {
SCNNode<RadarObjectNode> *node = objc_getAssociatedObject(self, _cmd);
return node;
}
- (void)setLastBestNode:(SCNNode<RadarObjectNode> *)bestNode {
objc_setAssociatedObject(self, @selector(lastBestNode), bestNode,
OBJC_ASSOCIATION_ASSIGN);
}
定睛一看,原来 AssociationPolicy 设置为了 OBJC_ASSOCIATION_ASSIGN
,也就是 weak 的含义,大概就是这里的问题了。至于这里为啥子要设置为 weak,是谁干的,经过 git blame,发现是当年还是小菜鸟的我寄几😭。
weak 修饰的变量和属性有一个特点,当指向的对象被释放后,它的值会自动更新为 nil
。因此就理(mei)所(you)当(si)然(kao)地以为直接使用 runtime AssociatedObject 相关方法也能达到这个效果……
于是先面向百度和谷歌搜索一番,得到的答案都是没有自动设置为 nil
的效果。
https://nshipster.com/associated-objects/
Weak associations to objects made with OBJC_ASSOCIATION_ASSIGN are not zero weak references, but rather follow a behavior similar to unsafe_unretained, which means that one should be cautious when accessing weakly associated objects within an implementation.
验证
目标:使用 OBJC_ASSOCIATION_ASSIGN 设置的关联并没有对象释放后自动设置为 nil
的功能。
- 创建一个空的 iOS 项目。
- 新建一个类
MyObject
,重写 dealloc 方法,方便打印 log 查看什么时候被释放了。 - 在 ViewController 里定义一个属性:
@property (nonatomic, strong) MyObject *object;
- 在
viewDidLoaded
中初始化一下这个属性
self.object = [[MyObject alloc] init];
- 接着,使用
OBJC_ASSOCIATION_ASSIGN
类型关联到 ViewController。
objc_setAssociatedObject(self, key, self.object, OBJC_ASSOCIATION_ASSIGN);
- 加两个按钮:一个释放 self.object,另一个使用
objc_getAssociatedObject
读取。
// 释放
self.object = nil;
// 读取
objc_getAssociatedObject(self, key);
运行:点击读取按钮,根据打印 log 正常读取到了值。先释放再读取,崩了。
解决
两种方案:
- 直接改成
OBJC_ASSOCIATION_RETAIN_NONATOMIC
使用强引用。 - 使用 weak 关键字来保证释放后自动设为
nil
。
使用哪个取决于是否应该持有对象,也就是强引用对象。第一种方案很简单,改下参数就行了,因为持有这个对象也是合理的,因此实际项目中用的这个简单方法改的。下面说一下第二种方案:
如何利用 weak 关键字实现关联对象的自动释放。
俗话说得好,没有添加中间层解决不了的问题,恩,我们来自定义一个中间层对象,就叫它 Wrapper
吧。
@interface Wrapper : NSObject
@property (nonatomic, weak) id object;
@end
关联对象时使用 Wrapper
包装一下,这样就可以利用 Wrapper
中的 weak 属性获得释放后设置为 nil
的能力了。
- (MyObject *)object {
MyWrapper *wrapper = objc_getAssociatedObject(self, _cmd);
return wrapper.object;
}
- (void)setObject:(MyObject *)object {
SEL key = @selector(object);
MyWrapper *wrapper = objc_getAssociatedObject(self, key);
if (wrapper == nil) {
wrapper = [[MyWrapper alloc] init];
objc_setAssociatedObject(self, key, wrapper, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
wrapper.object = object;
}
结论
关联对象时 objc_setAssociatedObject
不应该使用 OBJC_ASSOCIATION_ASSIGN
。
OBJC_ASSOCIATION_ASSIGN
关联的对象并不具备“释放后自动设置为 nil
” 的功能。因为基础类型无法进行关联,必须转化为对象类型,而使用 OBJC_ASSOCIATION_ASSIGN
关联的对象又有释放后再访问崩溃的隐患。因此 OBJC_ASSOCIATION_ASSIGN
的使用场景非常少,建议不使用。
发散
为什么 weak 关键字有这么大的魔力,能判断出对象被释放了?
一句话解释:因为有内部的表去记录所有的 weak 引用,释放对象时更新这个表中的数据,weak 引用就知道应该设置为 nil
了