runtime

“利用Runtime避免数组越界”过程中的坑

2017-08-08  本文已影响75人  下羊

背景

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

上一篇 下一篇

猜你喜欢

热点阅读