[iOS Runtime]数组越界写全了吗

3,024 阅读3分钟

背景

做了个升级检查,其中有一段代码直接下标访问的数组arr[0],我敢这样写,因为我用runtime判断了数组越界。但是我现在发现没写全,我真是个大傻子😂,关键是我还写了测试代码。

解决

2019.11.22更新:

@马德里不可思义的提醒,为了保证代码的健壮性,不建议hook,我非常赞同他的观点。但hook的最终目的不是隐藏问题,而是避免线上crash的一种方式,同时也要保证平时代码的健壮性。于是再加上DEBUG的判断,只有在RELEASE的情况下才去hook

隐藏的实现类

先说不可变数组,它的实际实现类有三种:

  1. __NSArrayI 多个元素
  2. __NSArray0 空数组
  3. __NSSingleObjectArrayI 单个元素

这些实现类怎么得到,写个代码,断点看下就明白了

方法交换

类方法(class_getClassMethod)和对象方法(class_getInstanceMethod)都可以添加,这里以对象方法为例

NSObject+Swizzling.h

+ (void)swizzlingInstanceMethodOrigSEL:(SEL)origSEL swizzleSEL:(SEL)swizzleSEL{
    Method origMe = class_getInstanceMethod(self, origSEL);
    Method swizzleMe = class_getInstanceMethod(self, swizzleSEL);
    
    // 不管原方法存不存在,添加原方法看下结果, 这个只交换了一半(SEL和Method关联)
    BOOL addOrigMe = class_addMethod(self, origSEL, method_getImplementation(swizzleMe), method_getTypeEncoding(swizzleMe));
    
    // 添加原方法成功,说明原方法之前不存在
    // 然后交换剩下的一半(SEL和Method关联)
    if (addOrigMe) {
        class_replaceMethod(self, swizzleSEL, method_getImplementation(origMe), method_getTypeEncoding(origMe));
    }
    // 添加原方法失败,说明原方法之前是存在的,就可以直接替换Method
    else{
        method_exchangeImplementations(origMe, swizzleMe);
    }
}

带上之前找到的实现类,就可以判断数组越界了

#import "NSArray+Safe.h"

+ (void)load{

#ifdef DEBUG

#else
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        Class arrayI = objc_getClass("__NSArrayI");
        Class arrayEmpty = objc_getClass("__NSArray0");
        Class arraySingle = objc_getClass("__NSSingleObjectArrayI");
        
        [arrayI swizzlingInstanceMethodOrigSEL:@selector(objectAtIndex:) swizzleSEL:@selector(sf_objectAtIndex:)];
        [arrayI swizzlingInstanceMethodOrigSEL:@selector(objectAtIndexedSubscript:) swizzleSEL:@selector(sf_objectAtIndexedSubscript:)];

        [arrayEmpty swizzlingInstanceMethodOrigSEL:@selector(objectAtIndex:) swizzleSEL:@selector(empty_objectAtIndex:)];
        [arrayEmpty swizzlingInstanceMethodOrigSEL:@selector(objectAtIndexedSubscript:) swizzleSEL:@selector(empty_objectAtIndexedSubscript:)];

        [arraySingle swizzlingInstanceMethodOrigSEL:@selector(objectAtIndex:) swizzleSEL:@selector(single_objectAtIndex:)];
        [arraySingle swizzlingInstanceMethodOrigSEL:@selector(objectAtIndexedSubscript:) swizzleSEL:@selector(single_objectAtIndexedSubscript:)];
    });
#endif
}

#pragma mark - __NSArrayI

- (id)sf_objectAtIndex:(NSUInteger)index{

    if (index > self.count - 1) {
        return nil;
    }
    return [self sf_objectAtIndex:index];
}

- (id)sf_objectAtIndexedSubscript:(NSUInteger)idx{

    if (idx > self.count - 1) {
        return nil;
    }
    return [self sf_objectAtIndexedSubscript:idx];
}

#pragma mark - __NSArray0

- (id)empty_objectAtIndex:(NSUInteger)index{
    return nil;
}

- (id)empty_objectAtIndexedSubscript:(NSUInteger)idx{
    return nil;
}

#pragma mark - __NSSingleObjectArrayI

- (id)single_objectAtIndex:(NSUInteger)index{
    if (index > self.count - 1) {
        return nil;
    }
    return [self single_objectAtIndex:index];
}

- (id)single_objectAtIndexedSubscript:(NSUInteger)idx{
    if (idx > self.count - 1) {
        return nil;
    }
    return [self single_objectAtIndexedSubscript:idx];
}

自己调用自己,死循环了吗?

如果不看前面,确实是死循环了,傻子才这样写😂。 但是前面交换了方法实现,以single_objectAtIndex为例子,简化一下就是

__NSSingleObjectArrayI.single_objectAtIndex = __NSSingleObjectArrayI.objectAtIndex

那么在single_objectAtIndex方法内部再次调用single_objectAtIndex,其实相当于调用了原来的方法objectAtIndex

居然有重复代码,能忍?

这里面,有重复代码,single_objectAtIndexsf_objectAtIndex,难道不能合在一起吗?答案能忍!是真不能合在一起,原因是,多次交换后回到了原来的方法。以__NSSingleObjectArrayI为例,如果写成这样

[arraySingle swizzlingInstanceMethodOrigSEL:@selector(objectAtIndex:) swizzleSEL:@selector(sf_objectAtIndex:)];
[arraySingle swizzlingInstanceMethodOrigSEL:@selector(objectAtIndexedSubscript:) swizzleSEL:@selector(sf_objectAtIndexedSubscript:)];

因为__NSArrayI交换了一轮了

简化下大概像这样,sf_objectAtIndex目前的实现就是__NSArrayI.objectAtIndex

self.sf_objectAtIndex = __NSArrayI.objectAtIndex

如果__NSSingleObjectArrayI直接交换sf_objectAtIndex,那么结果就是

__NSSingleObjectArrayI.objectAtIndex = __NSArrayI.objectAtIndex

所以这个时候会崩溃,还是老老实实的写每个实现类的方法。

可变数组

实现类就只有一个__NSArrayM,实现同上

参考

iOS开发中防止数组越界导致的崩溃(升级版)