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

背景

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 点击查看原文 Web SDK 开发手册 SDK 概述 网易云信 SDK 为 Web 应用提供一个完善的 IM 系统...
    layjoy阅读 13,662评论 0 15
  • 世上无难事,只怕有心人。 这或许是我们喝过的最早的一碗关于励志的鸡汤,而且在此后的一生中,主动或被动的一喝再喝。 ...
    幺微阅读 206评论 0 0
  • 沈老师,我现在太后悔了!之前孩子不睡觉我就喂着奶让他睡,结果现在惯出毛病了!我晚上困得都恨不得用火柴棍把眼皮支起来...
    童年密码阅读 289评论 0 0
  • “到处都是传奇,可不见得有这么圆满的收场。胡琴咿咿哑哑拉着,在万盏灯的夜晚,拉过来又拉过去,说不尽的沧桑的故事-不...
    YOUTH部落阅读 12,701评论 61 500