背景
使用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