一種基於KVO的頁面載入,渲染耗時監控方法
在介紹本文之前,請先允許我提出一個問題,如果你要無痕監控任意一個頁面(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替換掉就行了。方法很多種,比如建立一個ViewController
的Category進行替換。但是這種方法你好像沒辦法任意對某個頁面進行替換。
有人說你可以runtime遍歷所有類判斷是不是
UIViewController
的子類,然後動態替換。理論是可行的,效率嘛,是比較低的。
方案
根據上述我們所知的缺陷,我們需要有一個兼顧動態性和效能的方案,能夠直接獲取到子類的IMP,這樣才能達到我們對於頁面載入渲染時間(viewDidLoad
, viewDidAppear
和viewWillAppear
)監控的需求。
基於這個需求,我很快想到了基於KVO的方案(如果你對KVO不瞭解,我們知道,在對於任意物件進行KVO監控的時候,iOS底層實際上幫你動態建立了一個隱蔽的類,同時幫了做了大量的setter,getter
函式的override,並呼叫原來類對應函式實現,從而讓你神不知鬼不覺的以為你還在用原來的類進行操作。
那我們該怎麼做呢?
對我們需要監聽的類的例項進行KVO,隨便監聽一個不存在的KeyPath。我們壓根不需要KVO的任何回撥,我們只是需要它能幫我們建立子類而已。
對KVO建立出來的子類新增我們需要Swizzle的方法對應的SEL及其IMP。因為本質上KVO只是對setter和getter方法進行了override,如果我們不提供我們自己的實現,還是會呼叫到原來的類的IMP。
在例項銷燬的時候,將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操作,此時對於原先類別為ViewController
的vc
物件來說,內部其實已經變成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/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 關於頁面載入耗時測試
- 一種新的頁面載入時間檢測方式
- 基於 ELKB 構建 Kylin 查詢時間監控頁面
- 10種優化頁面載入速度的方法優化
- 基於Spring AOP實現對外介面的耗時監控Spring
- js 進入頁面載入的方法JS
- 基於Vue的SPA如何優化頁面載入速度Vue優化
- 一種實現 MediaWiki 分頁面載入 JS 的思路JS
- C# 開發技巧 輕鬆監控方法執行耗時C#
- 分享一個登入頁面基於Tailwind CSSAICSS
- zabbix應用教程:基於Nginx頁面響應的日誌監控用例Nginx
- Hystrix 監控視覺化頁面——Dashboard 流監控視覺化
- Python Selenium等待(waits)頁面載入完成的三種方法PythonAI
- 一種對雲主機進行效能監控的監控系統及其監控方法
- CSS 非同步載入技術 不影響頁面渲染CSS非同步
- 如何監控前端頁面FPS前端
- 計算頁面執行時間的兩種方法
- Flutter 耗時監控 | 路由名為空原因分析Flutter路由
- 基於Serverless雲函式站點監控的方法Server函式
- 如何完美地處理JavaScript渲染頁面中的非同步載入?JavaScript非同步
- Javascript在頁面載入時的執行順序JavaScript
- 基於OkHttp的Http監控HTTP
- Unix檔案系統頁面監控實現-效果頁面
- Vue 頁面狀態保持頁面間資料傳輸的一種方法Vue
- 下一代實時渲染——基於深度學習的渲染深度學習
- 監控頁面後退前進,瀏覽器文件載入事件之pageshow、pagehide瀏覽器事件IDE
- 基於各種感測器的空調系統監控
- KVO監聽容器類(陣列,字典等)屬性的兩種方法陣列
- https頁面載入http資源的解決方法HTTP
- 基於 IntersectionObserver 實現一個元件的曝光監控Server元件
- 頁面渲染:效能分析
- 頁面渲染機制
- oracle備庫延時的監控方法Oracle
- LoadRunner監控Linux的三種方法Linux
- 基於Prometheus的資料庫監控Prometheus資料庫
- Unix檔案系統頁面監控實現
- 頁面載入全過程
- 頁面載入和解析流程