简单说下写这篇简书的目的吧, swizzle 对 iOS 开发者并不陌生, 网上也有很多资料. 但是我浏览一些文章, 多数都是写的不全. 为什么不全, 一是 swizzle 在实现方法交换时不够严谨, 二是, 对数组, 字典添加安全防护时也不全面, 里面还是有很多细节的.
先上代码:
示例代码地址 提取码: 3iy6
一, swizzle 的简单介绍
swizzle 又叫黑魔法, 它可以在运行时实现两个方法 (对象方法或者类方法) 的交换.
- @implementation Person
- - (void)run
- {
- NSLog(@"Person -- run");
- }
- @end
- @implementation Student
- + (void)load
- {
- [Swizzle swizzleInstanceMethodWithClass:[self class] originlaSelector:@selector(run) swizzleSelector:@selector(goodRun)];
- }
- - (void)goodRun
- {
- NSLog(@"Student --- goodRun");
- // [self goodRun];
- }
- @end
- Student *student = [Student new];
- [student run];
运行结果
我们可以看到, 子类 (student) 调用父类的方法 (run) 时, 内部调的是子类的方法(goodRun).
二, swizzle 的实现
- // 交换某个类的对象方法
- + (void)swizzleInstanceMethodWithClass:(Class)cls originlaSelector:(SEL)originalSelector swizzleSelector:(SEL)swizzleSelector
- {
- Method originalMethod = class_getInstanceMethod(cls, originalSelector);
- Method swizzleMethod = class_getInstanceMethod(cls, swizzleSelector);
- BOOL isAddMethod = class_addMethod(cls,originalSelector,method_getImplementation(swizzleMethod),method_getTypeEncoding(swizzleMethod));
- if (isAddMethod) {
- class_replaceMethod(cls,swizzleSelector,method_getImplementation(originalMethod),method_getTypeEncoding(originalMethod));
- } else {
- method_exchangeImplementations(originalMethod, swizzleMethod);
- }
- }
- // 交换某个类的类方法
- + (void)swizzleClassMethodWithClass:(Class)cls originlaSelector:(SEL)originalSelector swizzleSelector:(SEL)swizzleSelector
- {
- Class metaClass = object_getClass(cls);
- Method originalMethod = class_getClassMethod(metaClass, originalSelector);
- Method swizzledMethod = class_getClassMethod(metaClass, swizzleSelector);
- BOOL isAddMethod = class_addMethod(metaClass,originalSelector,method_getImplementation(swizzledMethod),method_getTypeEncoding(swizzledMethod));
- if (isAddMethod) {
- class_replaceMethod(metaClass,swizzleSelector,method_getImplementation(originalMethod),method_getTypeEncoding(originalMethod));
- } else {
- method_exchangeImplementations(originalMethod, swizzledMethod);
- }
- }
注意点: 网上有些资料是不先调用 class_addMethod 函数的, 而是直接调用 method_exchangeImplementations 就完了
现在我们就来讲解下为什么要先调用 class_addMethod 函数
先结合场景我们看下不这么做的后果是什么:
首先我们先回顾上面简介 swizzle 写的代码, 可以看到在 Student 类中我先把 [slef goodRun] 方法注释了, 如果不注释 [slef goodRun] 方法, 并且不先执行执行 class_addMethod 和里面的判断的话调用[[Person new] run] 是会奔溃的. 这是奔溃的信息
奔溃信息
那么, 为什么就会奔溃呢? 也就是 class_addMethod 和判断里面代码的作用是什么了. 我们先把步骤解析一下:
1.Person 中的 run 方法实现变成 Student 中 goodRun 方法实现, 而 Student 中的 goodRun 方法除了自己需要添加的功能外还调用 Person 中的 run 方法(这是保证父类的方法功能不影响).
2.[[Person new] run]方法执行可以解析为两步: 一是 goodRun 方法中自己添加的功能 , 二是 Person 类中的 run 方法. 问题就是处在第二步, Person 类中的 run 方法已经变成 goodRun 了, 就等于, 父类调用了子类的 goodRun 方法, 大家都知道这是不允许的, 所以会报方法找不到的错误.
3.[[Student new] run] 方法则是子类调用父类的方法, 而父类的 run 方法实现变成 Student 中 goodRun 方法的实现, 同样解析成两步是: 一是 goodRun 中自己添加的功能, 二是父类中 run 方法的功能. 子类调用父类的方法这是没问题的. 所以 [[Student new] run] 是不会奔溃的.
这就是奔溃原因的所在. 而在开发中实现方法的交换, 都是会先加自己需要的功能, 最后再调源方法的. 比如: 我想给数组添加对象时过滤掉 nil 的对象, 以防奔溃, 那么你就可以在替换的方法中添加判断, 最后一步需要再调用源方法 addObject, 这样才能保证数组添加对象功能的正常和过滤 nil 的对象.
class_addMethod: 给某个类添加一个方法, 添加成功返回 YES, 否者返回 NO
class_replaceMethod: 将某个类中的方法替换
那么, 怎么来解决这种奔溃问题呢
问题就出现在父类的实例调用自己方法时, 调用了子类的方法, 所以, 解决办法就出来了, 我们让父类调用自己的方法时还是调用的自己的方法, 而子类调用时, 调用的是子类自己重写父类的方法和新增的方法功能. 简单来说就是 [[Person new] run] 是调 Person 自己的 run 方法,[[Student new] run] 是调用 Student 重写父类的 run 方法和自己新加的 goodRun 方法.
class_addMethod 函数和里面判断步骤详解:
先调用 class_addMethod 函数, 给 Student 类中新增一个名字叫 run 的方法而实现是用 Student 新增的 goodRun 方法的实现, 这就等于 Student 重写了父类的方法, 并且这个方法还有我们自己新增的功能
再调用 class_replaceMethod 方法, 将我们新增的 goodRun 方法的实现替换成父类 Person 中的 run 方法实现.
这个时候, Person 类中的 run 方法还是自己的 run 方法, 而 Student 中的新增了一个带有父类 Person 的 run 方法功能和自己新增的功能, 新增的 goodRun 方法实现是父类 Person 的 run 方法实现.
[[Person new] run]调用是调用自己的 run 方法, 所以没有问题
[[Student new] run] 方法解析成具体的步骤为: 一: 调用 Studnet 中新增的 run 方法, 二: 新增的原来 goodRun 方法的功能 三: 父类中 run 方法的实现.
这样就达到了我们既新增了功能, 又保证原方法的功能.
用一份伪代码来描述这个过程就很清晰明了了
未新增方法和替换前. jpeg
调用 class_addMethod 后. jpeg
调用 class_replaceMethod 函数后. jpeg
三, swizzle 在数组等容器类中的应用
对这些容器类做防护时, 我们就需要明确对应的类型都是有哪些, 还有需要做防护的 API 都有哪些, 只对单个 API 或者类型不全时, 还是没有起到防护的作用, 也就失去了这功能的意义了.
1.NSArray:
先了解 NSArray 在不同形式下的类型:
alloc 后所得到的类为__NSPlaceholderArray
当 init 为一个空数组后, 变成了__NSArray0
如果有且仅有一个元素, 那么为__NSSingleObjectArrayI
如果数组大于一个元素, 那么为__NSArrayI
需要防护的 API 有哪些:
- arrayWithObjects:count:
- objectAtIndex:
- objectsAtIndexes:
- arrayWithObjects:count:
- 2.NSMutableArray:
类型:__NSArrayM
需要防护的 API:
- objectAtIndex:
- addObject:
- removeObjectAtIndex:
- removeObjectsAtIndexes:
- removeObjectsInRange:
- replaceObjectAtIndex:withObject:
- replaceObjectsAtIndexes:withObjects:
- replaceObjectsInRange:withObjectsFromArray:
- insertObject:atIndex:
- insertObjects:atIndexes:
- 3.NSDictionary:
需要防护的类型:
- __NSPlaceholderDictionary
- NSDictionary
需要防护的 API:
- dictionaryWithObject:forKey:
- dictionaryWithObjects:forKeys:
- 4.NSMutableDictionary:
需要防护的类型:
__NSDictionaryM
需要防护的 API:
- setObject:forKey:
- setValue:forKey:
- removeObjectForKey:
- 5.NSString:
需要防护的类型:
__NSCFConstantString
需要防护的 API:
- characterAtIndex:
- substringToIndex:
- substringFromIndex:
- substringWithRange:
- stringByReplacingCharactersInRange:withString:
- stringByAppendingString:
- 6.NSMutableString:
需要防护的类型:
____NSCFString
需要防护的 API:
- replaceCharactersInRange:withString:
- insertString:atIndex:
- deleteCharactersInRange:
如何得到这些容器类型呢, 只要我们多试几种创建和初始化的形式, 然后让程序奔溃, 从奔溃的信息里面就可以看到对应的类型是什么了.
补充:
对于有些 API, 比如: NSDictionary 的 objectForKey: 传入的 key 为 nil 时不会奔溃, 那么我们就不需要防护了. 还有一些像 NSMutableDictionary 继承至 NSDictionary 所以, 初始化的那些方法不需要写了, 同时 removeObjectForKeys: 这种在数组种就做了防护, 所以也不需要了. 这些 API 不一定写全了, 但在日常开发中基本是够用了, 其他不常见的 API 添不添加也就显的不重要了.
最后在说点其他的东西.
很多人写的 swizzle 实现是写在 load 方法中的, 至于到底要不要写在 load 方法中, 我觉得还是依据个人项目的需要把.
首先, load 方法是在程序加载到内存的时候调用, 也就是程序启动前系统自动调用, 而且只会调用一次. 但是, 大量的 load 方法是会影响程序的启动时间, 虽然 load 方法只会调用一次, 但却可以手动调用. 所以, 又出现了在 load 方法中使用 GCD 的 dispatch_once 保证只会交换一次了.
个人觉得, 功能就应该具备开启和关闭的能力, 既然我可以开启防护, 那么就应该具备关闭防护的能力. 那么在 load 方法中写, 就不合适了
既然要具备开启和关闭的能力, 那么就需要做好过滤重复开启的操作
结束
除了数组, 字典, 字符串, 还有其他的都可以做防护, 比如: NSNotification 移除一个不存在的监听者, KVC 的设值了一个不存在的 key 或者 keyPath,KVO 监听了一个不存在的 keyPath, 或者移除了一个不存在的 keyPath, 消息发送中找不到方法的错误等等. 如果你将这些都加上防护, 那么一定会减少很多奔溃的出现.
来源: http://www.jianshu.com/p/38f8dfb1d5aa