原文 : 与佳期的个人博客(gonghonglou.com)
因为 SafeKit 的异常保护的原理是在 category 替换系统方法,只需在工程中引用 SafeKit 即可避免 NSArray 数组越界等引发的 crash,并不需要额外操作。所以日常开发中渐渐的并不会怎么在意到 SafeKit 的存在。
最近公司有一份项目需要重构,完全重写的那种,从新建一份空工程开始。之前并没有在意 SafeKit 的存在,所以在最开始并没有在工程中引入 SafeKit,直到一次痛苦的 debug 发现 crash 发生在这样的地方:
// cacheId 为 NSNmber 类型
if ([obj1.cacheId isEqualToNumber:obj2.cacheId]) {
// ...
}
报错信息:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFNumber compare:]: nil argument'
因为在执行 NSNmber
的 isEqualToNumber:
方法时并没有判断 obj2.cacheId
是否为 nil
,苹果的API也没有对异常保护,所以当 obj2.cacheId
为 nil
时便会 crash。然后才想起 SafeKit
而且以这种写法 Xcode 也不会给出警告,所以在 coding 时很容易忽略为 nil
的情况。
SafeKit 源码
SafeKit 的源码非常少,原理非常简单,就是将 NSNumber
, NSArray
, NSMutableArray
, NSDictionary
, NSMutableArray
, NSString
, NSMutableString
中会因越界、为 nil
等情况发生 crash 的方法替换为自己的方法,在自己的方法中加判断,如果越界、为 nil
等 直接 return,否则继续执行。
例如NSArray
#import "NSArray+SafeKit.h"
#import "NSObject+swizzle.h"
@implementation NSArray (SafeKit)
- (instancetype)initWithObjects_safe:(id *)objects count:(NSUInteger)cnt {
NSUInteger newCnt = 0;
for (NSUInteger i = 0; i < cnt; i++) {
if (!objects[i]) {
break;
}
newCnt++;
}
self = [self initWithObjects_safe:objects count:newCnt];
return self;
}
- (id)safe_objectAtIndex:(NSUInteger)index {
if (index >= [self count]) {
return nil;
}
return [self safe_objectAtIndex:index];
}
- (NSArray *)safe_arrayByAddingObject:(id)anObject {
if (!anObject) {
return self;
}
return [self safe_arrayByAddingObject:anObject];
}
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self safe_swizzleMethod:@selector(initWithObjects_safe:count:) tarClass:@"__NSPlaceholderArray" tarSel:@selector(initWithObjects:count:)];
[self safe_swizzleMethod:@selector(safe_objectAtIndex:) tarClass:@"__NSArrayI" tarSel:@selector(objectAtIndex:)];
[self safe_swizzleMethod:@selector(safe_arrayByAddingObject:) tarClass:@"__NSArrayI" tarSel:@selector(arrayByAddingObject:)];
});
}
@end
以 safe_arrayByAddingObject:
替换 arrayByAddingObject:
方法,当 anObject 不存在则直接返回self
以 safe_objectAtIndex:
替换 objectAtIndex:
方法,当数组越界时直接返回 nil
注意,在 class_getInstanceMethod
方法中,要先知道类对应的真实的类名才行,例如 NSArray 其实在 Runtime 中对应着 __NSArrayI
:
类 | Runtime 中对应 |
---|---|
NSNumber | __NSCFNumber |
NSArray | __NSArrayI |
NSMutableArray | __NSArrayM |
NSDictionary | __NSDictionaryI |
NSMutableDictionary | __NSDictionaryM |
NSString | __NSCFString |
NSString | __NSCFConstantString |
具体对应参考 SafeKit 源码
其中,为了方便 NANumber
, NSDictionary
等分类调用,Method Swizzling 操作也被作者在 NSObject
的 Swizzle
分类中替换成自己的 safe_swizzleMethod
方法:
@implementation NSObject(Swizzle)
+ (void)safe_swizzleMethod:(SEL)srcSel tarSel:(SEL)tarSel{
Class clazz = [self class];
[self safe_swizzleMethod:clazz srcSel:srcSel tarClass:clazz tarSel:tarSel];
}
+ (void)safe_swizzleMethod:(SEL)srcSel tarClass:(NSString *)tarClassName tarSel:(SEL)tarSel{
if (!tarClassName) {
return;
}
Class srcClass = [self class];
Class tarClass = NSClassFromString(tarClassName);
[self safe_swizzleMethod:srcClass srcSel:srcSel tarClass:tarClass tarSel:tarSel];
}
+ (void)safe_swizzleMethod:(Class)srcClass srcSel:(SEL)srcSel tarClass:(Class)tarClass tarSel:(SEL)tarSel{
if (!srcClass) {
return;
}
if (!srcSel) {
return;
}
if (!tarClass) {
return;
}
if (!tarSel) {
return;
}
Method srcMethod = class_getInstanceMethod(srcClass,srcSel);
Method tarMethod = class_getInstanceMethod(tarClass,tarSel);
method_exchangeImplementations(srcMethod, tarMethod);
}
@end
需要注意的是:
在 iOS10 及以前,NSArray 的语法糖 array[i]
用法会先调用 - (ObjectType)objectAtIndexedSubscript:(NSUInteger)idx NS_AVAILABLE(10_8, 6_0);
方法,如果没有再调用 - (ObjectType)objectAtIndex:(NSUInteger)index;
方法,所以 SafeKit 可以保证安全。
但是在 iOS11 beta 版中, array[i]
语法糖会直接调用 - (ObjectType)objectAtIndexedSubscript:(NSUInteger)idx NS_AVAILABLE(10_8, 6_0);
方法,如果没有则直接报错,所以为了适配 iOS11 ,在 SafeKit 的 NSArray+SafeKit 分类中还应该替换掉 - (ObjectType)objectAtIndexedSubscript:(NSUInteger)idx NS_AVAILABLE(10_8, 6_0);
方法。
Method Swizzling 使用分析
Method Swizzling 大概是 Runtime 中最常用的一个黑魔法了,它本质上就是对 IMP 和 SEL 进行交换。
Method Swizzling 应该在 +load 方法中执行
+load 方法是当类或分类被添加到 Objective-C runtime 时被调用的;
+initialize 方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用。也就是说 +initialize 方法是以懒加载的方式被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的 +initialize 方法是永远不会被调用的。
所以 Method Swizzling 应该在 +load 方法中执行,避免 Method Swizzling 不会被执行到的情况
使用 dispatch_once 保证执行次数
Method Swizzling 本质上就是对 IMP 和 SEL 进行交换,如果被执行偶数次那么调换就会失效,相当于没有调换。比如同时调换 NSArray 和 NSMutableArray 中的 objectAtIndex:,如果不用 dispatch_once 保证执行,就可能导致调换方法失效。
也正因为这个原因,在 load 方法中执行 Method Swizzling 时不可调用 [super load]
方法,否则同样会导致调换方法失效。
参考
Objective-C Method Swizzling 的最佳实践 一文中给出的最佳实践:
@interface UIViewController (MRCUMAnalytics)
@end
@implementation UIViewController (MRCUMAnalytics)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(mrc_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (success) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
#pragma mark - Method Swizzling
- (void)mrc_viewWillAppear:(BOOL)animated {
[self mrc_viewWillAppear:animated];
[MobClick beginLogPageView:NSStringFromClass([self class])];
}
@end
- 主类本身有实现需要替换的方法,也就是
class_addMethod
方法返回NO
。这种情况的处理比较简单,直接交换两个方法的实现。 - 主类本身没有实现需要替换的方法,而是继承了父类的实现,即
class_addMethod
方法返回YES
。这时使用class_getInstanceMethod
函数获取到的originalSelector
指向的就是父类的方法,我们再通过执行class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
将父类的实现替换到我们自定义的mrc_viewWillAppear
方法中。这样就达到了在mrc_viewWillAppear
方法的实现中调用父类实现的目的。 -
mrc_viewWillAppear:
方法的定义看似是递归调用引发死循环,其实不会。因为[self mrc_viewWillAppear:animated]
消息会动态找到mrc_viewWillAppear:
方法的实现,而它的实现已经被我们与viewWillAppear:
方法实现进行了互换,所以这段代码不仅不会死循环,如果把[self mrc_viewWillAppear:animated]
换成[self viewWillAppear:animated]
反而会引发死循环。
神经病院Objective-C Runtime出院第三天——如何正确使用Runtime 一文中给出的Swizzling Method 标准定义,避免命名冲突:
@implementation NSView (MyViewAdditions)
static void MySetFrame(id self, SEL _cmd, NSRect frame);
static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame);
static void MySetFrame(id self, SEL _cmd, NSRect frame) {
// do custom work
SetFrameIMP(self, _cmd, frame);
}
+ (void)load {
[self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP];
}
@end
虽然上面的代码看上去不是OC(因为使用了函数指针),但是这种做法确实有效的防止了命名冲突的问题。原则上来说,其实上述做法更加符合标准化的Swizzling。这种做法可能和人们使用方法不同,但是这种做法更好。Swizzling Method 标准定义应该是如下的样子:
typedef IMP *IMPPointer;
BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
IMP imp = NULL;
Method method = class_getInstanceMethod(class, original);
if (method) {
const char *type = method_getTypeEncoding(method);
imp = class_replaceMethod(class, original, replacement, type);
if (!imp) {
imp = method_getImplementation(method);
}
}
if (imp && store) { *store = imp; }
return (imp != NULL);
}
@implementation NSObject (FRRuntimeAdditions)
+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
return class_swizzleMethodAndStore(self, original, replacement, store);
}
@end
后记
小白出手,请多指教。如言有误,还望斧正!
转载请保留原文地址:http://gonghonglou.com/2017/09/07/analyse-safekit/