我们知道,NSArray的objectAtIndex方法若使用了一个超出范围的index值,那么将会抛出异常导致程序终止运行。
然而在发布的App中,其实NSArray的异常有些小题大做了。即使显示数据为空,也比Crash掉的用户体验要好的多啊。(例如初始化的Cell在填充数据时,objectAtIndex获取数据index越界导致异常)
因此,我就写了一个NSArray的Category:
- (id)safeObjectAtIndex:(NSUInteger)index {
if (index < self.count) {
return [self objectAtIndex:index];
} else {
return nil;
}
}
解决问题是解决了,可是很不好看,也没有办法保证其他成员也一定使用这个方法啊。再者,这样是无法对新语法(Objective-C Literals)起作用的(这点是硬伤……)。
突然想到了之前看过的Method swizzling的方法,利用Runtime特性,可以替换现有方法的实现。在这里使用在合适不过了。(深入阅读:深入浅出Cocoa之Method Swizzling)
那么试试看吧。如果你看了上面提到的那篇文章,应该发现了jrswizzle——Method swizzling方法的封装。我们就不造轮子了,用CocoaPods将它加入到我们的项目中吧。
首先创建NSArray的Category,当DEBUG模式时,index超出范围将抛出异常,而Release版本中只会返回nil:
@interface NSArray (SafeCategory)
- (id)TKSafe_objectAtIndex:(NSUInteger)index;
@end
@implementation NSArray (SafeCategory)
- (id)TKSafe_objectAtIndex:(NSUInteger)index {
if (index < self.count) {
return [self TKSafe_objectAtIndex:index];
} else {
#ifdef DEBUG
NSAssert(NO, @"index %d > count %d", index, self.count);
#endif
return nil;
}
}
@end
然后在程序中调用替换方法:
#import <JRSwizzle/JRSwizzle.h>
NSError *error = nil;
[NSArray jr_swizzleClassMethod:@selector(objectAtIndex:)
withClassMethod:@selector(TKSafe_objectAtIndex:)
error:&error];
NSLog(@"Err: %@", error);
悲剧,出错了:
Err: Error Domain=NSCocoaErrorDomain Code=-1 "+[NSObject(JRSwizzle) jr_swizzleMethod:withMethod:error:]: original method objectAtIndex: not found for class NSArray" UserInfo=0x1754d770 {NSLocalizedDescription=+[NSObject(JRSwizzle) jr_swizzleMethod:withMethod:error:]: original method objectAtIndex: not found for class NSArray}
它说没有在NSArray中找到objectAtIndex方法,Why?
这里的NSArray是一种特殊的类,英文叫做Class cluster,中文翻译过来是类簇,在设计模式中,这个叫做工厂类,它在外层提供了很多方法接口,但是这些方法的实现是由具体的内部类来实现的。当使用NSArray生成一个对象时,初始化方法会判断哪个“自己内部的类”最适合生成这个对象,然后这个“工厂”就会生成这个具体的类对象返回给你。这种又外层类提供统一抽象的接口,然后具体实现让隐藏的,具体的内部类来实现,在设计模式中称为“抽象工厂”模式。(引用自objc’s category and class cluster)
也就是说,对于普通的类,我们使用上述方法是没有问题的,然而对于Class cluster这种工厂类,就需要找到它的真身才行。
通过对Class cluster的了解,我们明白了平时使用的NSArray实例其实是另外一种类型,让我们看看它们的真身:
NSLog(@"%@", [NSArray class]);
NSLog(@"%@", [[NSArray array] class]);
NSLog(@"%@", [[NSMutableArray array] class]);
运行查看输出,应该是如下内容:
NSArray
__NSArrayI
__NSArrayM
第一行调用NSArray类方法class,当然是它自己;第二行调用[NSArray array]方法返回了一个实例,类型是__NSArrayI;第三行调用了[NSMutableArray array]方法返回了可变数组的实例,类型是__NSArrayM。
现在已经知道了待替换方法类的名称,获得它的Class就很随意了,我们有三种方法:
- Class class = [[NSArray array] class];
- Class class = NSClassFromString(@”__NSArrayI”);
- Class class = objc_getClass(“__NSArrayI”);
第二种和第三种方式基本相同,都是根据类名字符串获得Class,但第三种写法编译器会报警告:Implicitly declaring library function ‘objc_getClass’ with type ‘id (const char *)’。第一种通过实体类获得Class,从安全性角度来说,我倾向于使用第一种。
最终代码如下,当然,我们还可以修改更多,这里有一份例子:
[[[NSArray array] class] jr_swizzleMethod:@selector(objectAtIndex:)
withMethod:@selector(TKSafe_objectAtIndex:)
error:&error];
这下新版本的Crash数量又降低了,想想还有点小激动呢。
网友评论