iOS - 一个崩溃 SIGSEGV / SEGV_ACCERR

作者: 拾识物者 | 来源:发表于2019-05-08 23:55 被阅读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 正常读取到了值。先释放再读取,崩了。

    解决

    两种方案:

    • 直接改成 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

    相关文章

      网友评论

        本文标题:iOS - 一个崩溃 SIGSEGV / SEGV_ACCERR

        本文链接:https://www.haomeiwen.com/subject/cqxmoqtx.html