关键词:运行时 runtime objc
HOOK:俗称钩子,通俗来讲是指通过运行时手段找到某个方法(勾住方法),然后在方法里面插入代码以实现我们自己的业务逻辑,其中最常见的业务场景便是无侵入埋点。在 iOS 中通常采用 runtime 技术来实现。下面以3个问题引出今天的内容。
一.如何实现 HOOK?
这个问题很简单,大家肯定都会,下面以 HOOK UIViewController 的 viewDidAppear 方法为例。为了便于讲解,先约定2个概念,如下:
待 HOOK 方法:比如 viewDidAppear(也就是后面的 sourceMethod)
HOOK 方法:比如 hook_viewDidAppear(也就是后面的 targetMethod)
1. 获取方法
用 class_getInstanceMethod 分别获取待 HOOK 的方法(sourceMethod)及 HOOK 方法(targetMethod)。class_getInstanceMethod 有个特点,若自己没有某个方法实现,会从其父类中查找。
// Note that this function searches superclasses for implementations
Method sourceMethod = class_getInstanceMethod(classObject, sourceSelector);
Method targetMethod = class_getInstanceMethod(classObject, targetSelector);
2. 添加方法
先尝试给待 HOOK 的方法添加方法实现,若添加成功,则反过来需要用待 HOOK 的方法实现替换掉 HOOK 的方法实现。
// `class_addMethod` will add an override of a superclass's implementation,
// but will not replace an existing implementation in this class
BOOL isAdd = class_addMethod(classObject, sourceSelector, method_getImplementation(targetMethod), method_getTypeEncoding(targetMethod));
if (isAdd) {
class_replaceMethod(classObject, targetSelector, method_getImplementation(sourceMethod), method_getTypeEncoding(sourceMethod));
}
3. 交换方法
若第2步添加失败,则表示待 HOOK 的方法已经存在对应的方法实现,此时直接交换2个方法的实现即可。
method_exchangeImplementations(sourceMethod, targetMethod);
完整代码如下:
#import "UIViewController+hook.h"
#import <objc/runtime.h>
@implementation UIViewController (hook)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL sourceSelector = @selector(viewDidAppear:);
SEL targetSelector = @selector(hook_viewDidAppear:);
[self hookClass:self sourceSelector:sourceSelector targetSelector:targetSelector];
});
}
+ (void)hookClass:(Class)classObject sourceSelector:(SEL)sourceSelector targetSelector:(SEL)targetSelector {
Method sourceMethod = class_getInstanceMethod(classObject, sourceSelector);
Method targetMethod = class_getInstanceMethod(classObject, targetSelector);
BOOL isAdd = class_addMethod(classObject, sourceSelector, method_getImplementation(targetMethod), method_getTypeEncoding(targetMethod));
if (isAdd) {
class_replaceMethod(classObject, targetSelector, method_getImplementation(sourceMethod), method_getTypeEncoding(sourceMethod));
} else {
method_exchangeImplementations(sourceMethod, targetMethod);
}
}
- (void)hook_viewDidAppear:(BOOL)animated {
NSLog(@"----------hook----------");
[self hook_viewDidAppear:animated];
}
@end
二.2个扩展同时 HOOK 同一个方法会怎样?
具体来说,就是写了2个分类 hook1、hook2,在 hook1、hook2 中都同时 HOOK 了某个方法,下面以 HOOK viewDidAppear 方法为例。
UIViewController+hook1.m
---------------.m----------------------
#import "UIViewController+hook1.h"
#import <objc/runtime.h>
@implementation UIViewController (hook1)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL sourceSelector = @selector(viewDidAppear:);
SEL targetSelector = @selector(hook1_viewDidAppear:);
[self hookClass:self sourceSelector:sourceSelector targetSelector:targetSelector];
});
}
+ (void)hookClass:(Class)classObject sourceSelector:(SEL)sourceSelector targetSelector:(SEL)targetSelector {
Method sourceMethod = class_getInstanceMethod(classObject, sourceSelector);
Method targetMethod = class_getInstanceMethod(classObject, targetSelector);
BOOL isAdd = class_addMethod(classObject, sourceSelector, method_getImplementation(targetMethod), method_getTypeEncoding(targetMethod));
if (isAdd) {
class_replaceMethod(classObject, targetSelector, method_getImplementation(sourceMethod), method_getTypeEncoding(sourceMethod));
} else {
method_exchangeImplementations(sourceMethod, targetMethod);
}
}
- (void)hook1_viewDidAppear:(BOOL)animated {
NSLog(@"----------hook1----------");
[self hook1_viewDidAppear:animated];
}
@end
UIViewController+hook2.m
---------------.m----------------------
#import "UIViewController+hook2.h"
#import <objc/runtime.h>
@implementation UIViewController (hook2)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL sourceSelector = @selector(viewDidAppear:);
SEL targetSelector = @selector(hook2_viewDidAppear:);
[self hookClass:self sourceSelector:sourceSelector targetSelector:targetSelector];
});
}
+ (void)hookClass:(Class)classObject sourceSelector:(SEL)sourceSelector targetSelector:(SEL)targetSelector {
Method sourceMethod = class_getInstanceMethod(classObject, sourceSelector);
Method targetMethod = class_getInstanceMethod(classObject, targetSelector);
BOOL isAdd = class_addMethod(classObject, sourceSelector, method_getImplementation(targetMethod), method_getTypeEncoding(targetMethod));
if (isAdd) {
class_replaceMethod(classObject, targetSelector, method_getImplementation(sourceMethod), method_getTypeEncoding(sourceMethod));
} else {
method_exchangeImplementations(sourceMethod, targetMethod);
}
}
- (void)hook2_viewDidAppear:(BOOL)animated {
NSLog(@"----------hook2----------");
[self hook2_viewDidAppear:animated];
}
@end
结论:
- 打印结果
// UIViewController+hook1.m 先编译
2021-08-07 22:26:17.798096+0800 hook[6663:235978] ----------A
2021-08-07 22:26:17.804583+0800 hook[6663:235978] ----------hook2----------
2021-08-07 22:26:17.804987+0800 hook[6663:235978] ----------hook1----------
// UIViewController+hook2.m 先编译
2021-08-07 22:33:46.783382+0800 hook[6733:240232] ----------A
2021-08-07 22:33:46.788859+0800 hook[6733:240232] ----------hook1----------
2021-08-07 22:33:46.789052+0800 hook[6733:240232] ----------hook2----------
- 2种情况,不同结果,其实归根结蒂是 load 方法调用顺序问题,load 方法调用顺序是先编译先调用 load
- 不管有几个分类同时 HOOK 同一个方法,最终都会成功,只是 HOOK 的结果顺序依赖于分类编译的先后顺序
三.子类 A 继承自父类 B,B 有实现方法 print,HOOK A 的 print 方法会怎样?
AViewController
---------------.h----------------------
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface AViewController : UIViewController
@end
NS_ASSUME_NONNULL_END
---------------.m----------------------
#import "AViewController.h"
@implementation AViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)test {
NSLog(@"----------A");
}
@end
BViewController
---------------.h----------------------
#import "AViewController.h"
NS_ASSUME_NONNULL_BEGIN
@interface BViewController : AViewController
@end
NS_ASSUME_NONNULL_END
---------------.m----------------------
#import "BViewController.h"
@implementation BViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
@end
BViewController+hook
---------------.m----------------------
#import "BViewController+hook.h"
#import <objc/runtime.h>
@implementation BViewController (hook)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL sourceSelector = @selector(test);
SEL targetSelector = @selector(hook_test);
[self hookClass:self sourceSelector:sourceSelector targetSelector:targetSelector];
});
}
+ (void)hookClass:(Class)classObject sourceSelector:(SEL)sourceSelector targetSelector:(SEL)targetSelector {
Method sourceMethod = class_getInstanceMethod(classObject, sourceSelector);
Method targetMethod = class_getInstanceMethod(classObject, targetSelector);
BOOL isAdd = class_addMethod(classObject, sourceSelector, method_getImplementation(targetMethod), method_getTypeEncoding(targetMethod));
if (isAdd) {
NSLog(@"----------isAdd----------");
class_replaceMethod(classObject, targetSelector, method_getImplementation(sourceMethod), method_getTypeEncoding(sourceMethod));
} else {
method_exchangeImplementations(sourceMethod, targetMethod);
}
}
- (void)hook_test {
NSLog(@"----------hook_test----------");
[self hook_test];
}
@end
结论:
- 打印结果
2021-08-07 20:56:18.344068+0800 hook[5914:201030] ----------isAdd----------
2021-08-07 20:56:18.521663+0800 hook[5914:201030] ----------hook_test----------
2021-08-07 20:56:18.521819+0800 hook[5914:201030] ----------A
- 虽然 BViewController 没有实现 test 方法,但是 HOOK 成功
- 若子类没有实现某个方法,则 class_getInstanceMethod 会从其父类中查找,故最终 HOOK 的是父类中的 test 方法