背景
使用NSArray时,index越界会直接导致整个APP崩溃,因此一直想有个办法,在调用objectAtIndex:等方法之前做判断,从而避免APP崩溃。
办法1
解决这个问题最笨的办法,当然就是在每次调用系统方法之前自行判断,但是这样显然太繁琐,也容易遗漏。
大家肯定都想只写一遍判断的代码,然后在每次调用时都执行。听起来似乎可以用子类来完成,但是NSArray其实是一个特殊的类,继承它会很痛苦的,不要问我怎么知道的。(T^T)
还有一种办法是利用Category。这里可以细分为两种方式,一是直接重写系统方法,二是自定义方法。
办法2
Category重写系统方法。使用时也就只要直接调用系统方法,比较方便,但是这种做法是官方文档中不推荐的。根据官方文档的描述,这样尽管也能编译运行(告警但不报错),但是实际执行时可能会出现意想不到的问题,所以不太安全。
图1
办法3
Category自定义方法。例如在Category中定义一个safelyGetObjectAtIndex:方法,在方法中先判断数组对象本身是否有效,再判断index是否越界,只有数组有效index也没有越界才调用系统的objectAtIndex:方法。这种方式比较安全,实现起来也简单,但是使用时就麻烦了,必须全部调用自定义的safelyGetObjectAtIndex:方法,也不能用array[2]这种快捷代码了。
图2
总之,单纯利用Category没办法一劳永逸地解决问题。
办法4
要想真正一劳永逸地改变系统方法的行为,最好的方式还是Runtime。利用Runtime将系统方法和自定义的方法进行交换,这样调用系统方法时,实际执行的是自定义的方法。听起来是不是感觉很完美?但是,要“冒名顶替”系统方法,就需要找到系统方法。咦?系统方法不就是NSArray的objectAtIndex:么?图样图森破!NSArray其实只是数组类簇统一的外壳而已,或者叫工厂类(这么描述可能不够科学,只是我自己的理解)。有没有被坑?所以我们需要先找到数组变量实际的类,但是官方文档上是找不到什么介绍的,因为这些类是私有的。于是我们只能通过代码试验一下:先定义一个数组变量,再查看其class属性。关于这个试验的结果,网上很多文章都提到了“__NSArrayI” 和 “__NSArrayM”这两个类,前者对应NSArray的实例,后者对应NSMutableArray的实例(猜测类名中的“I”代表“immutable”, “M”代表“mutable”)。是不是感觉有这两个类不就完事儿了么?坑又来了,实际上除了这两个,数组类簇还有很多其他成员,比如 “__NSArray0”,对应空的不可变数组,名字里有个“0”嘛,讲理;还有“__NSSingleObjectArrayI”,顾名思义,是只有一个成员的不可变数组;如果定义数组变量时,只alloc而不init,你还会发现“__NSPlaceholderArray”。所以在替换系统方法时,我们需要对“__NSArrayI” “__NSArrayM” “__NSArray0” 和 “__NSSingleObjectArrayI”都进行替换。
图3
图4
这里有个细节,有些情况下并不一定是图4的输出。比如在真机调试时也可能是图5这样的输出
图5
这也是为什么很多文章中只提到了“__NSArrayI” 和 “__NSArrayM”。确切的机制我也不知道,反正我是把四个子类都替换了,因为确实出现过因为漏掉了“__NSArray0” 和“__NSSingleObjectArrayI”而导致崩溃的情况。
现在我们就可以来替换系统方法了。仍然利用Category,在其中重写load方法。网上有的文章是直接进行替换,有的是用dispatch_once()来确保只交换一次。我不太确定是不是一定要加dispatch_once,但是反正加上了。
图6
然后实现自定义方法,用来和系统方法进行交换。此处仅举一例(图7),实际上要分别写四个方法。虽然这四个方法内容都差不多,但是不能合并到一起。你问为什么不能合并?呵呵呵,因为我们上一步做的事情是【交换】了系统方法和自定义方法。如果你只有一个自定义方法,那么只能换一次,你非要换两次,就等于把已经换出来的系统方法又换到别处了。听起来是不是很乱,实际情况只会更乱!还是不要问我怎么知道的(T^T)。
图7
这里有一个有趣的地方,自定义方法中,如果判断数组有效index也没有越界,不是应该调用系统的objectAtIndex:方法么,为什么是调用了自定义方法本身呢,不会形成死循环么?答案其实很简单,因为等到代码实际执行的时候,两个方法已经做了【交换】!
最后一个坑其实网上的文章大多都有提及,就是在ARC下,替换了可变数组“__NSArrayM”的objectAtIndex:之后,会出现一个BUG:替换之后,在键盘弹出状态下按Home键退出App,再回到App时就会崩溃。开启僵尸对象(Zombie Objects)调试,可以看到输出“[UIKeyboardLayoutStar release]: message sent to deallocated instance”。总之就是内存管理出问题了。所以我们需要将替换系统方法的代码写在一个独立的文件里,并且对这个文件关闭ARC(在Build Phases设置-fno-objc-arc参数)。有的文章还提到,在关闭ARC之后,应该使用@autoreleasepool{},个人对此还不是很确定。
图8
最终代码
同时实现了方法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类簇实际上有很多子类,不同的构造方法会生成不同子类的实例,需要分别处理
//替换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);
//替换可变数组__NSArrayM的objectAtIndex:方法 会导致bug:键盘弹出的状态下,按Home键退出,再进入app时会崩溃。将本文件设置为非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 {//网上有些文章中的代码,在改成非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;
// //网上很多文章使用了try_catch的方式,但是个人不太熟悉,所以没有采用
// @try {
// return [self NSMutaleArray_safely_objectAtIndex:index];
// }
// @catch (NSException *exception) {
// NSLog(@"NSMutaleArray_safely_objectAtIndex exception:%@",exception);
// return nil;
// }
// @finally {
// }
}
@end
参考文章:
Runtime替换系统方法
http://www.jianshu.com/p/5492d2d3342b
http://www.jianshu.com/p/b0d3a64e76a2
http://blog.csdn.net/lqq200912408/article/details/50761139
http://www.cnblogs.com/n1ckyxu/p/6047556.html
类簇相关
http://www.jianshu.com/p/c60d9ffcde4b
http://www.cocoachina.com/ios/20141219/10696.html
http://www.cnblogs.com/PeterWolf/p/6183898.html
最后一个坑的BUG调试
http://blog.csdn.net/rainbowfactory/article/details/72654088