iOS - 一個崩潰 SIGSEGV / SEGV_ACCERR

起因

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

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容