iOS 不闪退的集合实践

在iOS开发过程中,我们常见的集合类中主要包括:

  • 不可变的数组–NSArray
  • 可变的数组–NSMutableArray
  • 不可变的字典–NSDictionary
  • 可变的字典–NSMutableDictionary
  • 不可变的集合–NSSet
  • 可变的集合–NSMutableSet

我们最常用且容易出错的集合类就是:NSArray,NSMutableArray,NSMutableDictionary!
NSArray主要的问题是数组越界问题
NSMutableArray主要的问题是数组越界,和设置了nil到数组中
NSMutableDictionary 主要是在我们网络请求时,设置参数时若参数为nil则闪退!
为了解决以上问题,我们想出了两种方案:

  • 为这些类添加类别,对之前的读取方法进一步包装,在包装中添加错误判断!这种方法入侵性过大,若工程之前已经开始了,大量的地方需要替换方法!
  • 为这些类添加类别,使用runtime的method swizzling方法,为原读取方法添加错误判断,这种方法完全无侵入,我们只需要为其添加类别即可!

为此下面我们着重讨论第二种方法
按照我们的第一想法肯定是类似下面的代码:

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzlingMethod];
    });
}

+ (void)swizzlingMethod {
    Class class = NSClassFromString(@"NSArray");
    Method originMethod = class_getInstanceMethod(class, @selector(objectAtIndex:));
    Method destMethod = class_getInstanceMethod(class, @selector(rh_objectAtIndex:));
    method_exchangeImplementations(originMethod, destMethod);
}
- (id)rh_objectAtIndex:(NSUInteger)index {
    if (self.count <= index) {
        return nil;
    } else {
        return [self rh_IobjectAtIndex:index];
    }
}

代码写完,模拟数组越界访问,结果根本没有调用!
经过资料查询:
Foundation框架里面有很多类使用类簇(class cluster)设计模式,来实现一些类如:UIButton,NSNumber,集合类等!其本质就是工厂设计模式;思路就是比如我们有一个类有很多的子类,如果把这些子类全部暴露给用户,用户会很晕,不如只暴露父类给用户,然后在父类中提供初始化方法:直接获取子类实例!如我们的NSNumber类他提供了:

+ (NSNumber *)numberWithChar:(char)value;
+ (NSNumber *)numberWithUnsignedChar:(unsigned char)value;
+ (NSNumber *)numberWithShort:(short)value;
+ (NSNumber *)numberWithUnsignedShort:(unsigned short)value;
+ (NSNumber *)numberWithInt:(int)value;
+ (NSNumber *)numberWithUnsignedInt:(unsigned int)value;
+ (NSNumber *)numberWithLong:(long)value;
+ (NSNumber *)numberWithUnsignedLong:(unsigned long)value;
+ (NSNumber *)numberWithLongLong:(long long)value;
+ (NSNumber *)numberWithUnsignedLongLong:(unsigned long long)value;
+ (NSNumber *)numberWithFloat:(float)value;
+ (NSNumber *)numberWithDouble:(double)value;
+ (NSNumber *)numberWithBool:(BOOL)value;
+ (NSNumber *)numberWithInteger:(NSInteger)value API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
+ (NSNumber *)numberWithUnsignedInteger:(NSUInteger)value API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

这么多子类如果全部暴露给用户,多复杂,我们只需要通过以上方法,想要获取哪个子类的实例,就调用不同的初始化方法,多好!

回到我们的集合类,Foundation框架对集合类的实现也是采取这种模式;
比如我们的NSArray就有__NSArray0,__NSSingleObjectArrayI,__NSArrayI!为什么要分这几种实例呢?首先__NSArray0是个单例,表示空数组,所有的空数组都一样,__NSSingleObjectArrayI表示单个元素的数组!__NSArrayI表示多个元素的数组!
所以我们在method swizzling的时候必须使用真正的class,否则无效果!

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzlingMethod];
    });
}

+ (void)swizzlingMethod {
    // __NSArrayI,__NSArray0,__NSSingleObjectArrayI
    [self swizzlingClass:NSClassFromString(@"__NSArray0")
                selector:@selector(objectAtIndex:)
            destSelector:@selector(rh_0objectAtIndex:)];
    [self swizzlingClass:NSClassFromString(@"__NSArray0")
                selector:@selector(objectAtIndexedSubscript:)
            destSelector:@selector(rh_0objectAtIndexedSubscript:)];
    
    [self swizzlingClass:NSClassFromString(@"__NSArrayI")
                selector:@selector(objectAtIndex:)
            destSelector:@selector(rh_1objectAtIndex:)];
    [self swizzlingClass:NSClassFromString(@"__NSArrayI")
                selector:@selector(objectAtIndexedSubscript:)
            destSelector:@selector(rh_1objectAtIndexedSubscript:)];
    
    [self swizzlingClass:NSClassFromString(@"__NSSingleObjectArrayI")
                selector:@selector(objectAtIndex:)
            destSelector:@selector(rh_IobjectAtIndex:)];
    [self swizzlingClass:NSClassFromString(@"__NSSingleObjectArrayI")
                selector:@selector(objectAtIndexedSubscript:)
            destSelector:@selector(rh_IobjectAtIndexedSubscript:)];
}

+ (void)swizzlingClass:(Class)class
              selector:(SEL)selector
          destSelector:(SEL)destSelector {
    Method originMethod = class_getInstanceMethod(class, selector);
    Method destMethod = class_getInstanceMethod(class, destSelector);
    method_exchangeImplementations(originMethod, destMethod);
}

- (id)rh_0objectAtIndexedSubscript:(NSUInteger)idx {
    if (self.count <= idx) {
        [RHExceptionHelper raiseException:@"index beyond array count"];
        return nil;
    } else {
        return [self rh_0objectAtIndexedSubscript:idx];
    }
}

- (id)rh_0objectAtIndex:(NSUInteger)index {
    if (self.count <= index) {
        [RHExceptionHelper raiseException:@"index beyond array count"];
        return nil;
    } else {
        return [self rh_0objectAtIndex:index];
    }
}

- (id)rh_1objectAtIndexedSubscript:(NSUInteger)idx {
    if (self.count <= idx) {
        [RHExceptionHelper raiseException:@"index beyond array count"];
        return nil;
    } else {
        return [self rh_1objectAtIndexedSubscript:idx];
    }
}

- (id)rh_1objectAtIndex:(NSUInteger)index {
    if (self.count <= index) {
        [RHExceptionHelper raiseException:@"index beyond array count"];
        return nil;
    } else {
        return [self rh_1objectAtIndex:index];
    }
}

- (id)rh_IobjectAtIndexedSubscript:(NSUInteger)idx {
    if (self.count <= idx) {
        [RHExceptionHelper raiseException:@"index beyond array count"];
        return nil;
    } else {
        return [self rh_IobjectAtIndexedSubscript:idx];
    }
}

- (id)rh_IobjectAtIndex:(NSUInteger)index {
    if (self.count <= index) {
        [RHExceptionHelper raiseException:@"index beyond array count"];
        return nil;
    } else {
        return [self rh_IobjectAtIndex:index];
    }
}

这里面有个知识点是:当我们使用arr[]这种语法获取数组值的时候,我们调用的是:objectAtIndexedSubscript:方法,若该方法未method swizzling的话当你使用arr[]这种语法的时候还是会有数组越界问题的出现!

在method swizzling过程中还遇到了一个坑:就是为了偷懒只写一个

- (id)rh_objectAtIndex:(NSUInteger)index {
    if (self.count <= index) {
        [RHExceptionHelper raiseException:@"index beyond array count"];
        return nil;
    } else {
        return [self rh_IobjectAtIndex:index];
    }
}

然后不同的类都exchange这个方法,逻辑肯定是错的,多次交换实现,最终成功的只有一个!

此外我们还加了一个异常处理,原因就是,我们在调试的时候还是希望数组越界等这种问题暴露出来,及时的修改bug,所以我们添加了一个方法实现:

+ (void)raiseException:(NSString *)reason {
#if DEBUG
    [[NSException exceptionWithName:reason
                             reason:reason
                           userInfo:nil] raise];
#else
    
#endif
}
跪求Star

你可能感兴趣的:(iOS 不闪退的集合实践)