在 iOS 开发中我们经常需要去 hook 系统方法,来满足一些特定的应用场景。
比如使用 Swizzling 来实现一些 AOP 的日志功能,比较常见的例子是 hook UIViewController
的 viewDidLoad
,动态为其插入日志。
这当然是一个很经典的例子,能让开发者迅速了解这个知识点。不过正如现在的娱乐圈,diss 天 diss 地,如果我们也想 hook 天,hook 地,顺便 hook 一下系统的 delegate 方法,该怎么做呢?
所以就进入今天的主题:如何优雅地 hook 系统的 delegate 方法?
hook 系统类的实例方法
首先,我们回想一下 hook UIViewController
的 viewDidLoad
方法,我们需要使用 category,为什么需要 category 呢?因为在 category 里面才能在不入侵源码的情况下,拿到实例方法 viewDidLoad
,并实现替换:
|
|
这个例子里面,有一个注意点,通常我们创建 ViewController
都是继承于 UIViewController
,因此如果想要使用这个日志打点功能,在自定义 ViewController
里面需要调用 [super viewDidLoad]
。所以一定需要明白,这个例子是替换 UIViewController
的 viewDidLoad
,而不是全部子类的 viewDidLoad
。
|
|
hook webView 的 delegate 方法
这个需求最初是项目中需要统计所有 webView
相关的数据,因此需要 hook webView 的 delegate
方法,今天也是以此为例,主要是 hook UIWebView
(WKWebView
类似)。
首先,我们需要明白,调用 delegate 的对象,是继承了 UIWebViewDelegate 协议的对象,因此如果要 hook delegate 方法,我们先要找到这个对象。
因此我们需要 hook [UIWebView setDelegate:
|
|
这里有个局限性,源码中需要调用 setDelegate:
方法,这样才会调用 dz_setDelegate:
。
接下来就是重点了,我们需要根据两种情况去动态地 hook delegate 方法,以 hook webViewDidFinishLoad:
为例:
- delegate 对象实现了
webViewDidFinishLoad:
方法。则交换实现。 - delegate 对象未实现了
webViewDidFinishLoad:
方法。则动态添加该 delegate 方法。
下面是 category 实现的完整代码,实现了以上两种情况下都能正确统计页面加载完成的数据:
|
|
与 hook 实例方法不相同的地方是,交换的两个类以及方法都不是 [self class]
,在实现过程中:
判断 delegate 对象的 delegate 方法(
originalMethod
)是否为空,为空则用class_addMethod
为 delegate 对象添加方法名为 (webViewDidFinishLoad:
) ,方法实现为(oriReplace_webViewDidFinishLoad:
)的动态方法。若已实现,则说明该 delegate 对象实现了
webViewDidFinishLoad:
方法,此时不能简单地交换originalMethod
与replacedMethod
,因为replaceMethod
是属于UIWebView
的实例方法,没有实现 delegate 协议,无法在 hook 之后调用原来的 delegate 方法:[self replace_webViewDidFinishLoad:webView];
。因此,我们也需要将
replace_webViewDidFinishLoad:
方法动态添加到 delegate 对象中,并使用添加后的方法和源方法交换。
结语
以上,通过动态添加方法并替换的方式,可以在不入侵源码的情况下,优雅地 hook 系统的 delegate 方法。通过合理使用 runtime 期间几个方法的特性,使得 hook 系统未实现的 delegate 方法成为可能。
最后献上:github 源码地址