奇妙的Hook编程思想
序言:
iOS中的Hook思想是我们探索runtime黑魔法的必经之路,好的程序猿是终身不断学习的开发者,一环接一环必历经九九八十一难,祝我们早日取得真经🌹
一、我们该筹备的Runtime消息机制
通常情况下,我们将一个对象实例化,实例化过程代码如下所示:
Person *p = [[Person alloc] init];
那么,runtime底层是怎么样来实现的呢?
Runtime最重要的核心有三点:
1.SEL 方法编号
2.IMP 方法指针
3.隐式参数
一张图说明下:
我们开始使用runtime,首先需要在Apple LLVM 9.0 -Preprocessing
设置下
接着在主代码里
// 导入runtime库
#import <objc/message.h>
// Person *p = [[Person alloc] init];
Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
p = objc_msgSend(p, sel_registerName("init"));
NSLog(@"%p",p);
objc_msgSend这种方法我是从哪里知道的?你可以新建一个macOS Command Line Tool工程,创建Person类并且在main.m函数里敲Person *p = [[Person alloc] init];
然后在终端cd目录,敲~$ clang -rewrite-objc main.m
,会在目录里生成一个main.cpp文件,打开main.cpp搜索int main
, 你会发现如下这种情况,就会明了
拿到实例方法run
// Person.h里声明一个run方法
- (void)run;
// Person.m里实现run方法
- (void)run {
NSLog(@"奔跑吧!!!");
}
// 回到ViewController.m
Person *p = [[Person alloc] init];
[p run];
// 可以看到控制台输出了:奔跑吧!!!
// 这里我们把它变为底层的写法
Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
p = objc_msgSend(p, sel_registerName("init"));
objc_msgSend(p, sel_registerName("run"));
// 也是完美的输出。
当然,如果我们需要拿到父类方法,可以这么做:
// 新建一个CRPerson类继承于Person
// 配置CRPerson.h
- (void)run;
// 配置CRPerson.m 与 Person.m的实现方法做区分
- (void)run {
NSLog(@"CR奔跑吧!!!");
}
// 回到ViewController.m
CRPerson *p = objc_msgSend(objc_getClass("CRPerson"), sel_registerName("alloc"));
p = objc_msgSend(p, sel_registerName("init"));
objc_msgSend(p, sel_registerName("run"));
struct objc_super superP = {p, objc_getClass("Person")};
objc_msgSendSuper(&superP, sel_registerName("run"));
输出结果:
此时需要传参数该如何设置?
// Person.h
- (void)runWithStr:(NSString *)str;
// Person.m
- (void)runWithStr:(NSString *)str {
NSLog(@"奔跑吧,%@!!!", str);
}
// 回到ViewController.m,实现以前的操作
Person *p = [[Person alloc] init];
[p runWithStr:@"超人"];
// 修改成底层写法?
Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
p = objc_msgSend(p, sel_registerName("init"));
objc_msgSend(p, sel_registerName("runWithStr:"), @"超人");
打印 “奔跑吧,超人!!! ” 成功!
二、我们该筹备的动态添加方法
创建一个新的工程,新建Person类,什么也不做,但是在ViewController.m里面添加如下代码:
// 导入
#import "Person.h"
Person * p = [[Person alloc]init];
// 调用run方法
[p performSelector:@selector(run:) withObject:@"超人"];
// 运行看看
// 程序奔溃,Xcode说它发送消息给Person,Person来到实现方法,但是Person说我没有找到这个方法啊,这怎么办???
用runtime机制解决这个问题,进入Person.m
// 导入Runtime
#import <objc/message.h>
@implementation Person
// 储备的两个知识点:
// 1. 如果该类接收到一个没有实现的类方法,就会来这里
//+ (BOOL)resolveClassMethod:(SEL)sel
// 2.如果该类接收到一个没有实现的实例方法,就会来这里
// 注意区分两者,一个是实例方法、一个是类方法
// 而我们的需求是来到实例方法👇🏻
+ (BOOL)resolveInstanceMethod:(SEL)sel {
//添加一个方法run
if (sel == sel_registerName("run:")) {
/*
参数1: cls 哪个类?
参数2: SEL 方法编号
参数3: IMP 方法实现
参数4: 返回值类型!
*/
class_addMethod(self, sel, (IMP)runAction, "v@:");
}
return [super resolveInstanceMethod:sel];
}
//默认参数
void runAction(id selfObj,SEL sel,id obj){
NSLog(@"%@,你今天奔跑了吗? ",obj);
NSLog(@"%@:%@:%@ ", selfObj, NSStringFromSelector(sel), obj);
}
@end
这里说下,class_addMethod
class_addMethod(<#Class _Nullable __unsafe_unretained cls#>, <#SEL _Nonnull name#>, <#IMP _Nonnull imp#>, <#const char * _Nullable types#>)
第四个参数看的我懵逼,查看官方文档,它们是这么做的👇🏻
写上"v@:",此处是跟着官方走
三、实现Hook用法
上图想告诉你,我们程序的运行,+ (void)load;
这个函数的实现方法会比 main.m中的代码块要先运行,这是由编译器和运行库决定的,想多了解下,可以看雷纯峰的 Objective-C +load
新建一个UIViewController+hook Category, .m文件敲下代码:
#import "UIViewController+hook.h"
#import <objc/message.h>
@implementation UIViewController (hook)
+ (void)load {
/*
class_getInstanceMethod: 获取实例方法
class_getClassMethod: 获取工厂方法
*/
Method viewWillAppear = class_getInstanceMethod([self class], sel_registerName("viewWillAppear:"));
Method cr_viewWillAppear = class_getInstanceMethod([self class], sel_registerName("cr_viewWillAppear:"));
method_exchangeImplementations(viewWillAppear, cr_viewWillAppear);
}
- (void)cr_viewWillAppear:(BOOL)animated {
[self cr_viewWillAppear:YES];
NSLog(@"进来就给我打印当前的类:%@", [self class]);
}
@end
两张图解释下:
类似于书本中的目录和页数,一一对应,方法编号找到方法实现
但是由于我们使用了runtime中的交换方法,所以:
系统中的viewWillAppear:
交换成了我们所要实现的方法:cr_viewWillAppear:
- (void)cr_viewWillAppear:(BOOL)animated {
[self cr_viewWillAppear:YES]; // 因为做了交换动作,所以这句话解决了递归,程序正常运行!
NSLog(@"进来就给我打印当前的类:%@", [self class]);
}
每当我们接手一个新项目时,想要快速了解进了那个类可以用上面提供的hook这么做,当然也还有其他更强大的功能,可因人而异🤡!
最后,该拓展的自定义KVO, 想要了解KVO,可以看 杨萧玉的《Objective-C中的KVC和KVO》
新建一个新的工程并新建一个Person类,.h文件声明一个带copy属性的age属性对象,即@property (copy, nonatomic) NSString *age;
, 接着在ViewController.m做实验,这里实验效果是翻译iOS底层的教学直播,也是做笔记之用:
// 导入Person
#import "Person.h"
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
[person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
}
然后打断点调试,注意断点位置
此时,person 的 isa指针指向Person,没错,符合我们的常识
断点执行next
我们添加了KVO之后person的isa指针指向了操作系统为Person自动添加的子类:NSKVONotifying_Person
动态运行库魅力就是这么巨大!
这里我们探索它的原理,通过runtime自己自定义KVO
同样的,首先需要设置环境配置
其次是新建一个分类继承自NSObject,命名为kvo
在NSObject+kvo.h中,模仿系统添加kvo的写法,定义一个实例方法
#import <Foundation/Foundation.h>
@interface NSObject (kvo)
- (void)CR_addObserver:(nonnull NSObject *)observer forKeyPath:(nonnull NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end
NSObject+kvo.m
#import "NSObject+kvo.h"
#import <objc/message.h>
@implementation NSObject (kvo)
// MARK:-自定义的KVO
- (void)CR_addObserver:(nonnull NSObject *)observer forKeyPath:(nonnull NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
// 1.创建一个类
// Person CRKVO_Person
NSString * oldName = NSStringFromClass([self class]);
NSString * newName = [@"CRKVO_" stringByAppendingString:oldName];
// 添加类
//- 参数1.这个类继承谁
//- 参数2.类名
Class MyClass = objc_allocateClassPair([self class], newName.UTF8String, 0);
// 注册类
objc_registerClassPair(MyClass);
// 2.动态修改self的类型!!
object_setClass(self, MyClass);
// 3.重写setAge: -- 给子类对象添加setAge:方法!!
class_addMethod(MyClass, @selector(setAge:), (IMP)setAge, "v@:@"); // 默认为"v@:" , 有参数加@
// 4.将观察者绑定到对象上面
/*
OBJC_ASSOCIATION_ASSIGN 默认,修饰符为__weak
OBJC_ASSOCIATION_RETAIN_NONATOMIC 修饰符为__strong
*/
objc_setAssociatedObject(self, (__bridge const void *)@"objc", observer, OBJC_ASSOCIATION_ASSIGN);
}
void setAge(id self,SEL sel,NSString * newName){
struct objc_super person = {self, class_getSuperclass([self class])};
// 修改age属性!!
objc_msgSendSuper(&person, sel, newName);
// 拿出观察者!!
id observer = objc_getAssociatedObject(self, @"objc");
/ /调用observer
objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:),@"age",self,@{@"age":newName},nil);
}
@end
ViewController.m
#import "ViewController.h"
#import "Person.h"
#import "NSObject+kvo.h"
@interface ViewController ()
@property (strong, nonatomic) Person *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
// [person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
[person CR_addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
self.person = person;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"有输出");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
static int a = 18;
a++;
self.person.age = [NSString stringWithFormat:@"%d", a];
NSLog(@"超人的年龄是:%@", _person.age);
}
相同的,在这里打断点调试
isa指针果然指向我们刚刚创建的子类CRKVO_Person
通过点击屏幕,监听到了age的变化