起因
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
了