奇妙的Hook编程思想

2018-06-07  本文已影响95人  超人猿

序言:

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的变化

In the end.实验效果完成
上一篇 下一篇

猜你喜欢

热点阅读