一次標(biāo)簽指針(Tagged Pointer)導(dǎo)致的事故

前言

最近遇到一起由objc_setAssociatedObjectobjc_getAssociatedObject引發(fā)的線上Crash事故竿拆,在痛心疾首的同時(shí)也覺得很有意思旦万,特此分享楣铁。

正文

問題背景

項(xiàng)目中已經(jīng)存在某個(gè)Catagory,會(huì)往一個(gè)第三方庫的類中掛載一個(gè)屬性以清,用下面代碼的TestCatagory中ssShowTime屬性來表示雾狈。

@interface ViewController(TestCategory)

@property (nonatomic, assign) long ssShowTime;

@end

具體的實(shí)現(xiàn)是用objc_setAssociatedObjectobjc_getAssociatedObject方法岛宦。


@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

該方法已經(jīng)跑了好幾個(gè)版本阱佛,沒有出現(xiàn)過任何問題帖汞。
后面在此基礎(chǔ)上又新增一個(gè)掛載屬性,我們用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屬性會(huì)用來存一些描述翩蘸,比如說用常量,又或者拼接起來的字符串淮逊,如下:

    self.ssLocalDesc = @"123";
    // 或者
    int index = 1;
    self.ssLocalDesc = [NSString stringWithFormat:@"Tag_%d", index];

一切都正常催首,直到下面這段代碼出現(xiàn):

    self.ssLocalDesc = [NSString stringWithFormat:@"Tag_%d", (int)time(NULL)];

這個(gè)賦值語句執(zhí)行完之后,再訪問self.ssLocalDesc屬性就會(huì)產(chǎn)生Crash泄鹏!

問題回溯

當(dāng)問題出現(xiàn)之后郎任,我們來看看是犯了哪些錯(cuò)誤,才會(huì)導(dǎo)致問題的出現(xiàn):
ssShowTime 屬性雖然是long备籽,但是內(nèi)部實(shí)現(xiàn)的時(shí)候還是通過NSNumber類來實(shí)現(xiàn)舶治,所以這里不應(yīng)該使用OBJC_ASSOCIATION_ASSIGN;

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胶台。

ssLocalDesc屬性是字符串歼疮,字符串通常使用strong或者copy杂抽,那么這里使用OBJC_ASSOCIATION_ASSIGN本身就是錯(cuò)誤的诈唬。
OBJC_ASSOCIATION_ASSIGN通常是為了避免循環(huán)引用而添加,不會(huì)對引用計(jì)數(shù)產(chǎn)生變化缩麸。

問題延伸

當(dāng)解決完這個(gè)問題之后铸磅,我們發(fā)現(xiàn)crash出現(xiàn)之前,有幾個(gè)延伸問題:
問題1:為什么ssShowTime這個(gè)屬性在運(yùn)行過程中不會(huì)Crash杭朱?
我們知道Crash是由于OBJC_ASSOCIATION_ASSIGN不會(huì)引用計(jì)數(shù)加1阅仔,導(dǎo)致對象被釋放出現(xiàn)野指針的情況。那么我們在number對象掛載之前弧械,看下對象的引用計(jì)數(shù)八酒。

- (void)setSsShowTime:(long)ssShowTime {
    NSNumber *number = @(ssShowTime);
    objc_setAssociatedObject(self, @selector(ssShowTime), number, OBJC_ASSOCIATION_ASSIGN);
}

結(jié)果非常意外,引用計(jì)數(shù)的值非常大刃唐。

(lldb) p CFGetRetainCount(number)
(CFIndex) $0 = 9223372036854775807

如果排除掉引用計(jì)數(shù)出錯(cuò)的可能羞迷,我們可以理解為什么number對象不會(huì)被釋放。

問題2:為什么ssLocalDesc這個(gè)屬性在測試不會(huì)Crash画饥,而在線上運(yùn)行會(huì)出現(xiàn)Crash衔瓮?
針對ssLocalDesc屬性,我構(gòu)造了三種情況:

  • 情況1抖甘,普通常量字符串热鞍;
    self.ssLocalDesc = @"123";

結(jié)果如下圖,引用計(jì)數(shù)也很大;字符串類型為常量字符串薇宠, 隨著App運(yùn)行就創(chuàng)建偷办,退出時(shí)才銷毀。

  • 情況2澄港,測試時(shí)較短的字符串爽篷;
    int index = 1;
    self.ssLocalDesc = [NSString stringWithFormat:@"Tag_%d", index];

結(jié)果如下圖,引用計(jì)數(shù)仍很大慢睡;字符串類型為TaggedPointerString逐工,這是標(biāo)簽指針類型的字符串,把指針當(dāng)做字符串對象來使用漂辐;

  • 情況3泪喊,上線后較長的字符串;
    self.ssLocalDesc = [NSString stringWithFormat:@"Tag_%d", (int)time(NULL)];

結(jié)果如下圖髓涯,引用計(jì)數(shù)為正常袒啼;字符串類型是普通字符串,這是我們最常見的字符串類型纬纪。這個(gè)類型的字符串蚓再,在下面訪問ssLocalDesc屬性時(shí)會(huì)發(fā)生Crash。

再回到問題1包各,我們知道NSNumber也使用類似的標(biāo)簽指針(Tagged Pointer)摘仅。當(dāng)數(shù)字較小的時(shí)候,NSNumber就不是真正的對象问畅,而是一個(gè)標(biāo)簽指針娃属,并不會(huì)像對象一樣走銷毀釋放的流程。
驗(yàn)證方法:使用一個(gè)較大的數(shù)字來初始化护姆。比如說設(shè)置ssShowTime為NSIntegerMax矾端,此時(shí)引用計(jì)數(shù)恢復(fù)正常范圍。

相關(guān)知識——Tagged Pointer

Tagged pointer:是用于提高性能并減少內(nèi)存使用的技術(shù)卵皂。原理是利用內(nèi)存存儲(chǔ)中的內(nèi)存對齊秩铆,對象的地址通常是指針大小的倍數(shù)。iOS的設(shè)備中大部分都是64位的機(jī)器灯变,所以指針通常是以64 位整型存儲(chǔ)殴玛。
由于內(nèi)存對齊,指針中會(huì)有一些位總會(huì)為零柒凉。為了高效利用這些空間族阅,iOS把對象指針的最低有效位為1時(shí),認(rèn)為該指針是 tagged pointer(標(biāo)簽指針)膝捞。tagged pointer最低位中的前3位不再被當(dāng)作isa指針的地址坦刀,而是表示一個(gè)特殊的tagged class表的索引值愧沟;這個(gè)索引值用來查找tagged pointer所對應(yīng)的類,剩余的60位則會(huì)被直接使用鲤遥。

總結(jié)

標(biāo)簽指針的具體概念沐寺,在附錄兩篇文章已經(jīng)描述得很清晰,這里就不再贅述盖奈。
這個(gè)事故還有很多隱藏因素導(dǎo)致混坞,比如說測試環(huán)境與線上環(huán)境不一致,比如說上線流程沒有按照規(guī)范執(zhí)行钢坦,比如說代碼規(guī)范沒有遵守究孕,比如說review流程沒有發(fā)現(xiàn)問題等等,針對這么多因素爹凹,其中有兩步是很重要的:
1厨诸、保證測試環(huán)境和線上環(huán)境一致;
2禾酱、按照上線流程進(jìn)行規(guī)范操作微酬;

為了能在測試階段發(fā)現(xiàn)問題,還是把測試環(huán)境和線上環(huán)境調(diào)成完全一樣的好颤陶;
從技術(shù)的角度來分析颗管,只要工程設(shè)置完全一致,就可以實(shí)現(xiàn)客戶端的測試環(huán)境=線上環(huán)境滓走。

附錄

tagged pointer
【譯】采用Tagged Pointer的字符串

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末垦江,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子闲坎,更是在濱河造成了極大的恐慌疫粥,老刑警劉巖茬斧,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件腰懂,死亡現(xiàn)場離奇詭異辱魁,居然都是意外死亡揩尸,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進(jìn)店門伦仍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來娄蔼,“玉大人怖喻,你說我怎么就攤上這事∷晁撸” “怎么了锚沸?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長涕癣。 經(jīng)常有香客問我哗蜈,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任距潘,我火速辦了婚禮炼列,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘音比。我一直安慰自己俭尖,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布洞翩。 她就那樣靜靜地躺著稽犁,像睡著了一般。 火紅的嫁衣襯著肌膚如雪骚亿。 梳的紋絲不亂的頭發(fā)上缭付,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天,我揣著相機(jī)與錄音循未,去河邊找鬼陷猫。 笑死,一個(gè)胖子當(dāng)著我的面吹牛的妖,可吹牛的內(nèi)容都是我干的绣檬。 我是一名探鬼主播,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼嫂粟,長吁一口氣:“原來是場噩夢啊……” “哼娇未!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起星虹,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤零抬,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后宽涌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體平夜,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年卸亮,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了忽妒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,841評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡兼贸,死狀恐怖段直,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情溶诞,我是刑警寧澤鸯檬,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站螺垢,受9級特大地震影響喧务,放射性物質(zhì)發(fā)生泄漏颜及。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一蹂楣、第九天 我趴在偏房一處隱蔽的房頂上張望俏站。 院中可真熱鬧,春花似錦痊土、人聲如沸肄扎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽犯祠。三九已至,卻和暖如春酌呆,著一層夾襖步出監(jiān)牢的瞬間衡载,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工隙袁, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留痰娱,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓菩收,卻偏偏與公主長得像梨睁,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子娜饵,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評論 2 354