iOS HOOK 中你不知道的那些事儿

1,795 阅读4分钟

关键词:运行时 runtime objc

hook.jpg

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 方法

demo 地址