一、知识储备 --> 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
![](https://img.haomeiwen.com/i24396915/622d6fbe00c20cd1.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];
日志如下:
![](https://img.haomeiwen.com/i24396915/39ac78c0ee431469.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面试基础知识 (一)
https://github.com/iOS-Mayday/heji
iOS面试基础知识 (二)
https://github.com/iOS-Mayday/heji
iOS面试基础知识 (三)
https://github.com/iOS-Mayday/heji
iOS面试基础知识 (四)
https://github.com/iOS-Mayday/heji
iOS面试基础知识 (五)
https://github.com/iOS-Mayday/heji
网友评论