iOS 代码注入— hook 实践

·  阅读 3731

一、知识储备 --> SEL、Method、IMP

一 "码" 当先,先看一下 objc_method 结构体

runtime.h
/// An opaque type that represents a method in a class definition.代表类定义中一个方法的不透明类型
typedef struct objc_method *Method;

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}

复制代码

我们来看下objc_method这个结构体的内容:

SEL method_name 方法名 char *method_types 方法类型 IMP method_imp 方法实现

1、SEL

Objc.h
/// An opaque type that represents a method selector.代表一个方法的不透明类型
typedef struct objc_selector *SEL;

复制代码

这里要先说明下selector和SEL的关系,selector是SEL的一个实例,相当于一个方法的代号,可以快速找到方法。

因为不同Class可以有相同名字的方法,这样就可以通过 Class + selector 快速定位到一个函数,从而拿到函数指针 IMP 。

在iOS中,runtime会在运行的时候,通过load函数,将所有的method hash然后map到set中。这样在运行的时候,寻找selector的速度就会非常快,不会因为runtime特性牺牲太多的性能。

这也带来了一个弊端,我们在写C代码的时候,经常会用到函数重载,就是函数名相同,参数不同,但是这在Objective-C中是行不通的,因为selector只记了method的name,没有参数,所以没法区分不同的method。

2、IMP

/// A pointer to the function of a method implementation.  指向一个方法实现的指针
typedef id (*IMP)(id, SEL, ...); 
#endif

复制代码

简单来说就是函数指针,用来找到具体实现使用。

3、method_types

const char *types——函数类型编码(包括返回值类型、参数类型),iOS提供了一个@encode指令,可以将具体的类型表示成字符串编码,也就是通过字符串来表示类型。主要目的是为了方便运行时,将函数的返回值和参数的类型通过字符串来描述并且存储。

       NSLog(@"%s",@encode(int));
       NSLog(@"%s",@encode(float));
       NSLog(@"%s",@encode(int *));
       NSLog(@"%s",@encode(id));
       NSLog(@"%s",@encode(void));
       NSLog(@"%s",@encode(SEL));
       NSLog(@"%s",@encode(float *));

复制代码

输出

2022-01-04 17:59:45.269504+0800 Runtime[71731:1839447] i
2022-01-04 17:59:45.269606+0800 Runtime[71731:1839447] f
2022-01-04 17:59:45.269692+0800 Runtime[71731:1839447] ^i
2022-01-04 17:59:45.269775+0800 Runtime[71731:1839447] @
2022-01-04 17:59:45.269853+0800 Runtime[71731:1839447] v
2022-01-04 17:59:45.269934+0800 Runtime[71731:1839447] :
2022-01-04 17:59:45.270033+0800 Runtime[71731:1839447] ^f

复制代码

image.png

例如- (int)test:(int)age height:(float)height,我们知道OC方法对应的底层函数前两个是默认参数id self 和SEL cmd,那么刚才的方法从左到右,返回值和参数的类型分别为int->id->SEL->int->float,转换成类型编码,就是i-@-:-i-f,而最终系统是这样表示的i24@0:8i16f20,你应该会好奇,里面怎么多了一些数字,其实它们是用来描述函数的参数的长度和位置的的,从左到右可以这么解读:

  • i —— 函数的返回值类型为int
  • 24 —— 参数所占的总长度(24字节)
  • @ —— 第一个参数id
  • 0 —— 第一个参数在内存中的起始偏移量(0字节,也就是从第0个字节开始算起)
  • : —— 第二个参数SEL
  • 8 —— 第二个参数在内存中的起始偏移量(8字节,也就是从第8个字节开始算起,因此上面的id参数占之前的8个字节)
  • i —— 第三个参数int
  • 16 —— 第三个参数在内存中的起始偏移量(16字节,也就是从第16个字节开始算起,因此上面的SEL参数占用了之前的8个字节)
  • f —— 第四个参数float
  • 20 —— 第四个参数在内存中的起始偏移量(20字节,也就是从第20个字节开始算起,因此上面的int参数占用了前面的4个字节,而总长度为24,因此最后的4个字节是给float参数用的)

二、代码注入

1、方法交换

**适用场景:**新方法和需要被代码注入的方法是相同的Class

实例:给 ViewController 的 viewDidLoad 方法进行代码注入

新建ViewController分类:ViewController+hook

#import "ViewController+hook.h"
#import <objc/runtime.h>

@implementation ViewController (hook)
+ (void)load {
    ///>>>获取原方法
    Method origMethod = class_getInstanceMethod(self.class, @selector(viewDidLoad));
    ///>>>获取hook新方法
    Method hookMethod = class_getInstanceMethod(self.class, @selector(hook_viewDidLoad));
    ///>>>方法交换
    method_exchangeImplementations(origMethod, hookMethod);
}

- (void)hook_viewDidLoad {
    NSLog(@"调用viewDidLoad方法前注入代码");

    [self hook_viewDidLoad];

    NSLog(@"调用viewDidLoad方法后注入代码");
}
@end

复制代码

这样,在走到viewDidLoad时就会走到hook_viewDidLoad,然后在由于方法交换,在hook_viewDidLoad中调hook_viewDidLoad就会调到原方法,从而实现代码注入。

2、添加方法、替换实现

**适用场景:**新方法和需要被代码注入的方法是不同的Class。

为什么无法使用方法交换来实现? 此时如果仍然使用方法交换,如果老方法的内部实现使用的Class的变量,由于调用方是新Class --> self已经是新的Class,变量将无法被找到,造成crash!!

**实例演示:**我们先建一个测试类 ClassA,里面提供两个实例方法,一个类方法,代码如下:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface ClassA : NSObject

- (void)test1;

- (void)test2;

+ (void)testClass;

@end

NS_ASSUME_NONNULL_END

复制代码
#import "ClassA.h"

@interface ClassA()

@end

@implementation ClassA

- (void)test1{
    NSLog(@"A Test1");
}

- (void)test2{
    NSLog(@"A Test2");
}

+ (void)testClass {
    NSLog(@"A TestClass");
}

@end

复制代码

2.1 实例方法代码注入

新建ClassB,在load方法中进行ClassA的 添加方法、替换实现

+ (void)load {
    ///>>>需要代码注入的类
    Class origClass = NSClassFromString(@"ClassA");
    ///>>>需要代码注入的实例方法
    NSArray *arr = @[@"test1",@"test2"];
    for (NSString *st in arr) {
        ///>>>获取原类方法
        Method origMethod = class_getInstanceMethod(origClass, NSSelectorFromString(st));
        ///>>>生成新的Selector
        NSString *newName = [NSString stringWithFormat:@"%@_add",st];
        SEL newSelector = NSSelectorFromString(newName);
        ///>>>将新的Selector添加给原类并指向原函数实现
        class_addMethod(origClass,newSelector, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));

        ///>>>将原方法的IMP均指向 ClassB 的 test
        IMP hookIMP = [[self new] methodForSelector:@selector(test)];
        method_setImplementation(origMethod, hookIMP);
    }
}

- (void)test {
    NSString * selName = NSStringFromSelector(_cmd);
    NSLog(@"%@执行之前代码注入",selName);
    NSString *new = [NSString stringWithFormat:@"%@_add",selName];
    [self performSelector:NSSelectorFromString(new)];
    NSLog(@"%@执行之后代码注入",selName);
}

复制代码

执行代码:

    ClassA *a = [ClassA new];
    [a test1];
    [a test2];

复制代码

日志如下:

image.png

2.2 类方法代码注入

///>>>类方法代码注入
    Method origClassMethod = class_getClassMethod(origClass, NSSelectorFromString(@"testClass"));

    class_addMethod(object_getClass(origClass),NSSelectorFromString(@"testClass_add"), method_getImplementation(origClassMethod), method_getTypeEncoding(origClassMethod));
    SEL mClassSel = @selector(testClass);
    IMP hookClassIMP = [self methodForSelector:mClassSel];
    method_setImplementation(origClassMethod, hookClassIMP);

复制代码

这里需要注意的是,由于是类方法,方法存在于元类的方法列表,因此需要将方法添加到元类

object_getClass(origClass)

复制代码

此时调用ClassA的类方法就会调到ClassB的testClass,实现类方法的代码注入

+ (void)testClass {
    NSLog(@"testClass执行之前代码注入");
    [self performSelector:NSSelectorFromString(@"testClass_add")];
    NSLog(@"testClass执行之后代码注入");
}
复制代码
分类:
iOS
标签:
收藏成功!
已添加到「」, 点击更改