iOS 如何优雅地 hook 系统的 delegate 方法?

在 iOS 开发中我们经常需要去 hook 系统方法,来满足一些特定的应用场景。

比如使用 Swizzling 来实现一些 AOP 的日志功能,比较常见的例子是 hook UIViewControllerviewDidLoad ,动态为其插入日志。

这当然是一个很经典的例子,能让开发者迅速了解这个知识点。不过正如现在的娱乐圈,diss 天 diss 地,如果我们也想 hook 天,hook 地,顺便 hook 一下系统的 delegate 方法,该怎么做呢?

所以就进入今天的主题:如何优雅地 hook 系统的 delegate 方法?

hook 系统类的实例方法

首先,我们回想一下 hook UIViewControllerviewDidLoad 方法,我们需要使用 category,为什么需要 category 呢?因为在 category 里面才能在不入侵源码的情况下,拿到实例方法 viewDidLoad ,并实现替换:

12345678910111213141516171819
#import <objc/runtime.h>@implementation  (swizzling)+ (void)load {        Method fromMethod = class_getInstanceMethod([self class], @selector(viewDidLoad));    Method toMethod = class_getInstanceMethod([self class], @selector(swizzlingViewDidLoad));    // 这里直接交换方法,不做判断,因为 UIViewController 的 viewDidLoad 肯定实现了。    method_exchangeImplementations(fromMethod, toMethod);}// 我们自己实现的方法,也就是和self的viewDidLoad方法进行交换的方法。- (void)swizzlingViewDidLoad {    NSString *str = [NSString stringWithFormat:@"%@", self.class];    NSLog(@"日志打点 : %@", self.class);    [self swizzlingViewDidLoad];}@end

这个例子里面,有一个注意点,通常我们创建 ViewController 都是继承于 UIViewController,因此如果想要使用这个日志打点功能,在自定义 ViewController 里面需要调用 [super viewDidLoad]。所以一定需要明白,这个例子是替换 UIViewControllerviewDidLoad,而不是全部子类的 viewDidLoad

123456
@implementation ViewController- (void)viewDidLoad {    [super viewDidLoad];    // }@end

hook webView 的 delegate 方法

这个需求最初是项目中需要统计所有 webView 相关的数据,因此需要 hook webView 的 delegate 方法,今天也是以此为例,主要是 hook UIWebViewWKWebView类似)。

首先,我们需要明白,调用 delegate 的对象,是继承了 UIWebViewDelegate 协议的对象,因此如果要 hook delegate 方法,我们先要找到这个对象。

因此我们需要 hook [UIWebView setDelegate:delegate] 方法,拿到 delegate 对象,才能动态地替换该方法。这里 swizzling 上场:

12345678910111213141516
@implementation UIWebView(delegate)+(void)load{    // hook UIWebView    Method originalMethod = class_getInstanceMethod([UIWebView class], @selector(setDelegate:));    Method swizzledMethod = class_getInstanceMethod([UIWebView class], @selector(hook_setDelegate:));    method_exchangeImplementations(originalMethod, swizzledMethod);}- (void)dz_setDelegate:(id<UIWebViewDelegate>)delegate{    [self dz_setDelegate:delegate];        // 拿到 delegate 对象,在这里做替换 delegate 方法的操作    }@end

这里有个局限性,源码中需要调用 setDelegate: 方法,这样才会调用 dz_setDelegate:

接下来就是重点了,我们需要根据两种情况去动态地 hook delegate 方法,以 hook webViewDidFinishLoad: 为例:

  • delegate 对象实现了 webViewDidFinishLoad: 方法。则交换实现。
  • delegate 对象未实现了 webViewDidFinishLoad: 方法。则动态添加该 delegate 方法。

下面是 category 实现的完整代码,实现了以上两种情况下都能正确统计页面加载完成的数据:

12345678910111213141516171819202122232425262728293031323334< 大专栏  iOS 如何优雅地 hook 系统的 delegate 方法?div class="line">35
上一篇:iOS 自定义返回按钮,保留系统滑动返回


下一篇:event & EventHandler