一種基於KVO的頁面載入,渲染耗時監控方法

dunne21發表於2021-09-09

在介紹本文之前,請先允許我提出一個問題,如果你要無痕監控任意一個頁面(UIViewController及其子類)的載入或者渲染時間,你會怎麼做。

很多人都會想到說用AOP啊,利用Method Swizzling來進行方法替換從而獲得方法呼叫耗時。
比如我們有一個ViewController,如果其實現了一個viewDidLoad方法進行睡眠5秒,如下所示:

@implementation ViewController- (void)viewDidLoad
{
    [super viewDidLoad];
    sleep(5);
}@end

相信很多人的第一直覺會是如下AOP程式碼(我們省略Method Swizzling相關的程式碼):

@implementation UIViewController (TestCase)+ (void)load
{    static dispatch_once_t onceToken;    dispatch_once(&onceToken, ^{
        wzq_swizzleMethod([UIViewController class], @selector(viewDidLoad), @selector(wzq_viewDidLoad));
    });
}

- (void)wzq_viewDidLoad
{    NSDate *date = [NSDate date];
    [self wzq_viewDidLoad];    NSTimeInterval duration = [[NSDate date] timeIntervalSinceDate:date];    NSLog(@"Page %@ cost %g in viewDidLoad", [self class], duration); 
}@end

但是,如果你自己嘗試了你會發現,你測算的時間壓根不是5秒。

為什麼呢?其原因在於我們Method Swizzling的時候,因為採用了對基類UIViewController進行替換,獲取到的viewDidLoad對應的IMP是屬於基類UIViewController的,而並不是ViewController自身覆寫的,所以我們監控的其實從子類ViewController呼叫[super viewDidLoad]的時候呼叫基類IMP的耗時。

好,看到這,有人就想了對應的方法,把-[ViewController viewDidLoad]的IMP替換掉就行了。方法很多種,比如建立一個ViewControllerCategory進行替換。但是這種方法你好像沒辦法任意對某個頁面進行替換。

有人說你可以runtime遍歷所有類判斷是不是UIViewController的子類,然後動態替換。理論是可行的,效率嘛,是比較低的。

方案

根據上述我們所知的缺陷,我們需要有一個兼顧動態性和效能的方案,能夠直接獲取到子類的IMP,這樣才能達到我們對於頁面載入渲染時間(viewDidLoadviewDidAppearviewWillAppear)監控的需求。

基於這個需求,我很快想到了基於KVO的方案(如果你對KVO不瞭解,我們知道,在對於任意物件進行KVO監控的時候,iOS底層實際上幫你動態建立了一個隱蔽的類,同時幫了做了大量的setter,getter函式的override,並呼叫原來類對應函式實現,從而讓你神不知鬼不覺的以為你還在用原來的類進行操作。

那我們該怎麼做呢?

  1. 對我們需要監聽的類的例項進行KVO,隨便監聽一個不存在的KeyPath。我們壓根不需要KVO的任何回撥,我們只是需要它能幫我們建立子類而已。

  2. 對KVO建立出來的子類新增我們需要Swizzle的方法對應的SEL及其IMP。因為本質上KVO只是對setter和getter方法進行了override如果我們不提供我們自己的實現,還是會呼叫到原來的類的IMP。

  3. 在例項銷燬的時候,將KVO監聽移除,不然會導致KVO still registering when deallocated這樣的Crash。

總體來說,我們需要做的就是三件事。

1. 對例項進行KVO

KVO方法只能在物件例項上進行操作,我們首先要獲取到的就是UIViewController及其子類的例項。

遍歷標頭檔案,發現UIViewController的初始化方法比較少,歸納為如下三種:

initinitWithCoder:initWithNibName:bundle:

我們先Swizzle這幾個方法:

 wzq_swizzleMethod([UIViewController class], @selector(initWithNibName:bundle:), @selector(wzq_initWithNibName:bundle:));wzq_swizzleMethod([UIViewController class], @selector(initWithCoder:), @selector(wzq_initWithCoder:));wzq_swizzleMethod([UIViewController class], @selector(init), @selector(wzq_init));

這幾個方法呼叫的時候,例項物件對應的記憶體已經分配出來了,無非就是建構函式還沒賦值,但是我們也能進行KVO了。KVO的程式碼如下所示:

NSString *identifier = [NSString stringWithFormat:@"wzq_%@", [[NSProcessInfo processInfo] globallyUniqueString]];
[vc addObserver:[NSObject new] forKeyPath:identifier options:NSKeyValueObservingOptionNew context:nil];

2. 新增我們想要的方法

我們剛剛已經對頁面例項進行了KVO操作,此時對於原先類別為ViewControllervc物件來說,內部其實已經變成NSKVONotifying_ViewController型別了。。如果我們想對其所在的型別新增方法的話,不能直接用[vc class],因為這個方法已經被內部override成了ViewController。我們需要使用object_getClass這個類進行真正的型別獲取,如下所示:

 // NSKVONotifying_ViewControllerClass kvoCls = object_getClass(vc);// ViewControllerClass originCls = class_getSuperclass(kvoCls);// 獲取原來實現的encodingconst 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:)));// 重點,新增方法。class_addMethod(kvoCls, @selector(viewDidLoad), (IMP)wzq_viewDidLoad, originViewDidLoadEncoding);class_addMethod(kvoCls, @selector(viewDidAppear:), (IMP)wzq_viewDidAppear, originViewDidAppearEncoding);class_addMethod(kvoCls, @selector(viewWillAppear:), (IMP)wzq_viewWillAppear, originViewWillAppearEncoding);

上述程式碼非常通俗易懂,不再贅述,替換完的方法如下,我們以wzq_viewDidLoad舉例:

static void wzq_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;    NSDate *date = [NSDate date];

    func(kvo_self, _sel);    NSTimeInterval duration = [[NSDate date] timeIntervalSinceDate:date];    NSLog(@"Class %@ cost %g in viewDidLoad", [kvo_self class], duration);
}

重點關注下上述程式碼中的注意點,之前我們在KVO生成的類中對應新增了原本沒有的實現,因此-[ViewController viewDidLoad]會走到我們的wzq_viewDidLoad方法中,但是我們怎麼才能呼叫到原來的viewDidLoad的呢?我們之前並沒有儲存對應的IMP呀。

這裡還是利用了KVO的特殊性:內部生成的NSKVONotifying_ViewController實際上是繼承自ViewController的

因此,Class origin_cls = class_getSuperclass(kvo_cls);實際上獲取到了ViewController類,我們從中取出對應的IMP,進行直接呼叫即可。

3. 移除KVO

我們利用Associate Object去移除就好了。一個物件釋放的時候會自動去清除其所在的assoicate object

基於這個原理,我們可以實現如下程式碼:

我們構建一個樁,把所有無用的KVO監聽都設定給這個樁,如下所示:

[vc addObserver:[WZQKVOObserverStub stub] forKeyPath:identifier options:NSKeyValueObservingOptionNew context:nil];

然後我們構建一個移除器,這個移除器弱引用儲存了vc的例項和對應的keypath,如下:

WZQKVORemover *remover = [WZQKVORemover new];remover.obj = vc;remover.keyPath = identifier.copy;

然後我們把這個移除器利用associate object設定給對應的vc。

objc_setAssociatedObject(vc, &wzq_associateRemoveKey, remover, OBJC_ASSOCIATION_RETAIN);

而在對應的移除器的dealloc方法裡,我們把kvo監聽給移除就可以了。

- (void)dealloc{#ifdef DEBUG
    NSLog(@"WZQKVORemover called");#endif
    if (_obj) {        [_obj removeObserver:[WZQKVOObserverStub stub] forKeyPath:_keyPath];
    }
}

額外

利用associate object移除KVO的正確性是有保障的,具體見runtime中associate object的原始碼:

void objc_removeAssociatedObjects(id object) 
{    if (object && object->hasAssociatedObjects()) {
        _object_remove_assocations(object);
    }
}void _object_remove_assocations(id object) {
    vector > elements;
    {        AssociationsManager manager;        AssociationsHashMap &associations(manager.associations());        if (associations.size() == 0) return;
        disguised_ptr_t disguised_object = DISGUISE(object);        AssociationsHashMap::iterator i = associations.find(disguised_object);        if (i != associations.end()) {
            // copy all of the associations that need to be removed.            ObjectAssociationMap *refs = i->second;            for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
                elements.push_back(j->second);
            }
            // remove the secondary table.
            delete refs;
            associations.erase(i);
        }
    }
    // the calls to releaseValue() happen outside of the lock.
    for_each(elements.begin(), elements.end(), ReleaseValue());
}



來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/1600/viewspace-2805398/,如需轉載,請註明出處,否則將追究法律責任。

相關文章