在回答页面浏览事件之前,我们先介绍 UIViewController 生命周期相关的内容,然后再介绍 iOS 的“黑魔法” Method Swizzling。

1、 UIViewController 生命周期

众所周知,每一个 UIViewController 都管理着一个由多个视图组成的树形结构,其中根视图保存在 UIViewController 的 view 属性中。UIViewController 会懒加载它所管理的视图集,直到第一次访问 view 属性时,才会去加载或者创建 UIViewController 的视图集。

有以下几种常用的方式加载或者创建 UIViewController 的视图集:

  • 使用 Storyboard
  • 使用 Nib 文件
  • 使用代码,即重写 – loadView

以上这些方法,最终都会创建出合适的根视图并保存在 UIViewController 的 view 属性中,这是 UIViewController 生命周期的第一步。当 UIViewController 的根视图需要展示在页面上时,会调用 – viewDidLoad 方法。在这个方法中,我们可以做一些对象初始化相关的工作。

需要注意的是:此时,视图的 bounds 还没有确定。对于使用代码创建视图,- viewDidLoad 方法会在 -loadView 方法调用结束之后运行;如果使用的是 Stroyboard 或者 Nib 文件创建视图,- viewDidLoad 方法则会在 – awakeFromNib 方法之后调用。

当 UIViewController 的视图在屏幕上的显示状态发生变化时,UIViewController 会自动回调一些方法,确保子类能够响应到这些变化。如下图所示,它展示了 UIViewController 在不同的显示状态时会回调不同的方法。

在 UIViewController 被销毁之前,还会回调 – dealloc 方法,我们一般通过重写这个方法来主动释放不能被 ARC 自动释放的资源。

我们现在对 UIViewController 的整个生命周期有了一些基本了解。那么,我们如何去实现页面浏览事件( $AppViewScreen 事件)的全埋点呢?

通过 UIViewController 的生命周期可知,当执行到 – viewDidAppear: 方法时,表示视图已经在屏幕上渲染完成,也即页面已经显示出来了,正等待用户进行下一步操作。因此,- viewDidAppear: 方法就是我们触发页面浏览事件的最佳时机。如果想要实现页面浏览事件的全埋点,需要使用  iOS 的“黑魔法” Method Swizzling 相关的技术。

下面,我们先介绍 Method Swizzling。

2、 Method Swizzling

Method Swizzling,顾名思义,就是交换两个方法的实现。简单的来说,就是利用 Objective-C runtime 的动态绑定特性,把一个方法的实现与另一个方法的实现进行交换。

在 Objective-C 的 runtime 中,一个类是用一个名为 objc_class 的结构体表示的,它的定义如下:

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif} OBJC2_UNAVAILABLE;

在上面的结构体中,虽然有很多字段在 OBJC2 中已经废弃了(OBJC2_UNAVAILABLE),但是了解这个结构体还是有助于我们理解 Method Swizzling 的底层原理。我们从上述结构体中可以发现,有一个 objc_method_list 指针,它保存着当前类的所有方法列表。同时,objc_method_list 也是一个结构体,它的定义如下:

struct objc_method_list {
    struct objc_method_list * _Nullable obsolete             OBJC2_UNAVAILABLE;    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}

在上面的结构体中,有一个 objc_method 字段,我们再来看看 objc_method 这个结构体:

struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}

从上面的结构体中可以看出,一个方法由下面三个部分组成:

  • method_name:方法名
  • method_types:方法类型
  • method_imp:方法实现

使用 Method Swizzling 交换方法,其实就是修改了 objc_method 结构体中的 method_imp,也即改变了 method_name 和 method_imp 的映射关系。

那我们如何改变 method_name 和 method_imp 的映射关系呢?在 Objective-C 的 runtime 中,提供了很多非常方便使用的函数,让我们可以很简单的就能实现 Method Swizzling,即改变 method_name 和 method_imp 的映射关系,从而达到交换方法的效果。

3、 实现页面浏览事件全埋点

通过对 UIViewController 生命周期和 Method Swizzling 的学习,我们就可以通过 Method Swizzling 来交换 UIViewController 的 – viewDidAppear: 方法,然后在交换的方法中触发 $AppViewScreen 事件,从而就可以实现页面浏览事件的全埋点了。

4、 遗留问题

按照目前的方案实现 $AppViewScreen 事件的全埋点,会有如下两个问题:

  • 应用程序热启动时(从后台恢复),第一个页面没有触发 $AppViewScreen 事件。这是由于这个页面没有再次执行 – viewDidAppear: 方法导致的。
  • 要求 UIViewController 的子类要么不重写 – viewDidAppear: 方法,一旦重写必须调用 [super viewDidAppear:animated],否则也不会触发 $AppViewScreen 事件。这是由于我们是直接交换 UIViewController 的 – viewDidAppear: 方法导致的。

 

每日一问的答案中可能无法全完解读这个问题,如果您是相关技术专家或者是对本问题有自己的见解,欢迎带着「批判性」的态度阅读,指正其中的不足。