360移動端性能監控實踐QDAS-APM(iOS篇)
一、背景
app的性能問題是影響用戶體驗的重要因素之一。性能問題主要包含:崩潰、網絡請求錯誤或者超時、UI響應速度慢、主線程卡頓、CPU和內存使用高、耗電量大等等。大多問題的原因在于開發者錯誤地使用了線程、鎖、系統函數、編程規范問題、數據結構等等。解決這個問題的關鍵在于盡早發現和定位問題。
360作為一家注重用戶體驗的公司,app的性能問題無疑是被重點關注的,我們也總結出了一套自己的app性能監控體系。在平時開發和用戶反饋的問題中,我們對性能問題進行了歸納,總結出了5個分別是:資源文件如何掌控、 版本質量如何保證、線上問題如何排查、開發階段如何防止性能衰減、性能監控是否能真實反映用戶體驗。同時學習了業內相對完善的性能監控平臺上的功能原理。從而得出了360在iOS端移動端線上性能監控方案——QDAS-APM。
二、功能和原理
QDAS-APM已經實現以下功能監控:
- 頁面渲染時長
- 主線程卡頓
- 網絡錯誤
- FPS
- 大文件存儲
- CPU
- 內存使用
- Crash
- 啟動時長
下面按照功能詳細介紹實現細節和原理。另外用戶在使用app時會感知性能問題,我們可以將其轉化為具體的性能監控指標。
1. 頁面渲染時長
什么是頁面渲染時長?頁面渲染時長其實是從頁面初始化到用戶能看到頁面效果的時間長度。所要了解的指標有:
- 生命周期系統方法執行時長
- 頁面類名
- 啟動類型
- 執行耗時
- 插件名稱
關鍵度量的指標是執行耗時,不同的方法和步驟產生的耗時在用戶能接受的范圍內才被認為是合理。其他指標則是起有關聯性作用和定位問題。直接hook UIViewController的方法明顯是不可行的,原因是它只作用在UIViewController的方法,而app中大部分都采用繼承UIViewController的方式。
這里列出兩個可行性方案:
- 采用KVO,我們知道對于任意對象進行KVO操作時,系統都會幫你動態的創建一個復制類,同時實現了setter getter函數的覆蓋和函數實現。
- 采用runtime遍歷所有類為UIViewController的子類,再進行動態替換。
這兩種方式更加推薦***種,出于對兼容性、性能、以及能夠直接獲取UIViewController的子類的IMP。那具體如何實現呢?總結歸納為三步驟:
- 需要創建一個UIViewController的類別,對UIViewController的實例進行KVO,目的是讓KVO創建需要監控UIViewController的子類。
- 添加需要監控的方法,在KVO創建出來的子類添加需要Swizzle的方法對應的SEL及其IMP。目的是控制調用原來類的方法時機。
- 在UIViewController的實例銷毀時,在dealloc方法里將KVO監聽移除,不然會導致Crash。
舉個例子:我們以監控到qh_viewDidLoad方法舉例:
- static void qh_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));
- void(*func)(UIViewController *, SEL) = (void(*)(UIViewController *, SEL))origin_imp;
- CFAbsoluteTime startTime = CACurrentMediaTime();
- func(kvo_self, _sel);
- CFAbsoluteTime endTime = CACurrentMediaTime();
- NSTimeInterval duration = (endTime - startTime)*1000;
- NSLog(@"Class %@ cost %g in viewDidLoad", [kvo_self class], duration);
- }
會有一種特殊情況,如果KVO生成的類中對應的類原本沒有實現監控方法,那么會造成什么后果呢?KVO內部生成的NSKVONotifying_ViewController實際上時繼承自ViewController,因此直接取出對應的IMP調用。
OK,上面說的是對UIViewController類方法的執行時長統計。我們還想知道用戶真正頁面跳轉后看到***針頁面圖像的時長要如何采集呢?
那是不是將UIViewController類的init+loadView+viewDidLoad+viewWillAppear+viewDidAppear方法執行時長之和就是頁面渲染時長了呢?
答案是否定的,下面舉了三個反面例子:
如何才能判斷屏幕渲染完成?是否能間接獲取屏幕渲染時長?
對于異步回調和異步渲染這兩種方式,用上面提到的5個方法執行時長之和是不適用的。接下來看下如何相對準確地來統計和計算的方案。
頁面渲染的時長和頁面的布局時長會在未來的某個時間點上達到一致。要想得到頁面渲染的時長可以間接地參考頁面的布局完成時長。在UIViewController的生命周期方法里有一個方法叫viewDidLayoutSubviews,它是干什么的呢?它其實是告訴了控制器的subviews布局完成的時間點。一般情況下會被調用兩次,在不同的操作系統版本里調用次數也不同。
2. 主線程卡頓分析
主線程的卡頓直接影響用戶使用體驗,其表現在頁面的操作流暢性影響。首先引入一個概念FPS(Frames Per Second):每秒顯示連續圖片的幀數。每秒幀數越多,UI操作就越流暢。一般應用保持在每秒50~60幀數,會給用戶帶來流暢的感覺,反之,用戶則會感知到卡頓。那為什么會出現主線程卡頓呢?首先了解下,每一幀圖像顯示到屏幕的原理。
這是觸屏幕顯示的原理流程圖。CPU負責計算顯示內容,包括視圖的創建、布局計算、圖片解碼、文本繪制等,cpu會把計算后的結果提交給GPU,GPU進行變換、合成、渲染后,將渲染結果提交到幀緩沖區,當下一次垂直同步信號到來時,視頻控制器從緩沖區里獲取視圖顯示到屏幕上。明白了就屏幕顯示的原理,接下來看下為甚么會產生卡頓。
圖上提到 V-Sync 是什么,以及為什么要在 iPhone 的顯示流程引入它呢?在 iPhone 中使用的是雙緩沖機制,即上圖中的 FrameBuffer 有兩個緩沖區,雙緩沖區的引入是為了提升顯示效率,但是與此同時,他引入了一個新的問題,當視頻控制器還未讀取完成時,比如屏幕內容剛顯示一半時,GPU 將新的一幀內容提交到幀緩沖區并把兩個緩沖區進行交換后,視頻控制器就會把新的一幀數據的下半段顯示到屏幕上,造成畫面撕裂現象,V-Sync 就是為了解決畫面撕裂問題,開啟 V-Sync 后,GPU 會在顯示器發出 V-Sync 信號后,去進行新幀的渲染和緩沖區的更新。
搞清楚了 iPhone 的屏幕顯示原理后,下面來看看在 iPhone 上為什么會出現卡頓現象,上文已經提及在圖像真正在屏幕顯示之前,CPU 和 GPU 需要完成自身的任務,而如果他們完成的時間錯過了下一次 V-Sync 的到來(通常是1000/60=16.67ms),這樣就會出現顯示屏還是之前幀的內容,這就是界面卡頓的原因。不難發現,無論是 CPU 還是 GPU 引起錯過 V-Sync 信號,都會造成界面卡頓。
3. 網絡監控
網絡監控一般通過 NSURLProtocol 和代碼注入(Hook)這兩種方式來實現,由于 NSURLProtocol 作為上層接口,使用起來更為方便,NSURLProtocol 屬于 URL Loading System 體系中,應用層的協議支持有限,只支持 FTP,HTTP,HTTPS 等幾個應用層協議,對于使用其他協議的流量則束手無策,所以存在一定的局限性。監控底層網絡庫 CFNetwork 則沒有這個限制。如果本地有https的證書驗證也不適用于NSURLProtocol這種方式。容易引起業務數據丟失問題。
(1) NSURLProtocol
上圖是基于NSURLProtocol協議來實現的,通過繼承自NSURLProtocol,并注冊。通過代理和自身方法來得到網絡請求相關的指標。
(2) HOOK方式—NSProxy
NSProxy is an abstract superclass defining an API for objects
that act as stand-ins for other objects or for objects that don’t exist yet. Typically, a message to a proxy is forwarded to the real object or causes the proxy to load (or transform itself into) the real object. Subclasses of NSProxy can be used to implement transparent distributed messaging (for example, NSDistantObject) or for lazy instantiation of objects that are expensive to create. |
上面的這段英文是 Apple 官方文檔給 NSProxy 的定義,NSProxy 和 NSObject 一樣都是根類,它是一個抽象類,可以通過繼承它,并重寫 -forwardInvocation: 和 -methodSignatureForSelector: 方法以實現消息轉發到另一個實例。綜上,NSProxy 的目的就是負責將消息轉發到真正的 target 的代理類。
那為什么我們不用Method swizzling 替換方法需要指定類名?是因為 NSURLConnectionDelegate 和 NSURLSessionDelegate 是由業務方指定,通常來說是不確定,所以這種場景不適合使用 Method swizzling。使用 NSProxy 可以解決,具體實現:proxy delegate 替換 NSURLConnection 和 NSURLSession 原來的 delegate,當 proxy delegate 收到回調時,如果是要 hook 的方法,則調用 proxy 的實現,proxy 的實現***會調用原來的 delegate;反之,則通過消息轉發機制將消息轉發給原來的 delegate。
下圖示意了整個操作流程:
通過對NSURLConnection、NSURLSession和CFNetwork這三個類中關鍵方法的hook來獲取上報指標。具體hook哪些方法,請看下圖:
將hook方法中得到的相關指標整理成需要的格式上報到服務端,服務端通過數據處理和拆分指標,匯總加計算生成最終的報表。
三、QDAS-APM在集成和使用上的便捷
由于sdk功能基本上采用的都是主動采集功能,無需二次開發,也無需額外引入系統庫。所以在集成和使用上非常便捷。在sdk的集成上,只需要三步驟:
- 引入sdk庫
- 引入sdk頭文件
- 在app的didFinishLauchingWithOptions里初始化sdk,并傳入appkey即可。
【本文是51CTO專欄機構360技術的原創文章,微信公眾號“360技術( id: qihoo_tech)”】