VCProfiler 主要是利用 KVO 自动生成子类的特性来检测 ViewController 的加载时长,只需要一个分类,对原项目没有任何侵入性,而且检测效果准确。
痛点
ViewController 的时长检测可以更好的帮助我们发现页面加载过程中耗时的操作,也可以体现 app 的流畅度。
方案
一般项目中对 ViewController 的时长检测是使用 Method Swizzle 替换了 VC 的 loadView
,ViewDidLoad
,ViewWillAppear
还有 ViewDidAppear
来对页面的加载时间进行检测,但是这种方式不太理想,原因在于我们替换的是 ViewController 的方法,而通常我们自己定义的 ViewController 是调用了父类的方法之后调用很多操作,而这些方法时长我们无法检测到。
VCProfiler 用了一种很巧妙的方式,利用 KVO 会创建一个子类的原理,对子类的一些关键方法进行 hook,这样就会在原类执行完成之后才会调用时间的记录方法,能够准确的测量 VC 的加载耗时。
实现
实现只有两个文件,但是里面的内容却很丰富,主要有两个类,一个分类,我逐个进行分析。
MTHFakeKVOObserver
这个类很简单,就是一个单例,作为观察者,监听 ViewController 一个没有意义的属性,目的是为了触发 KVO。
MTHFakeKVORemover
@interface MTHFakeKVORemover : NSObject
@property (nonatomic, unsafe_unretained) id target;
@property (nonatomic, copy) NSString *keyPath;
@end
@implementation MTHFakeKVORemover
- (void)dealloc {
VCLog(@"dealloc: %@", _target);
[_target removeObserver:[MTHFakeKVOObserver shared] forKeyPath:_keyPath];
_target = nil;
}
@end
这个类的作用主要是为了移除观察者,有两个属性,target
弱引用保存了当前的 ViewController
,keyPath
保存对应的 keypath
。
具体的使用以及为何使用 unsafe_unretained
而不是 weak
我们在下面一起分析。
UIViewController+VCDetector
主要的内容就是在这个分类中了,首先我们可以看这一部分代码:
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [UIViewController class];
[self swizzleMethodInClass:class originalMethod:@selector(initWithNibName:bundle:) swizzledSelector:@selector(pmy_initWithNibName:bundle:)];
[self swizzleMethodInClass:class originalMethod:@selector(initWithCoder:) swizzledSelector:@selector(pmy_initWithCoder:)];
});
}
- (instancetype)pmy_initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil {
[self createAndHookKVOClass];
[self pmy_initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
return self;
}
- (nullable instancetype)pmy_initWithCoder:(NSCoder *)aDecoder {
[self createAndHookKVOClass];
[self pmy_initWithCoder:aDecoder];
return self;
}
很明显,这一段的目的是在程序启动时,交换 UIViewController
的 initWithNibName:bundle:
和自己实现的 pmy_initWithNibName:bundle:
以及 initWithCoder:
和pmy_initWithCoder:
,这里为什么不交换 init
方法呢?因为 init
方法最终也是调用 initWithNibName:bundle:
来实现,所以不需要再多做一次操作了。
总的来说,这一部分就是为了让 viewController 在初始化的时候调用 createAndHookKVOClass
这个方法。
- (void)createAndHookKVOClass {
//监听一个没有意义的属性,目的只是为了触发KVO实现一个NSKVONotifying_ViewController子类并且将 isa 指针指向子类
[self addObserver:[MTHFakeKVOObserver shared] forKeyPath:kUniqueFakeKeyPath options:NSKeyValueObservingOptionNew context:nil];
// 给ViewController添加一个MTHFakeKVORemover对象,利用关联对象的原理,ViewController调用delloc的时候,会释放所有的关联对象。
//MTHFakeKVORemover弱引用ViewController
MTHFakeKVORemover *remover = [[MTHFakeKVORemover alloc] init];
remover.target = self;
remover.keyPath = kUniqueFakeKeyPath;
objc_setAssociatedObject(self, &kAssociatedRemoverKey, remover, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// NSKVONotifying_ViewController
Class kvoCls = object_getClass(self);
//对比当前的viewDidLoad的指针和当前方法的指针,确保在这之前没有viewDidLoad被hook
IMP currentViewDidLoadImp = class_getMethodImplementation(kvoCls, @selector(viewDidLoad));
if (currentViewDidLoadImp == (IMP)pmy_viewDidLoad) {
return;
}
// ViewController
Class originCls = class_getSuperclass(kvoCls);
VCLog(@"Hook %@", kvoCls);
// 获取原来实现的encoding
const char *originLoadViewEncoding = method_getTypeEncoding(class_getInstanceMethod(originCls, @selector(loadView)));
const char *originViewDidLoadEncoding = method_getTypeEncoding(class_getInstanceMethod(originCls, @selector(viewDidLoad)));
const char *originViewDidAppearEncoding = method_getTypeEncoding(class_getInstanceMethod(originCls, @selector(viewDidAppear:)));
const char *originViewWillAppearEncoding = method_getTypeEncoding(class_getInstanceMethod(originCls, @selector(viewWillAppear:)));
// 重点,为 NSKVONotifying_ViewController 添加常用的检测方法。
// 当调用 VC 的下列方法的时候,会直接调用 NSKVONotifying_ViewController 添加的方法
class_addMethod(kvoCls, @selector(loadView), (IMP)pmy_loadView, originLoadViewEncoding);
class_addMethod(kvoCls, @selector(viewDidLoad), (IMP)pmy_viewDidLoad, originViewDidLoadEncoding);
class_addMethod(kvoCls, @selector(viewDidAppear:), (IMP)pmy_viewDidAppear, originViewDidAppearEncoding);
class_addMethod(kvoCls, @selector(viewWillAppear:), (IMP)pmy_viewWillAppear, originViewWillAppearEncoding);
}
以上这一段就是利用 KVO 以及 hook 子类的关键方法实现耗时检测,其中每一部分的意思我都写上了注释,配合代码很容易理解。
为什么不使用 weak
而使用 unsafe_unretained
,我们知道,对象在调用 dealloc 内部会调用 _object_remove_assocations(obj)
方法,也就是释放所有的关联对象,那么在 ViewController 销毁的时候,会调用 MTHFakeKVORemover 的 dealloc 方法,在 MTHFakeKVORemover 调用 dealloc 的时候,会调用 objc_clear_deallocating(obj)
清除弱引用表,那么在 remover 进入 -dealloc
方法的时候,上述代码中的 if (_obj)
判断将永远为 false。因此使用 unsafe_unretained
来修饰 VC 实例。
static void pmy_viewDidLoad(UIViewController *kvo_self, SEL _sel) {
Class kvo_cls = object_getClass(kvo_self);
Class origin_cls = class_getSuperclass(kvo_cls);
IMP origin_imp = method_getImplementation(class_getInstanceMethod(origin_cls, _sel));
assert(origin_imp != NULL);
void (*func)(UIViewController *, SEL) = (void (*)(UIViewController *, SEL))origin_imp;
VCLog(@"VC: %p -viewDidLoad \t\tbegin at CF time:\t%lf", kvo_self, CFAbsoluteTimeGetCurrent());
func(kvo_self, _sel);
VCLog(@"VC: %p -viewDidLoad \t\tfinish at CF time:\t%lf", kvo_self, CFAbsoluteTimeGetCurrent());
}
然后就是实现时间统计的操作了,首先我们拿到父类也就是 ViewController 的 viewDidLoad
方法的 IMP指针,在调用第一次时间统计之后,传入 NSKVONotifying_ViewController 和方法名,类似于是调用了 [super viewDidLoad]
,然后在之后再统计一次时间,整个时间统计的过程就完成了。
- (void)dealloc {
VCLog(@"dealloc: %@", _target);
[_target removeObserver:[MTHFakeKVOObserver shared] forKeyPath:_keyPath];
_target = nil;
}
最后就是移除监听者,至此,整个流程也分析完了,最后再来梳理一遍整个流程。
1、load 方法中交换 ViewController 的 init 的方法实现。
2、TestVC 调用 init 方法的时候,会调用 addObserver
监听一个不会使用的对象,触发 KVO 生成一个子类 NSKVONotifying_ TestVC 并将 isa 指针指向 NSKVONotifying_ TestVC。
3、TestVC 关联对象 MTHFakeKVORemover用于 dealloc 的时候移除监听,MTHFakeKVORemover 对 TestVC 弱引用,修饰符使用 unsafe_unretained
。
4、NSKVONotifying_ TestVC 重写一些常用来检测的方法。
5、TestVC 调用 viewDidLoad
或者其他检测的方法,优先调用 NSKVONotifying_ TestVC 中的方法,开始统计。
6、然后 viewDidLoad
内方法执行完成之后,结束统计。
7、TestVC 调用 dealloc
方法,释放关联对象,MTHFakeKVORemover 调用 dealloc
方法,移除监听,完成。
总结
VCProfiler 代码不多,只有 200 来行,但是留给我的思考很多,而且内部涉及到了很多的底层原理,包括 Method Swizzle,关联对象的原理,dealloc 的原理,KVO 的原理,以及runtime 的运用,以及 weak 和 unsafe_unretained 的区别等。只有对各种机制了解透彻,并且运用熟练才能想出这种技巧来实现我们所需要的功能,正因如此,我们才需要对我们的程序运行过程深入的理解,只有清楚程序核心原理,才能慢慢熟悉,最终才能做到随心所欲,收放自如。
参考文档: