chenzhao

  • java
  • iOS
  • IT
知识积累
不积跬步无以至千里
  1. 首页
  2. iOS
  3. 正文

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

2017年 7月 16日 87点热度 0人点赞 0条评论

From: http://www.daizi.me/2017/11/01/iOS%20%E5%A6%82%E4%BD%95%E4%BC%98%E9%9B%85%E5%9C%B0%20hook%20%E7%B3%BB%E7%BB%9F%E7%9A%84%20delegate%20%E6%96%B9%E6%B3%95%EF%BC%9F/

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

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

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

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

hook 系统类的实例方法

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


#import "UIViewController+swizzling.h"

#import <objc/runtime.h>

@implementation UIViewController (swizzling)

+ (void)load { 

Method fromMethod = class_getInstanceMethod([self class], @selector(viewDidLoad)); 

Method toMethod = class_getInstanceMethod([self class], @selector(swizzlingViewDidLoad)); 

method_exchangeImplementations(fromMethod, toMethod); 

} 

- (void)swizzlingViewDidLoad { 

NSString *str = [NSString stringWithFormat:@"%@", self.class]; 

NSLog(@"日志打点 : %@", self.class); 

[self swizzlingViewDidLoad]; 

} 

@end

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


@implementation ViewController 

- (void)viewDidLoad { 

[super viewDidLoad]; 

} 

@end

hook webView 的 delegate 方法

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

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

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


@implementation UIWebView(delegate)

+(void)load{ 

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]; 

} 

@end

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

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

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

static void dz_exchangeMethod(Class originalClass, SEL originalSel, Class replacedClass, SEL replacedSel, SEL orginReplaceSel){

Method originalMethod = class_getInstanceMethod(originalClass, originalSel);

Method replacedMethod = class_getInstanceMethod(replacedClass, replacedSel);

if (!originalMethod) {

Method orginReplaceMethod = class_getInstanceMethod(replacedClass, orginReplaceSel);

BOOL didAddOriginMethod = class_addMethod(originalClass, originalSel, method_getImplementation(orginReplaceMethod), method_getTypeEncoding(orginReplaceMethod));

if (didAddOriginMethod) {

NSLog(@"did Add Origin Replace Method");

}

return;

}

BOOL didAddMethod = class_addMethod(originalClass, replacedSel, method_getImplementation(replacedMethod), method_getTypeEncoding(replacedMethod));

if (didAddMethod) {

NSLog(@"class_addMethod_success --> (%@)", NSStringFromSelector(replacedSel));

Method newMethod = class_getInstanceMethod(originalClass, replacedSel);

method_exchangeImplementations(originalMethod, newMethod);

}else{

NSLog(@"Already hook class --> (%@)",NSStringFromClass(originalClass));

}

}

@implementation UIWebView(delegate)

+(void)load{

Method originalMethod = class_getInstanceMethod([UIWebView class], @selector(setDelegate:));

Method swizzledMethod = class_getInstanceMethod([UIWebView class], @selector(dz_setDelegate:));

method_exchangeImplementations(originalMethod, swizzledMethod);

}

  • (void)dz_setDelegate:(id)delegate{

[self dz_setDelegate:delegate];

[self exchangeUIWebViewDelegateMethod:delegate];

}

#pragma mark - hook webView delegate 方法

  • (void)exchangeUIWebViewDelegateMethod:(id)delegate{

dz_exchangeMethod([delegate class], @selector(webViewDidFinishLoad:), [self class], @selector(replace_webViewDidFinishLoad:),@selector(oriReplace_webViewDidFinishLoad:));

}

  • (void)oriReplace_webViewDidFinishLoad:(UIWebView *)webView{

NSLog(@"统计加载完成数据");

}

  • (void)replace_webViewDidFinishLoad:(UIWebView *)webView

{

NSLog(@"统计加载完成数据");

[self replace_webViewDidFinishLoad:webView];

}

@end

与 hook 实例方法不相同的地方是,交换的两个类以及方法都不是 [self class],在实现过程中:

  1. 判断 delegate 对象的 delegate 方法(originalMethod)是否为空,为空则用 class_addMethod 为 delegate 对象添加方法名为 (webViewDidFinishLoad:) ,方法实现为(oriReplace_webViewDidFinishLoad:)的动态方法。

  2. 若已实现,则说明该 delegate 对象实现了 webViewDidFinishLoad: 方法,此时不能简单地交换 originalMethod 与 replacedMethod,因为 replaceMethod 是属于 UIWebView 的实例方法,没有实现 delegate 协议,无法在 hook 之后调用原来的 delegate 方法:[self replace_webViewDidFinishLoad:webView];。

因此,我们也需要将 replace_webViewDidFinishLoad: 方法动态添加到 delegate 对象中,并使用添加后的方法和源方法交换。

结语

以上,通过动态添加方法并替换的方式,可以在不入侵源码的情况下,优雅地 hook 系统的 delegate 方法。通过合理使用 runtime 期间几个方法的特性,使得 hook 系统未实现的 delegate 方法成为可能。

最后献上:github 源码地址

标签: 暂无
最后更新:2022年 11月 11日

陈昭

IT 程序员

打赏 点赞
< 上一篇
下一篇 >

文章评论

取消回复

COPYRIGHT © 2022 chenzhao. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang