一次標籤指針(Tagged Pointer)致使的事故

前言

最近遇到一塊兒由objc_setAssociatedObjectobjc_getAssociatedObject引起的線上Crash事故,在痛心疾首的同時也以爲頗有意思,特此分享。bash

正文

問題背景

項目中已經存在某個Catagory,會往一個第三方庫的類中掛載一個屬性,用下面代碼的TestCatagory中ssShowTime屬性來表示。markdown

@interface ViewController(TestCategory)

@property (nonatomic, assign) long ssShowTime;

@end
複製代碼

具體的實現是用objc_setAssociatedObjectobjc_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屬性,我構造了三種狀況:

  • 狀況1,普一般量字符串;
self.ssLocalDesc = @"123";
複製代碼

結果以下圖,引用計數也很大;字符串類型爲常量字符串, 隨着App運行就建立,退出時才銷燬。

  • 狀況2,測試時較短的字符串;
int index = 1;
    self.ssLocalDesc = [NSString stringWithFormat:@"Tag_%d", index];
複製代碼

結果以下圖,引用計數仍很大;字符串類型爲TaggedPointerString,這是標籤指針類型的字符串,把指針當作字符串對象來使用;

  • 狀況3,上線後較長的字符串;
self.ssLocalDesc = [NSString stringWithFormat:@"Tag_%d", (int)time(NULL)];
複製代碼

結果以下圖,引用計數爲正常;字符串類型是普通字符串,這是咱們最多見的字符串類型。這個類型的字符串,在下面訪問ssLocalDesc屬性時會發生Crash。

再回到問題1,咱們知道NSNumber也使用相似的標籤指針(Tagged Pointer)。當數字較小的時候,NSNumber就不是真正的對象,而是一個標籤指針,並不會像對象同樣走銷燬釋放的流程。 驗證方法:使用一個較大的數字來初始化。好比說設置ssShowTime爲NSIntegerMax,此時引用計數恢復正常範圍。

相關知識——Tagged Pointer

Tagged pointer:是用於提升性能並減小內存使用的技術。原理是利用內存存儲中的內存對齊,對象的地址一般是指針大小的倍數。iOS的設備中大部分都是64位的機器,因此指針一般是以64 位整型存儲。 因爲內存對齊,指針中會有一些位總會爲零。爲了高效利用這些空間,iOS把對象指針的最低有效位爲1時,認爲該指針是 tagged pointer(標籤指針)。tagged pointer最低位中的前3位再也不被看成isa指針的地址,而是表示一個特殊的tagged class表的索引值;這個索引值用來查找tagged pointer所對應的類,剩餘的60位則會被直接使用。

總結

標籤指針的具體概念,在附錄兩篇文章已經描述得很清晰,這裏就再也不贅述。 這個事故還有不少隱藏因素致使,好比說測試環境與線上環境不一致,好比說上線流程沒有按照規範執行,好比說代碼規範沒有遵照,好比說review流程沒有發現問題等等,針對這麼多因素,其中有兩步是很重要的: 一、保證測試環境和線上環境一致; 二、按照上線流程進行規範操做;

爲了能在測試階段發現問題,仍是把測試環境和線上環境調成徹底同樣的好; 從技術的角度來分析,只要工程設置徹底一致,就能夠實現客戶端的測試環境=線上環境。

附錄

tagged pointer

【譯】採用Tagged Pointer的字符串

相關文章
相關標籤/搜索