Objective-C 的 MethodSwizzling

方法混淆在Objective-C中的使用还是比较常见的,要搞清楚它的本质,首先要理解两个概念。

一、运行时(runtime)

Objective-C是一门动态语言,有着非常灵活的运行时特性。runtime是由CC++、以及汇编语言编写的一套API,它为Objective-C提供了面向对象特性以及运行时特性。

二、方法的本质

Objective-C中,方法是由SELIMP组成的,前者叫做方法编号,后者叫方法实现。\
OC中调用方法叫做发送消息,发送消息前会先查找消息,查找过程就是通过SEL查找IMP的过程,另外,我们经常在代码中使用@selector(someMethod)这样的语法,@selector()叫做方法选择器,其返回的就是SEL,真正执行时会根据这个SEL查找对应的IMP

三、MethodSwizzling

3.1 原理

方法混淆就是利用runtime特性以及方法的组成本质实现的。比如有方法sel1对应imp1sel2对应imp2,经过方法混淆,使得runtime在方法查找时将sel1的查找结果变为imp2

3.2 实战

新建一个iOS工程,新建一个类Person,并为Person添加两个方法-walk-run

// Person.h
@interface Person : NSObject
- (void)walk;
- (void)run;
@end
// Person.m
@implementation Person
- (void)walk {
 NSLog(@"walk");
}
- (void)run {
 NSLog(@"run");
}
@end

新建一个NSObject的分类MethodSwizzling如下:

// NSObject+MethodSwizzling.h
@interface NSObject (MethodSwizzling)
+ (void)methodswizzlingWithClass:(Class)cls orgSEL:(SEL)orgSEL targetSEL:(SEL)targetSEL;
@end
// NSObject+MethodSwizzling.m
#import <objc/runtime.h>
@implementation NSObject (MethodSwizzling)
+ (void)methodswizzlingWithClass:(Class)cls orgSEL:(SEL)orgSEL targetSEL:(SEL)targetSEL {
 Method orgMethod = class_getInstanceMethod(cls, orgSEL);
 Method tgtMethod = class_getInstanceMethod(cls, targetSEL);
 method_exchangeImplementations(orgMethod, tgtMethod);
}
@end

Person.m中重写+load方法:

+ (void)load {
 static dispatch_once_t onceToken;
 dispatch_once(&onceToken, ^{
 [self methodswizzlingWithClass:self
 orgSEL:@selector(walk)
 targetSEL:@selector(run)];
 });
}

ViewController-viewDidLoad中调用-walk

- (void)viewDidLoad {
 [super viewDidLoad];
 Person *person = [[Person alloc] init];
 [person walk];
}

运行程序,控制台输出如下:

2020-02-09 21:26:07.613230+0800 TestObjC[1716:74628] run

调用的是-walk,实际执行的是-run,完成了方法混淆。

3.3 方法混淆的坑点

如果只声明了-run,但是没有实现,运行后会执行-walk方法,也就是不会交换成功,但如果没实现-walk,实现了-run,程序并不会执行-run,而是直接会崩掉,因为我们是显示的调用了-walk方法。因此在做方法混淆时,需要考虑方法是否实现的场景。\
如果有人把子类的方法和父类的方法进行交换,那么父类在调用该方法时就会出现问题,所以还得考虑交换的是否是本类的方法。\
基于以上的坑点,为了程序的健壮性,需要把方法混淆的函数修改一下:

@implementation NSObject (MethodSwizzling)
+ (void)methodswizzlingWithClass:(Class)cls orgSEL:(SEL)orgSEL targetSEL:(SEL)targetSEL {
 if (nil == cls) {
 NSLog(@"methodswizzlingWithClass: nil class");
 return;
 }
 Method orgMethod = class_getInstanceMethod(cls, orgSEL);
 Method tgtMethod = class_getInstanceMethod(cls, targetSEL);
 // 1.当原方法未实现,添加方法,并设置IMP为空实现
 if (nil == orgMethod) {
 class_addMethod(cls, orgSEL, method_getImplementation(tgtMethod), method_getTypeEncoding(tgtMethod));
 method_setImplementation(tgtMethod,
 imp_implementationWithBlock(^(id self, SEL _cmd) {
 NSLog(@"methodswizzlingWithClass: %@, orgMethod is nil", cls);
 }));
 return;
 }
 // 2.在当前类添加原方法,添加失败,说明当前类实现了该方法
 BOOL didAddMethod = class_addMethod(cls,
 orgSEL,
 method_getImplementation(orgMethod),
 method_getTypeEncoding(orgMethod));
 if (didAddMethod) {
 // 添加成功,当前类未实现,父类实现了orgSEL
 // 此时不必做方法交换,直接将tgtMethod的IMP替换为父类orgMethod的IMP即可
 class_replaceMethod(cls,
 targetSEL,
 method_getImplementation(orgMethod),
 method_getTypeEncoding(orgMethod));
 } else {
 // 正常情况,直接交换
 method_exchangeImplementations(orgMethod, tgtMethod);
 }
}
@end

验证一下效果,(Person-walk-run都实现),并添加一个-jump方法:

// Person.m
- (void)jump {
 NSLog(@"jump");
}

新建一个类Student继承自Person,并添加一个方法-study

// Student.h
@interface Student : Person
- (void)study;
@end
// Student.m
@implementation Student
+ (void)load {
 static dispatch_once_t onceToken;
 dispatch_once(&onceToken, ^{
 [self methodswizzlingWithClass:self
 orgSEL:@selector(jump)
 targetSEL:@selector(study)];
 });
}
- (void)study {
 NSLog(@"study");
}
@end

然后viewDidLoad中修改如下:

// ViewController.m
- (void)viewDidLoad {
 [super viewDidLoad];
 Person *person = [[Person alloc] init];
 [person walk];
 Student *std = [[Student alloc] init];
 [std study];
 [person jump];
}

运行以后,输出结果如下:

2020-02-09 22:38:55.180903+0800 TestObjC[2551:122647] run
2020-02-09 22:38:55.181070+0800 TestObjC[2551:122647] jump
2020-02-09 22:38:55.181185+0800 TestObjC[2551:122647] jump

Student中,并没有实现-jump,但它把-jump-study做了交换(实际实现是在Student中添加了-jump,然后将其IMP置为Person-jumpIMP),这样以来,Student调用-study就执行了-jumpIMP,效果上相当于做了交换,而父类Person调用-jump并没有受到影响。\
把父类Person-jump的实现注释掉,同时注释掉viewDidLoad中第8行[person jump],运行程序,输出如下:

2020-02-09 22:49:39.611851+0800 TestObjC[2667:129133] run
2020-02-09 22:49:39.612015+0800 TestObjC[2667:129133] methodswizzlingWithClass: Student, orgMethod is nil
作者:OSMin

%s 个评论

要回复文章请先登录注册