Method Swizzling
方法交换,这是发生在Runtime
中的一种处理两个方法交换的手段,它处理的是什么?怎么处理的?今天就来探究一下。
method swizzling
是在runtime将一个方法的实现替换为另一个方法的实现。在前面的篇章中,我们知道,一个方法的实现是需要sel、imp
对应的,通过sel
就能找到imp
。method swizzling
正是通过改变sel、imp
指向来实现方法的交换。这也是Runtime
的一种应用。
下面是sel、imp
交换前后示意图
经过method swizzling
,也可以理解为imp swizzling
,将二者的sel、imp
,通过更改实现
我们是在什么时间去处理这个调用转换呢?我们可以选择在+load
调用时(也有用在initialize
)。由前面的篇章我们了解到,如果类实现+load
,使得该类成为非懒加载类,这个方法系统调用的时间会很早,所以在runtime
过程中,放在此处是很合适的。
先来看看处理方法交换的相关API,苹果文档显示右如下的方法是处理方法可能用到的:
method_invoke 调用指定方法的实现。
method_invoke_stret 调用返回数据结构的指定方法的实现。
method_getName 返回方法的名称。
method_getTypeEncoding 返回描述方法参数和返回类型的字符串。
method_copyReturnType 返回描述方法返回类型的字符串。
method_copyArgumentType 返回描述方法的单个参数类型的字符串。
method_getReturnType 通过引用返回描述方法返回类型的字符串。
method_getNumberOfArguments 返回方法接受的参数数量。
method_getArgumentType 通过引用返回描述方法的单个参数类型的字符串。
method_getDescription 返回指定方法的方法描述结构。
method_setImplementation 设置方法的实现。
method_exchangeImplementations 交换两种方法的实现。
method_getImplementation 返回方法的实现。
实操
由于选择在+load
中处理方法交换,+load
可能被多次调用,那就得保证方法交换仅仅执行一次,所以采用单例模式处理:
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
/** 保证执行一次交换
* 定义一个Runtime工具类,在其中实现交换
* animalInstanceMethod:原始待替换方法
* smallCatInstanceMethod:替换之后的方法
*/
[TLRuntimeTool runtimeMethodSwizzlingWithClass:self originSEL:@selector(animalInstanceMethod) swizzlingSEL:@selector(smallCatInstanceMethod)];
});
}
以下是TLRuntimeTool
的实现
//--------TLRuntimeTool.h----------
@interface TLRuntimeTool : NSObject
+(void)runtimeMethodSwizzlingWithClass:(Class)targetCls originSEL:(SEL)oriSel swizzlingSEL:(SEL)swizzlingSel;
@end
//--------TLRuntimeTool.m ----------
#import "TLRuntimeTool.h"
#import <objc/runtime.h>
@implementation TLRuntimeTool
+(void)runtimeMethodSwizzlingWithClass:(Class)targetCls originSEL:(SEL)oriSel swizzlingSEL:(SEL)swizzlingSel
{
if (!targetCls) {
NSLog(@"传入的类不能为空");
return;
}
Method originMethod = class_getInstanceMethod(targetCls, oriSel);
Method swizzlingMethod = class_getInstanceMethod(targetCls, swizzlingSel);
method_exchangeImplementations(originMethod, swizzlingMethod);
}
现在对方法替换做一个测试,定义一个Animal类、Cat类、Cat+small分类
,并在VC页面对其调用
以下是各类实现
- Animal
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Animal : NSObject
-(void)animalInstanceMethod;
+(void)animalClassMethod;
@end
NS_ASSUME_NONNULL_END
// -----------------------
#import "Animal.h"
@implementation Animal
- (void)animalInstanceMethod{
NSLog(@"animal instance method:%s",__func__);
}
+ (void)animalClassMethod{
NSLog(@"animal class metohd: %s",__func__);
}
@end
- Cat
#import "Animal.h"
NS_ASSUME_NONNULL_BEGIN
@interface Cat : Animal
@end
NS_ASSUME_NONNULL_END
#import "Cat.h"
@implementation Cat
- (void)animalInstanceMethod{
NSLog(@"cat instance method:%s",__func__);
}
@end
- Cat+small
#import "Cat.h"
NS_ASSUME_NONNULL_BEGIN
@interface Cat (small)
@end
NS_ASSUME_NONNULL_END
//----------------
#import "Cat+small.h"
#import "TLRuntimeTool.h"
@implementation Cat (small)
+ (void)load
{
/** 保证执行一次交换
* 定义一个Runtime工具类,在其中实现交换
* animalInstanceMethod:原始待替换方法
* smallCatInstanceMethod:替换之后的方法
*/
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[TLRuntimeTool runtimeMethodSwizzlingWithClass:self originSEL:@selector(animalInstanceMethod) swizzlingSEL:@selector(smallCatInstanceMethod)];
});
}
- (void)smallCatInstanceMethod
{
NSLog(@"给 small cat 分类添加了实例方法:%s",__func__);
[self smallCatInstanceMethod];
}
@end
正常情况下,这样就能处理运行时对方法的替换,不会有什么问题。但会有一些坑点,这样是不完善的,下面来看一看
坑点1:[self smallCatInstanceMethod]
是否会产生递归
这里可能会有一个面试点,这样是否会产生递归呢?答案是不会,因为经过方法替换,自己调用自己的 smallCatInstanceMethod,其实该originSel
指向的是swizzlingIMP
,而swizzlingIMP
指向的又会是originIMP
,不会产生递归。
方法替换结果
坑点2:父类的方法,父类实现了,子类未实现
Animal类实现了一个方法animalInstanceMethod()
,继承它的一个子类Cat未实现该方法animalInstanceMethod()
,然后需要替换的就是该子类Cat的animalInstanceMethod()
,而子类就会因为未实现而奔溃。
奔溃提示
-[Animal smallCatInstanceMethod]
找不到,就是因为在TLRuntimTool
在方法替换过程中,分类替换方法中去执行[self smallCatInstanceMethod]
,Animal 的animalInstanceMethod
的实现已经是指向Cat类的smallCatInstanceMethod
,而分类中未实现animalInstanceMethod
,当Animal去查找对应IMP时,是查找不到smallCatInstanceMethod
,所以就崩溃了。为了解决该情况,我们就需要在TLRuntimTool
中对特殊情况做处理:
//--------TLRuntimeTool.h----------
@interface TLRuntimeTool : NSObject
+(void)runtimeMethodSwizzlingWithClass:(Class)targetCls originSEL:(SEL)oriSel swizzlingSEL:(SEL)swizzlingSel;
@end
//--------TLRuntimeTool.m ----------
#import "TLRuntimeTool.h"
#import <objc/runtime.h>
@implementation TLRuntimeTool
+(void)runtimeMethodSwizzlingWithClass:(Class)targetCls originSEL:(SEL)oriSel swizzlingSEL:(SEL)swizzlingSel
{
if (!targetCls) {
NSLog(@"传入的类不能为空");
return;
}
Method originMethod = class_getInstanceMethod(targetCls, oriSel);
Method swizzlingMethod = class_getInstanceMethod(targetCls, swizzlingSel);
BOOL swizzlingResult = class_addMethod(targetCls,
oriSel,
method_getImplementation(swizzlingMethod),
method_getTypeEncoding(originMethod));
/**
* 判断是否能添加成功;YES->表明对象类没有方法,重写一个实现方法
*/
if (swizzlingResult) {
class_replaceMethod(targetCls,
swizzlingSel,
method_getImplementation(originMethod),
method_getTypeEncoding(originMethod));
}else{
// 原有类有实现方法
method_exchangeImplementations(originMethod, swizzlingMethod);
}
}
先获取到原有方法和将替换方法,对二者做一个操作,向具有给定名称和实现的目标类中添加新方法.
1.如果返回YES,表明目标类没有方法,重写一个实现方法,替换给定类的方法的实现。即originSEL_A -> originIMP_B -> swizzlingSEL - > originIMP_B
.
2.如果返回NO,则该类有给定方法的实现,则替换两个方法的实现;即originSEL_B -> oringIMP_A
.
这样就处理之后优化了
坑点3:父类子类都不曾实现需要替换的方法
假设需要替换Cat类的一个animalInstanceMethod,父类声明了方法animalInstanceMethod
,子类父类都未实现,按坑点2优化后的结果依然是有问题的。
按照
- (void)smallCatInstanceMethod
{
NSLog(@"给 small cat 分类添加了实例方法:%s",__func__);
[self smallCatInstanceMethod];
}
这个时候就会出现递归
原因是,子类父类都未实现
animalInstanceMethod
,在Cat交换smallCatInstanceMethod
时再次调用自身就会产生递归。为了避免这个情况,那就还需要优化:
@implementation TLRuntimeTool
+(void)runtimeMethodSwizzlingWithClass:(Class)targetCls originSEL:(SEL)oriSel swizzlingSEL:(SEL)swizzlingSel
{
if (!targetCls) {
NSLog(@"传入的类不能为空");
return;
}
Method originMethod = class_getInstanceMethod(targetCls, oriSel);
Method swizzlingMethod = class_getInstanceMethod(targetCls, swizzlingSel);
if (!originMethod) {
// 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现
class_addMethod(targetCls, oriSel, method_getImplementation(swizzlingMethod), method_getTypeEncoding(swizzlingMethod));
method_setImplementation(swizzlingMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
}
BOOL swizzlingResult = class_addMethod(targetCls,
oriSel,
method_getImplementation(swizzlingMethod),
method_getTypeEncoding(originMethod));
/**
* 判断是否能添加成功;如果成功,表明对象类没有方法,重写一个实现方法
*/
if (swizzlingResult) {
class_replaceMethod(targetCls,
swizzlingSel,
method_getImplementation(originMethod),
method_getTypeEncoding(originMethod));
}else{
// 原有类有实现方法
method_exchangeImplementations(originMethod, swizzlingMethod);
}
}
添加了一个对原有方法的判断,避免对没有实现的方法进行替换而出错,在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现。
这样就非常棒的解决了几个特殊情况下导致的错误。
方法替换的应用
method-swizzling最常用的应用是防止数组越界奔溃、字典取值崩溃等情况。
在iOS中NSNumber、NSArray、NSDictionary等这些类都是类簇,NSArray的实现时可能会由多个类组成。所以如果想对NSArray进行替换,必须获取到其本类(__NSArrayM
)进行替换,直接对NSArray进行操作是无效的。类簇详情请参看Apple文档class cluster的内容.
// 开发过程中断点时留意下就知道其底层的类,如 mutA __NSArrayM * @"2 elements" 0x00006000036fd770
类簇 “真身”
NSArray ---> __NSArrayI
NSMutableArray ---> __NSArrayM
NSDictionary ---> __NSDictionaryI
NSMutableDictionary ---> __NSDictionaryM
NSNumber ---> __NSCFNumber
以NSArray
为例,新建一个NSArray
分类,在里面添加以下方法,区分开发模式
和发布模式
。
#import "NSArray+AvoidCrash.h"
#import "TLRuntimeTool.h"
#import <objc/runtime.h>
@implementation NSArray (AvoidCrash)
+ (void)load
{
/** 保证执行一次交换
* 定义一个Runtime工具类,在其中实现交换
* animalInstanceMethod:原始待替换方法
* smallCatInstanceMethod:替换之后的方法
*/
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[TLRuntimeTool runtimeMethodSwizzlingWithClass:NSClassFromString(@"__NSArrayI")
originSEL:@selector(objectAtIndex:)
swizzlingSEL:@selector(avoidCrashObjectsAtIndexes:)];
});
}
//__NSArrayI objectAtIndex:
- (id)avoidCrashObjectsAtIndexes:(NSUInteger)index {
#ifdef DEBUG // 开发模式
return [self avoidCrashObjectsAtIndexes:index];
#else // 发布模式
id object = nil;
@try {
object = [self avoidCrashObjectsAtIndexes:index];
}
@catch (NSException *exception) {
// 捕捉到的错误
NSLog(@"** Exception class :%s ** Exception Method: %s \n", class_getName(self.class), __func__);
NSLog(@"Uncaught exception description: %@", exception);
NSLog(@"%@", [exception callStackSymbols]);
}
@finally {
return object;
}
#endif
}
@end
写一个数组,取值测试如下
NSArray * mutA = @[@"3",@"2"];
NSLog(@"%@",[mutA objectAtIndex:3]);
-
开发模式
该崩还是让它崩 -
模拟发布模式
读取崩溃错误
打印了错误日志,但程序不会崩溃
网友评论