最近遇到一塊兒由objc_setAssociatedObject
和objc_getAssociatedObject
引起的線上Crash事故,在痛心疾首的同時也以爲頗有意思,特此分享。bash
項目中已經存在某個Catagory,會往一個第三方庫的類中掛載一個屬性,用下面代碼的TestCatagory中ssShowTime屬性來表示。markdown
@interface ViewController(TestCategory)
@property (nonatomic, assign) long ssShowTime;
@end
複製代碼
具體的實現是用objc_setAssociatedObject
和objc_getAssociatedObject
方法。oop
@implementation ViewController (TestCategory) - (void)setSsShowTime:(long)ssShowTime { NSNumber *number = @(ssShowTime); objc_setAssociatedObject(self, @selector(ssShowTime), number, OBJC_ASSOCIATION_ASSIGN); } - (long)ssShowTime { NSNumber *number = objc_getAssociatedObject(self, @selector(ssShowTime)); return [number longValue]; } @end 複製代碼
該方法已經跑了好幾個版本,沒有出現過任何問題。 後面在此基礎上又新增一個掛載屬性,咱們用ssLocalDesc來表示。性能
@property (nonatomic, strong) NSString *ssLocalDesc; - (void)setSsLocalDesc:(NSString *)ssLocalDesc { objc_setAssociatedObject(self, @selector(ssLocalDesc), ssLocalDesc, OBJC_ASSOCIATION_ASSIGN); } - (NSString *)ssLocalDesc { NSString *ret = objc_getAssociatedObject(self, @selector(ssLocalDesc)); return ret; } 複製代碼
ssLocalDesc屬性會用來存一些描述,好比說用常量,又或者拼接起來的字符串,以下:測試
self.ssLocalDesc = @"123"; // 或者 int index = 1; self.ssLocalDesc = [NSString stringWithFormat:@"Tag_%d", index]; 複製代碼
一切都正常,直到下面這段代碼出現:ui
self.ssLocalDesc = [NSString stringWithFormat:@"Tag_%d", (int)time(NULL)]; 複製代碼
這個賦值語句執行完以後,再訪問self.ssLocalDesc
屬性就會產生Crash!atom
當問題出現以後,咱們來看看是犯了哪些錯誤,纔會致使問題的出現: ssShowTime 屬性雖然是long,可是內部實現的時候仍是經過NSNumber類來實現,因此這裏不該該使用OBJC_ASSOCIATION_ASSIGN;spa
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
* The association is not made atomically. */
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied.
* The association is not made atomically. */
OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.
* The association is made atomically. */
OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.
* The association is made atomically. */
};
複製代碼
這裏更合適的作法是使用OBJC_ASSOCIATION_RETAIN或者OBJC_ASSOCIATION_RETAIN_NONATOMIC。.net
ssLocalDesc屬性是字符串,字符串一般使用strong或者copy,那麼這裏使用OBJC_ASSOCIATION_ASSIGN自己就是錯誤的。 OBJC_ASSOCIATION_ASSIGN一般是爲了不循環引用而添加,不會對引用計數產生變化。3d
當解決完這個問題以後,咱們發現crash出現以前,有幾個延伸問題: 問題1:爲何ssShowTime這個屬性在運行過程當中不會Crash? 咱們知道Crash是因爲OBJC_ASSOCIATION_ASSIGN不會引用計數加1,致使對象被釋放出現野指針的狀況。那麼咱們在number對象掛載以前,看下對象的引用計數。
- (void)setSsShowTime:(long)ssShowTime { NSNumber *number = @(ssShowTime); objc_setAssociatedObject(self, @selector(ssShowTime), number, OBJC_ASSOCIATION_ASSIGN); } 複製代碼
結果很是意外,引用計數的值很是大。
(lldb) p CFGetRetainCount(number) (CFIndex) $0 = 9223372036854775807 複製代碼
若是排除掉引用計數出錯的可能,咱們能夠理解爲何number對象不會被釋放。
問題2:爲何ssLocalDesc這個屬性在測試不會Crash,而在線上運行會出現Crash? 針對ssLocalDesc屬性,我構造了三種狀況:
self.ssLocalDesc = @"123"; 複製代碼
結果以下圖,引用計數也很大;字符串類型爲常量字符串, 隨着App運行就建立,退出時才銷燬。
int index = 1; self.ssLocalDesc = [NSString stringWithFormat:@"Tag_%d", index]; 複製代碼
結果以下圖,引用計數仍很大;字符串類型爲TaggedPointerString,這是標籤指針類型的字符串,把指針當作字符串對象來使用;
self.ssLocalDesc = [NSString stringWithFormat:@"Tag_%d", (int)time(NULL)]; 複製代碼
結果以下圖,引用計數爲正常;字符串類型是普通字符串,這是咱們最多見的字符串類型。這個類型的字符串,在下面訪問ssLocalDesc屬性時會發生Crash。
再回到問題1,咱們知道NSNumber也使用相似的標籤指針(Tagged Pointer)。當數字較小的時候,NSNumber就不是真正的對象,而是一個標籤指針,並不會像對象同樣走銷燬釋放的流程。 驗證方法:使用一個較大的數字來初始化。好比說設置ssShowTime爲NSIntegerMax,此時引用計數恢復正常範圍。
Tagged pointer:是用於提升性能並減小內存使用的技術。原理是利用內存存儲中的內存對齊,對象的地址一般是指針大小的倍數。iOS的設備中大部分都是64位的機器,因此指針一般是以64 位整型存儲。 因爲內存對齊,指針中會有一些位總會爲零。爲了高效利用這些空間,iOS把對象指針的最低有效位爲1時,認爲該指針是 tagged pointer(標籤指針)。tagged pointer最低位中的前3位再也不被看成isa指針的地址,而是表示一個特殊的tagged class表的索引值;這個索引值用來查找tagged pointer所對應的類,剩餘的60位則會被直接使用。
標籤指針的具體概念,在附錄兩篇文章已經描述得很清晰,這裏就再也不贅述。 這個事故還有不少隱藏因素致使,好比說測試環境與線上環境不一致,好比說上線流程沒有按照規範執行,好比說代碼規範沒有遵照,好比說review流程沒有發現問題等等,針對這麼多因素,其中有兩步是很重要的: 一、保證測試環境和線上環境一致; 二、按照上線流程進行規範操做;
爲了能在測試階段發現問題,仍是把測試環境和線上環境調成徹底同樣的好; 從技術的角度來分析,只要工程設置徹底一致,就能夠實現客戶端的測試環境=線上環境。