iOS-Developer-OCapp打包测试上线开发工作

iOS - 一个崩溃 SIGSEGV / SEGV_ACCERR

2019-05-08  本文已影响430人  拾识物者

起因

Bugly 上出现了一个崩溃日志 SIGSEGV/SEGV_ACCERR。

分析

一个内存非法引用问题,看了下堆栈,崩溃时最后一行代码是:

self.lastBestNode.focused = NO;

怎么属性访问还能出现非法内存呢?非法内存访问最常见的就是访问已经释放了的对象的指针,看上面这句话其实是两个调用:

node = self.lastBestNode;
node.focused = NO;

于是先检查了一下 lastBestNodefocused 这两个属性的定义情况:

focused 属性比较简单,就是一个 BOOL 属性,直接用的 @synthesize focused; 生成 getter 和 setter,应该不会有什么问题。

@property (assign, nonatomic) BOOL focused;

lastBestNode 有点不寻常了,它是在 category 中定义的,使用了 runtime 中的 objc_getAssociatedObjectobjc_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 的功能。

  1. 创建一个空的 iOS 项目。
  2. 新建一个类 MyObject,重写 dealloc 方法,方便打印 log 查看什么时候被释放了。
  3. 在 ViewController 里定义一个属性:
@property (nonatomic, strong) MyObject *object;
  1. viewDidLoaded 中初始化一下这个属性
self.object = [[MyObject alloc] init];
  1. 接着,使用 OBJC_ASSOCIATION_ASSIGN 类型关联到 ViewController。
objc_setAssociatedObject(self, key, self.object, OBJC_ASSOCIATION_ASSIGN);
  1. 加两个按钮:一个释放 self.object,另一个使用 objc_getAssociatedObject 读取。
// 释放
self.object = nil;
// 读取
objc_getAssociatedObject(self, key);

运行:点击读取按钮,根据打印 log 正常读取到了值。先释放再读取,崩了。

解决

两种方案:

使用哪个取决于是否应该持有对象,也就是强引用对象。第一种方案很简单,改下参数就行了,因为持有这个对象也是合理的,因此实际项目中用的这个简单方法改的。下面说一下第二种方案:
如何利用 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

上一篇下一篇

猜你喜欢

热点阅读