前言

Method Swizzling 可以被称为方法替换, 可以干出一些神奇的事情, 例如 Monkey 之前分享的 JSPatch 相关.
这里分享的是用于页面加载时间的统计.

起因, 早先看到一些 SDK 提供无需新增代码, 只要加入 SDK 就可以监控到 App 的页面加载时间, 网络请求失败率等情况. 这种黑科技一下就勾起我的好奇心, 开始了研究相关信息. 这些 SDK 都是商业 SDK, 无法看到开源代码, 只能自己琢磨. 猜测是某种 hook 技术, 然后用这个一点点搜索, 查到了 swizzle. 虽然无法确定这个就是他们的实际解决方案, 但这个应该是最接近的方案了.

iOS View 加载生命周期

init-初始化程序
loadView-只初始化 view
viewDidLoad-加载视图
viewWillAppear-UIViewController 对象的视图即将加入窗口时调用;
viewDidApper-UIViewController 对象的视图已经加入到窗口时调用;
viewWillDisappear-UIViewController 对象的视图即将消失、被覆盖或是隐藏时调用;
viewDidDisappear-UIViewController 对象的视图已经消失、被覆盖或是隐藏时调用;
viewVillUnload-当内存过低时,需要释放一些不需要使用的视图时,即将释放时调用;
viewDidUnload-当内存过低,释放一些不需要的视图时调用。

我们统计页面加载时间, 可以认为就是记录这几个函数的加载时间点,然后求差值. method swizzling 可以对 UIViewController 类进行扩展.加入我们的统计打点. 这样加入一次,所有的 UIViewController 子类也都可以统计加载时间了.

代码实现

#import "UIViewController+Logging.h"
#import <objc/runtime.h>

static inline void swizzle_methods(Class class, SEL originalSelector, SEL swizzledSelector) {

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        BOOL didAddMethod =
        class_addMethod(class,
                        originalSelector,
                        method_getImplementation(swizzledMethod),
                        method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
}


@implementation UIViewController (Logging)

+(void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL originalSelector = @selector(loadView);
        SEL swizzledSelector = @selector(log_loadView);
        swizzle_methods([self class], originalSelector, swizzledSelector);

        SEL originalSelectorFinished = @selector(viewDidAppear:);
        SEL swizzledSelectorFinished = @selector(log_viewDidAppear:);
        swizzle_methods([self class], originalSelectorFinished, swizzledSelectorFinished);
    });
}

#pragma mark - Method Swizzling

-(void)log_loadView{
    [self log_loadView];
    NSLog(@"test loadView: %@", [self class]);
}

-(void)log_viewDidAppear:(BOOL)animated{
    [self log_viewDidAppear:animated];
    NSLog(@"test viewDidAppear: %@", [self class]);
}
@end

每次 UIViewController 执行 loadView 方法时, 实际执行的是 log_loadView. (执行 log_loadView 时, 执行的是 loadView.) 就这样,每个 UIViewController 子类都会自带统计打点功能.

后记

  1. 这个功能如果扩展一些, 把 NSLog 换成向后台发请求, 带上当前时间, uid 之类的可以实现统计用户页面停留时长, 显示次数等.
  2. 如果对原理感兴趣, 请自行搜索 iOS category 和 method swizzling
  3. 我们 swizzle 的只是父类的方法, 如果子类重写该方法的话,对子类的该方法不生效, 除非子类通过 super 调用父类方法
  4. 目前看没有找到 swizzle uiwebview 中 webViewDidStartLoad 好的方法, 因为 webViewDidStartLoad 在协议中定义, 在Method originalMethod = class_getInstanceMethod(class, originalSelector), originalMethod 会指向 0x0, 所以这方面如果有啥方法可以指导一下

推荐文章:

ios 逆向工程 - 内部钩子 (Method Swizzling) http://my.oschina.net/iq19900204/blog/411372
iOS ViewController 生命周期 http://www.cnblogs.com/chenyg32/p/3873679.html
Objective-C Runtime 运行时之四:Method Swizzling http://southpeak.github.io/blog/2014/11/06/objective-c-runtime-yun-xing-shi-zhi-si-:method-swizzling/


↙↙↙阅读原文可查看相关链接,并与作者交流