“利用Runtime避免數(shù)組越界”過程中的坑

背景

使用NSArray時,index越界會直接導(dǎo)致整個APP崩潰卿啡,因此一直想有個辦法嘱支,在調(diào)用objectAtIndex:等方法之前做判斷,從而避免APP崩潰厕九。

辦法1

解決這個問題最笨的辦法蓖捶,當(dāng)然就是在每次調(diào)用系統(tǒng)方法之前自行判斷,但是這樣顯然太繁瑣扁远,也容易遺漏俊鱼。
大家肯定都想只寫一遍判斷的代碼刻像,然后在每次調(diào)用時都執(zhí)行。聽起來似乎可以用子類來完成并闲,但是NSArray其實是一個特殊的類细睡,繼承它會很痛苦的,不要問我怎么知道的帝火。(T^T)
還有一種辦法是利用Category纹冤。這里可以細(xì)分為兩種方式,一是直接重寫系統(tǒng)方法购公,二是自定義方法萌京。

辦法2

Category重寫系統(tǒng)方法。使用時也就只要直接調(diào)用系統(tǒng)方法宏浩,比較方便知残,但是這種做法是官方文檔中不推薦的。根據(jù)官方文檔的描述比庄,這樣盡管也能編譯運(yùn)行(告警但不報錯)求妹,但是實際執(zhí)行時可能會出現(xiàn)意想不到的問題,所以不太安全佳窑。


圖1

辦法3

Category自定義方法制恍。例如在Category中定義一個safelyGetObjectAtIndex:方法,在方法中先判斷數(shù)組對象本身是否有效神凑,再判斷index是否越界净神,只有數(shù)組有效index也沒有越界才調(diào)用系統(tǒng)的objectAtIndex:方法。這種方式比較安全溉委,實現(xiàn)起來也簡單鹃唯,但是使用時就麻煩了,必須全部調(diào)用自定義的safelyGetObjectAtIndex:方法瓣喊,也不能用array[2]這種快捷代碼了坡慌。


圖2

總之,單純利用Category沒辦法一勞永逸地解決問題藻三。

辦法4

要想真正一勞永逸地改變系統(tǒng)方法的行為洪橘,最好的方式還是Runtime。利用Runtime將系統(tǒng)方法和自定義的方法進(jìn)行交換棵帽,這樣調(diào)用系統(tǒng)方法時熄求,實際執(zhí)行的是自定義的方法。聽起來是不是感覺很完美岖寞?但是抡四,要“冒名頂替”系統(tǒng)方法柜蜈,就需要找到系統(tǒng)方法仗谆。咦指巡?系統(tǒng)方法不就是NSArray的objectAtIndex:么?圖樣圖森破隶垮!NSArray其實只是數(shù)組類簇統(tǒng)一的外殼而已藻雪,或者叫工廠類(這么描述可能不夠科學(xué),只是我自己的理解)狸吞。有沒有被坑勉耀?所以我們需要先找到數(shù)組變量實際的類,但是官方文檔上是找不到什么介紹的蹋偏,因為這些類是私有的便斥。于是我們只能通過代碼試驗一下:先定義一個數(shù)組變量,再查看其class屬性威始。關(guān)于這個試驗的結(jié)果枢纠,網(wǎng)上很多文章都提到了“__NSArrayI” 和 “__NSArrayM”這兩個類,前者對應(yīng)NSArray的實例黎棠,后者對應(yīng)NSMutableArray的實例(猜測類名中的“I”代表“immutable”晋渺, “M”代表“mutable”)。是不是感覺有這兩個類不就完事兒了么脓斩?坑又來了木西,實際上除了這兩個,數(shù)組類簇還有很多其他成員随静,比如 “__NSArray0”八千,對應(yīng)空的不可變數(shù)組,名字里有個“0”嘛燎猛,講理叼丑;還有“__NSSingleObjectArrayI”,顧名思義扛门,是只有一個成員的不可變數(shù)組鸠信;如果定義數(shù)組變量時,只alloc而不init论寨,你還會發(fā)現(xiàn)“__NSPlaceholderArray”星立。所以在替換系統(tǒng)方法時,我們需要對“__NSArrayI” “__NSArrayM” “__NSArray0” 和 “__NSSingleObjectArrayI”都進(jìn)行替換葬凳。


圖3

圖4

這里有個細(xì)節(jié)绰垂,有些情況下并不一定是圖4的輸出。比如在真機(jī)調(diào)試時也可能是圖5這樣的輸出


圖5

這也是為什么很多文章中只提到了“__NSArrayI” 和 “__NSArrayM”火焰。確切的機(jī)制我也不知道劲装,反正我是把四個子類都替換了,因為確實出現(xiàn)過因為漏掉了“__NSArray0” 和“__NSSingleObjectArrayI”而導(dǎo)致崩潰的情況。
現(xiàn)在我們就可以來替換系統(tǒng)方法了占业。仍然利用Category绒怨,在其中重寫load方法。網(wǎng)上有的文章是直接進(jìn)行替換谦疾,有的是用dispatch_once()來確保只交換一次南蹂。我不太確定是不是一定要加dispatch_once,但是反正加上了念恍。


圖6

然后實現(xiàn)自定義方法六剥,用來和系統(tǒng)方法進(jìn)行交換。此處僅舉一例(圖7)峰伙,實際上要分別寫四個方法疗疟。雖然這四個方法內(nèi)容都差不多,但是不能合并到一起瞳氓。你問為什么不能合并秃嗜?呵呵呵,因為我們上一步做的事情是【交換】了系統(tǒng)方法和自定義方法顿膨。如果你只有一個自定義方法锅锨,那么只能換一次,你非要換兩次恋沃,就等于把已經(jīng)換出來的系統(tǒng)方法又換到別處了必搞。聽起來是不是很亂,實際情況只會更亂囊咏!還是不要問我怎么知道的(T^T)恕洲。


圖7

這里有一個有趣的地方,自定義方法中梅割,如果判斷數(shù)組有效index也沒有越界霜第,不是應(yīng)該調(diào)用系統(tǒng)的objectAtIndex:方法么,為什么是調(diào)用了自定義方法本身呢户辞,不會形成死循環(huán)么泌类?答案其實很簡單,因為等到代碼實際執(zhí)行的時候底燎,兩個方法已經(jīng)做了【交換】刃榨!

最后一個坑其實網(wǎng)上的文章大多都有提及,就是在ARC下双仍,替換了可變數(shù)組“__NSArrayM”的objectAtIndex:之后枢希,會出現(xiàn)一個BUG:替換之后,在鍵盤彈出狀態(tài)下按Home鍵退出App朱沃,再回到App時就會崩潰苞轿。開啟僵尸對象(Zombie Objects)調(diào)試茅诱,可以看到輸出“[UIKeyboardLayoutStar release]: message sent to deallocated instance”“嶙洌總之就是內(nèi)存管理出問題了瑟俭。所以我們需要將替換系統(tǒng)方法的代碼寫在一個獨(dú)立的文件里,并且對這個文件關(guān)閉ARC(在Build Phases設(shè)置-fno-objc-arc參數(shù))秀睛。有的文章還提到尔当,在關(guān)閉ARC之后莲祸,應(yīng)該使用@autoreleasepool{}蹂安,個人對此還不是很確定。


圖8

最終代碼

同時實現(xiàn)了方法3和方法4
.h文件

@interface NSArray (XYSafety)
-(id)safelyGetObjectAtIndex:(NSUInteger)index;
@end

.m文件

#import <objc/runtime.h>
@implementation NSArray (XYSafety)

-(id)safelyGetObjectAtIndex:(NSUInteger)index
{
    if(self){
        if([self isKindOfClass:[NSArray class]]){
            if(self.count>0){
                if(index<self.count){
                    return [self objectAtIndex:index];
                }else{
                    NSLog(@"index:%lu out of bounds:%lu",index,self.count-1);
                }
            }else{
                NSLog(@"empty array");
            }
        }else{
            NSLog(@"not array class");
        }
    }else{
        NSLog(@"nil array");
    }
    
    NSLog(@"%@", [NSThread callStackSymbols]);
    return nil;
}

+(void)load{
    XYLog(@"");
    [super load];
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{  //方法交換只要一次就好
        //NSArray類簇實際上有很多子類锐帜,不同的構(gòu)造方法會生成不同子類的實例田盈,需要分別處理
        //替換objectAtIndex方法
        Method old0 = class_getInstanceMethod(objc_getClass("__NSArray0"), @selector(objectAtIndex:));
        Method new0 = class_getInstanceMethod(objc_getClass("__NSArray0"), @selector(NSArray0_safely_objectAtIndex:));
        method_exchangeImplementations(old0, new0);
        //替換objectAtIndex方法
        Method old1 = class_getInstanceMethod(objc_getClass("__NSSingleObjectArrayI"), @selector(objectAtIndex:));
        Method new1 = class_getInstanceMethod(objc_getClass("__NSSingleObjectArrayI"), @selector(NSArray1_safely_objectAtIndex:));
        method_exchangeImplementations(old1, new1);
        //替換objectAtIndex方法
        Method oldI = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
        Method newI = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(NSArray_safely_objectAtIndex:));
        method_exchangeImplementations(oldI, newI);
        //替換可變數(shù)組__NSArrayM的objectAtIndex:方法 會導(dǎo)致bug:鍵盤彈出的狀態(tài)下,按Home鍵退出缴阎,再進(jìn)入app時會崩潰允瞧。將本文件設(shè)置為非ARC(-fno-objc-arc),可以避免崩潰
        Method oldM = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(objectAtIndex:));
        Method newM =  class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(NSMutaleArray_safely_objectAtIndex:));
        method_exchangeImplementations(oldM, newM);
    });
}
-(id)NSArray0_safely_objectAtIndex:(NSUInteger)index
{
    if(_not VALID_ARR(self)){
        NSLog(@"[NSArray0_safely_objectAtIndex] invalid array");
    }
    else{
        if(index<self.count){
            return [self NSArray0_safely_objectAtIndex:index];
        }
        else{
            NSLog(@"[NSArray0_safely_objectAtIndex] index:%lu out of bounds:%lu",index,self.count-1);
        }
    }
    NSLog(@"%@", [NSThread callStackSymbols]);
    return nil;
}
-(id)NSArray1_safely_objectAtIndex:(NSUInteger)index
{
    if(_not VALID_ARR(self)){
        NSLog(@"[NSArray1_safely_objectAtIndex] invalid array");
    }
    else{
        if(index<self.count){
            return [self NSArray1_safely_objectAtIndex:index];
        }
        else{
            NSLog(@"[NSArray1_safely_objectAtIndex] index:%lu out of bounds:%lu",index,self.count-1);
        }
    }
    NSLog(@"%@", [NSThread callStackSymbols]);
    return nil;
}
-(id)NSArray_safely_objectAtIndex:(NSUInteger)index
{
    if(_not VALID_ARR(self)){
        NSLog(@"[NSArray_safely_objectAtIndex] invalid array");
    }
    else{
        if(index<self.count){
            return [self NSArray_safely_objectAtIndex:index];
        }
        else{
            NSLog(@"[NSArray_safely_objectAtIndex] index:%lu out of bounds:%lu",index,self.count-1);
        }
    }
    NSLog(@"%@", [NSThread callStackSymbols]);
    return nil;
}
-(id)NSMutaleArray_safely_objectAtIndex:(NSUInteger)index
{
    if(_not VALID_ARR(self)){
        NSLog(@"[NSMutaleArray_safely_objectAtIndex] invalid array");
    }
    else{
        if(index<self.count){
            //@autoreleasepool {//網(wǎng)上有些文章中的代碼蛮拔,在改成非ARC之后述暂,添加了autoreleasepool,個人還不確定是不是需要
                return [self NSMutaleArray_safely_objectAtIndex:index];
            //}
        }
        else{
            NSLog(@"[NSMutaleArray_safely_objectAtIndex] index:%lu out of bounds:%lu",index,self.count-1);
        }
    }
    NSLog(@"%@", [NSThread callStackSymbols]);
    return nil;

//    //網(wǎng)上很多文章使用了try_catch的方式建炫,但是個人不太熟悉畦韭,所以沒有采用
//    @try {
//        return [self NSMutaleArray_safely_objectAtIndex:index];
//    }
//    @catch (NSException *exception) {
//        NSLog(@"NSMutaleArray_safely_objectAtIndex exception:%@",exception);
//        return nil;
//    }
//    @finally {
//    }
}

@end

參考文章:

Runtime替換系統(tǒng)方法
http://www.reibang.com/p/5492d2d3342b
http://www.reibang.com/p/b0d3a64e76a2
http://blog.csdn.net/lqq200912408/article/details/50761139
http://www.cnblogs.com/n1ckyxu/p/6047556.html
類簇相關(guān)
http://www.reibang.com/p/c60d9ffcde4b
http://www.cocoachina.com/ios/20141219/10696.html
http://www.cnblogs.com/PeterWolf/p/6183898.html
最后一個坑的BUG調(diào)試
http://blog.csdn.net/rainbowfactory/article/details/72654088

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市肛跌,隨后出現(xiàn)的幾起案子艺配,更是在濱河造成了極大的恐慌,老刑警劉巖衍慎,帶你破解...
    沈念sama閱讀 219,366評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件转唉,死亡現(xiàn)場離奇詭異,居然都是意外死亡稳捆,警方通過查閱死者的電腦和手機(jī)赠法,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來乔夯,“玉大人期虾,你說我怎么就攤上這事⊙敝觯” “怎么了镶苞?”我有些...
    開封第一講書人閱讀 165,689評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長鞠评。 經(jīng)常有香客問我茂蚓,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,925評論 1 295
  • 正文 為了忘掉前任聋涨,我火速辦了婚禮晾浴,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘牍白。我一直安慰自己脊凰,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評論 6 392
  • 文/花漫 我一把揭開白布茂腥。 她就那樣靜靜地躺著狸涌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪最岗。 梳的紋絲不亂的頭發(fā)上帕胆,一...
    開封第一講書人閱讀 51,727評論 1 305
  • 那天,我揣著相機(jī)與錄音般渡,去河邊找鬼懒豹。 笑死,一個胖子當(dāng)著我的面吹牛驯用,可吹牛的內(nèi)容都是我干的脸秽。 我是一名探鬼主播,決...
    沈念sama閱讀 40,447評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼蝴乔,長吁一口氣:“原來是場噩夢啊……” “哼记餐!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起淘这,我...
    開封第一講書人閱讀 39,349評論 0 276
  • 序言:老撾萬榮一對情侶失蹤剥扣,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后铝穷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體钠怯,經(jīng)...
    沈念sama閱讀 45,820評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評論 3 337
  • 正文 我和宋清朗相戀三年曙聂,在試婚紗的時候發(fā)現(xiàn)自己被綠了晦炊。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,127評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡宁脊,死狀恐怖断国,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情榆苞,我是刑警寧澤稳衬,帶...
    沈念sama閱讀 35,812評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站坐漏,受9級特大地震影響薄疚,放射性物質(zhì)發(fā)生泄漏碧信。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評論 3 331
  • 文/蒙蒙 一街夭、第九天 我趴在偏房一處隱蔽的房頂上張望砰碴。 院中可真熱鬧,春花似錦板丽、人聲如沸呈枉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽猖辫。三九已至,卻和暖如春乃正,著一層夾襖步出監(jiān)牢的瞬間住册,已是汗流浹背婶博。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評論 1 272
  • 我被黑心中介騙來泰國打工瓮具, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人凡人。 一個月前我還...
    沈念sama閱讀 48,388評論 3 373
  • 正文 我出身青樓名党,卻偏偏與公主長得像,于是被迫代替她去往敵國和親挠轴。 傳聞我的和親對象是個殘疾皇子传睹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,721評論 0 9
  • 點(diǎn)擊查看原文 Web SDK 開發(fā)手冊 SDK 概述 網(wǎng)易云信 SDK 為 Web 應(yīng)用提供一個完善的 IM 系統(tǒng)...
    layjoy閱讀 13,768評論 0 15
  • 世上無難事岸晦,只怕有心人欧啤。 這或許是我們喝過的最早的一碗關(guān)于勵志的雞湯,而且在此后的一生中启上,主動或被動的一喝再喝邢隧。 ...
    幺微閱讀 209評論 0 0
  • 沈老師包券,我現(xiàn)在太后悔了纫谅!之前孩子不睡覺我就喂著奶讓他睡,結(jié)果現(xiàn)在慣出毛病了溅固!我晚上困得都恨不得用火柴棍把眼皮支起來...
    童年密碼閱讀 298評論 0 0
  • “到處都是傳奇付秕,可不見得有這么圓滿的收場。胡琴咿咿啞啞拉著侍郭,在萬盞燈的夜晚询吴,拉過來又拉過去俩垃,說不盡的滄桑的故事-不...
    YOUTH部落閱讀 12,733評論 61 500