Flutter性能監控實踐

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"前言","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 移動端APP作爲與用戶交互的工具,用戶體驗是衡量應用優秀與否的重要指標,其中性能尤爲重要。在Google推出Flutter跨平臺方案後,貝殼也將Flutter接入到多個APP中. 在此之前貝殼已經在原生監控中實現了頁面加載、幀率與卡頓等監控。隨着Flutter在貝殼各種業務場景的使用, 隨之而來的問題就是Flutter性能怎麼樣,用戶體驗如何。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 首先可以明確的是在不影響APP性能的情況下,更多的性能數據能夠輔助我們改進缺陷,優化以提升APP的使用體驗。因此在基於數據採集與性能損耗的可行性初步調研後,我們將Flutter監控功能聚焦在頁面加載、幀率、卡頓這三個點上。接下來將從技術調研與論證、架構設計、實現、以及實踐效果分別介紹。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"技術調研與論證","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"概念概述","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 通常我們說Flutter中一切皆Widget,描述的是頁面模塊都是用Widget來表示。Widget是頁面的一個不可變的描述,也是Flutter框架的核心要素。其中Navigator(使用堆棧規則來管理一組‘頁面’的Widget)通過移動小部件從一個Widget頁面可視化地切換到另一個Widget頁面。當我們打開一個Widget頁面,實際是將Route頁面添加到Navigator堆棧中,然後由Navigator調度顯示棧頂的Route頁面。Route頁面是Widget頁面的抽象描述(包含基礎數據,透明,動畫屬性等),起到連接Widget與Navigator作用,也避免相互的耦合。下圖爲Flutter頁面顯示的時序圖:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/73/739f83920f3310d587617062b02fe8fb.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"頁面唯一性","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 對於頁面數據採集,首先要解決的是如何將多類性能數據關聯到某一個具體的頁面。對於原生來說, 通過頁面名稱就能做到:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"   iOS(ViewController):在一個可執行文件中,ViewController名稱是唯一的;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"   Android(Activity、Fragment):同一個包名下Activity和Fragment類名是唯一的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是在Flutter中沒有直接獲取頁面唯一標識方式(如","attrs":{}},{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"strong","attrs":{}}],"text":"getPackageName","attrs":{}},{"type":"text","text":"),並且Widget類名是可以重複的,也就無法精確定位頁面唯一類。那麼我們該如何給一個頁面定義唯一標識?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"經過調研我們找到兩種方式如下表:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ff/ff8ab1cbad47d2a797a72e778ae755ea.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 結合iOS瘦身的效果與編譯時插入包體對比,以及未來對Dart代碼瘦身混淆的考量,我們選擇了後者。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"頁面定義:與Route關聯的頁面頂級Widget作爲我們的統計單元。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"頁面唯一標識:使用頁面所在的文件","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"importUri(","attrs":{}},{"type":"text","text":"Dart文件的唯一標識","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":") + ClassName","attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"頁面生命週期","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 對Flutter Widget來說, 本質上沒有生命週期這一概念,因爲Widget樹只是不斷重建的過程。Flutter Framework 提供兩個大類Widget:","attrs":{}},{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"stateful","attrs":{}},{"type":"text","text":" 和 ","attrs":{}},{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"stateless","attrs":{}},{"type":"text","text":" widgets,其創建及調用時機如下圖:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/88/88d017817f03dd57e36ec3950065c0ea.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/3f/3f0ffdf855b7441f21f34cf67686d6d6.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 我們發現,對比StatelessWidget 和 StatefulWidget可以看出兩者存在的差異。StatelessWidget沒有可變狀態,沒有合適的監控點。而如果在StatefulWidget的build、createState、didUpdateWidget或didChangeDependencies等中打點, 會因widget樹狀態重建頻繁導致打點過多,對頁面本身性能產生影響。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 那麼頁面首幀與頁面繪製完成應該在什麼時機獲取呢?結合頁面加載過程(","attrs":{}},{"type":"link","attrs":{"href":"https://flutter.dev/docs/resources/architectural-overview#building-widgets","title":"","type":null},"content":[{"type":"text","text":"architectural-overview - building-widgets","attrs":{}}]},{"type":"text","text":")分析,我們最終通過Navigator對Route的管理作爲頁面生命週期監控hook點。對於頁面首幀我們採用在Route buildPage後增加一個PostFrameCallback。而對於頁面加載完成,我們給TransitionRoute 的AnimationController設置statusListener, 並監聽AnimationStatus.completed狀態來確定動畫結束時機,以此確定頁面加載完成的時間點。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"幀率與卡頓","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"幀率:通常指每秒繪製的幀數(frames per second)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"丟幀:因系統負載導致幀率過低所造成的畫面出現停滯現象,也叫跳幀或者掉幀。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"卡頓:一般來說指丟幀的另一個概念,指畫面出現停滯現象比丟幀更明顯。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" Flutter官方有提供一套基於 ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"SchedulerBinding.addTimingsCallback","attrs":{}},{"type":"text","text":" 回調實現的幀率方案。從源碼中可以看出,當flutter頁面有視圖繪製刷新時, 系統吐出一串FrameTiming數據 (與Android dumpsys gfxinfo中的frameStats類似)。並且其對性能的影響也可以忽略不計(官方數據iPhone6s:對60fps的設備每幀增加 0.1ms 的負載,每秒CPU佔用0.01%)","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"Flutter spends less than 0.1ms every 1 second to report the timings (measured on iPhone6S). The 0.1ms is about 0.6% of 16ms (frame budget for 60fps), or 0.01% CPU usage per second.","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 我們可以直接使用這種方式獲取監控所要採集的FPS數據源。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"既然有了幀率數據源,我們如何用數據衡量頁面性能?如何爲業務開發同學提供一個客觀的指標來評判性能,以及如何驗證優化效果?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 通常來說FPS是衡量頁面流暢度的指標,如何計算FPS得出大家都認可的參考標準呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在經過調研與實踐後,我們爲卡頓閾值找到一個具有說服力的衡量指標。列舉如下:","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"卡頓閾值的選取","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 首先我們認爲卡頓本身是一個很主觀的東西,就好像有人覺得打王者榮耀玩流暢模式(30FPS)好像也還算流暢,有人會覺得不開高幀率(60FPS)就沒法玩。那有沒有什麼較爲客觀一點的標準?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在網上查閱了查閱了大量研究資料後,我們找到了一篇發表在ICIP上的論文 ","attrs":{}},{"type":"link","attrs":{"href":"https://www.researchgate.net/profile/Zhan-Ma-6/publication/224359119_Modeling_the_impact_of_frame_rate_on_perceptual_quality_of_video/links/00b7d514f3b347250e000000/Modeling-the-impact-of-frame-rate-on-perceptual-quality-of-video.pdf","title":null,"type":null},"content":[{"type":"text","marks":[{"type":"underline","attrs":{}}],"text":"Modeling the impact of frame rate on perceptual quality of video","attrs":{}}]},{"type":"text","text":"他們選擇了6類視頻,在不同幀率下進行了測試,實驗結果如下圖所示:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/58/5880242a40c4a67c19d48870aae70fd7.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 該圖反映了幀率和人眼主觀感受之間的關係。6 個測試序列分別使用6張圖表示。每張圖x座標代表幀率,y座標代表人眼主觀感受(MOS),紅色虛線代表CIF(352×288)分辨率序列的擬合曲線,藍色虛線代表QCIF(176×144)分辨率序列的擬合曲線。主觀感受取值範圍 0-100,數值越大代表主觀感受越好。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"據此結果,實驗設計模型通過標準因子將六種場景的得分標準化,並繪製到一個座標下","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/49/49e217a822d90bbe2773230cd32593c8.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 從該圖可以看出,當幀率大於15 幀的時候,人眼的主觀感受差別不大,基本上都處於較高的水平。而幀率小於15幀以後,人眼的主觀感受會急劇下降,人眼會立刻感受到畫面的不連貫性。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"要達到15FPS,單幀耗時不能超過 16.7ms * 4。最終,我們選擇了","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":" 16.7ms * 4 (60Hz設備)","attrs":{}},{"type":"text","text":"作爲Flutter頁面卡頓的閾值。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"架構設計","attrs":{}}]},{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下圖列出前端、後端、原生、Flutter的側重點:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e3/e311d28e792520fb1a89732066aca494.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 後端的能力在數據處理這塊,原生APM監控體系中也比較成熟。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 前端頁面展示主要包含如下三個方面:","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l 頁面通用功能","attrs":{}},{"type":"text","text":":版本的數據, 版本維度數據對比","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l 頁面加載功能包括:","attrs":{}},{"type":"text","text":"訪問數,首次渲染時間,二次渲染時間,頁面生命週期","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l 頁面幀率功能包括:","attrs":{}},{"type":"text","text":"FPS平均值,平均卡頓次數。(FPS最差值,丟幀平均值,丟幀峯值,這三個值作爲參考數據來統計)","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" FlutterPlugin在客戶端主要聚焦在如下方面:","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l 頁面唯一性標識獲取","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l 頁面生命週期監控點","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l 生命週期與Platform映射規則","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l 頁面加載採集方案實現","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l 頁面加載本地計算與統計","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l 幀率數據源如何採集","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l 幀率如何計算","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l 卡頓標準如何確定","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l 上傳模塊的實現","attrs":{}}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"監控SDK實現","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"概覽圖","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c2/c2bbe127792d84269efbca619211f5cc.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Hook時機點如上圖(後面詳細介紹),由MonitorService分發事件:","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l pageChange: ","attrs":{}},{"type":"text","text":"頁面的起始點可以考慮在Navigator.push調用,但在1.22容器打開的首個頁面並不會觸發push, 我們在新版本將hook點放到Route install函數中。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l buildPage: ","attrs":{}},{"type":"text","text":"頁面構建時間耗時= buildPageEnd - buildPageStart","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l firstFrame: ","attrs":{}},{"type":"text","text":"頁面第一幀繪製完成時機 (Route buildPageEnd + postFrameCallback)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l animatorEnd: ","attrs":{}},{"type":"text","text":"頁面繪製並且動畫完成時間點 (Animation Completed + postFrameCallback)","attrs":{}}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"編譯時Hook能力(AOP)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 藉助","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"beike_aspectd (","attrs":{}},{"type":"link","attrs":{"href":"https://mp.weixin.qq.com/s?__biz=MzIyMTg0OTExOQ%3D%3D&mid=2247486207&idx=2&sn=520474e581fce9df1523e963c42ea709&chksm=e837398fdf40b0998190de240ae81d02b8731ae7bbc157ec3bab98345ea9fae250091deac11c&mpshare=1&scene=1&srcid=1103aTWnR7vfJqJH4hdDUlz5&sharer_sharetime=1635930132689&sharer_shareid=297c951c4cf4d173a55f2f115fabd97a#rd","title":"","type":null},"content":[{"type":"text","text":"文章鏈接","attrs":{}}]},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":")","attrs":{}},{"type":"text","text":"編譯時代碼注入能力,將監控庫的函數編譯時注入到app.dill中。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"涉及的內容如下:","attrs":{}}]},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"navigator hook點: push和pop的時機","attrs":{}}]}]}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/// lib/src/widgets/navigator.dart\nFuture push(Route route) {}\nvoid pop([ T? result ]) {}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 在Navigator 2.0 後命令式API變更爲聲明式API,Navigator initState中push邏輯被移除掉了,轉由initialRoute的創建route add觸發,因此我們將Page Change的時機調整到Route install()方法中。即TransitionRoute:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/// lib/src/widgets/routes.dart\n/// abstract class TransitionRoute...\nvoid install() {//增加 }","attrs":{}}]},{"type":"numberedlist","attrs":{"start":2,"normalizeStart":2},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"buildPage點: 獲取Widget的類名,頁面唯一性的一部分","attrs":{}}]}]}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/// lib/src/material/page.dart\n/// lib/src/cupertino/route.dart\n@override\nWidget buildPage(BuildContext context,Animation animation,\nAnimation secondaryAnimation,\n) {}\n\n@Inject(\"package:flutter/src/material/page.dart\", \"MaterialPageRoute\",\n \"-buildPage\",\n lineNum: 87)\n@pragma(\"vm:entry-point\")\nvoid routeBeforePage() {\n //...\n}\n\n@Inject(\"package:flutter/src/material/page.dart\", \"MaterialPageRoute\",\n \"-buildPage\",\n lineNum: 97)\n@pragma(\"vm:entry-point\")\nvoid routeAfterPage() {\n //...\n}","attrs":{}}]},{"type":"numberedlist","attrs":{"start":3,"normalizeStart":3},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"動畫結束時機","attrs":{}}]}]}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":" @Inject(\"package:flutter/src/widgets/pages.dart\", \"PageRoute\",\n \"-createAnimationController\",\n lineNum: 41)\n @pragma(\"vm:entry-point\")\n void createAnimationController() {\n Object controller; //Aspectd Ignore\n AnimationController animationController = controller;\n // 這裏要注意1.12.13和2.x版本差異\n animationController.addStatusListener((state) {\n if (state == AnimationStatus.completed) {\n WidgetsBinding.instance.addPostFrameCallback((duration) {\n Logger.devLog('AnimatorEnd結束時間點');\n // ...\n });\n }\n });\n }","attrs":{}}]},{"type":"numberedlist","attrs":{"start":3,"normalizeStart":3},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"Widget注入方法獲取importUri","attrs":{}}]}]}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/// @Add是beike_aspectd提供的編譯時註解\n@Add(\"package:.+\\\\.dart\", \".*\", isRegex: true, superCls: 'Widget')\n@pragma(\"vm:entry-point\")\ndynamic importUri(PointCut pointCut) {\n// 獲取 importUri\n \treturn pointCut.sourceInfos[\"importUri\"];\n}","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"頁面加載","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 頁面加載主要是在對Route加載顯示頁面的流程。 主要採集內容如下:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/80/80e3f1ed3cf1db41d8ef7c279b627152.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"幀率與卡頓","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 因爲幀率的數據源是動態數據,所以用單幀時間換算FPS的計算原則來統計。以單幀的繪製效率(結合vsync信號時間)評估1秒能夠繪製的幀數。我們稱之爲: ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"單幀FPS","attrs":{}},{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":" 。","attrs":{}},{"type":"text","text":"以下以60Hz設備舉例說明其計算:","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"user"}},{"type":"strong","attrs":{}}],"text":"[單幀FPS] = 1000 / Math.max(單幀時間, 16.7 * Math.ceil(單幀時間 / 16.7))","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"主要採集內容如下: ","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/6c/6c88b18d6f2fbcd55422e5969b5248ff.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 綜上所訴, 我們對一個頁面從打開到退出的關鍵生命週期進行hook,計算對應的首幀耗時、平均 Fps、卡頓次數等數據。在頁面退出後,獲取到頁面的唯一標識(包名+類名),以及對應的性能數據,並將其上傳到遠端.","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"實踐效果","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"線下實時FPS展示面板","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 在profile/debug 模式下,FPS展示面板可以直觀的評估頁面流暢度。可以查看當前設備最近100(可配置)幀的表現情況:(如下圖)","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/7f/7f768742b6f3a4fa6680046be4b2f565.gif","alt":null,"title":"","style":[{"key":"width","value":"25%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 除了實時的FPS 查看,我們還將性能監控庫中的數據進行了本地展示(下圖)。以此掌握當前頁面在不同設備上的性能表現,進行更精確的優化。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d0/d0f004121981782418d6ca9cec9941ff.gif","alt":null,"title":"","style":[{"key":"width","value":"25%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"線上數據採集","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 下圖爲線上採集的數據,結合線下FPS工具裏採集的數據可以幫助業務方更快看到優化效果。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f4/f48028bcac4595f812b26ba10ee5007c.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/da/da10a9bf9c51a3f98813190018b6e95d.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 實際端上還採集了頁面丟幀數據,但沒有顯示在網頁上,因爲我們認爲這並不能很好的衡量實際使用過程中渲染性能。從目前資料來看影響的因素有2點:","attrs":{}}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":1,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"幀率不能過低, 並且保持穩定:如持續低於30fps時,動畫連貫性受到影響.","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":1,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"幀率穩定: 如fps是60,20,60,30,... 等不均勻速率, 容易產生的視覺上的卡頓.","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於FPS計算,目前騰訊","attrs":{}},{"type":"link","attrs":{"href":"https://perfdog.qq.com/","title":"","type":null},"content":[{"type":"text","text":"PrefDog","attrs":{}}]},{"type":"text","text":"比較高得影響力,其中說到衡量FPS的要點: ","attrs":{}}]},{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1) Avg(FPS):平均幀率(一段時間內平均FPS)   ","attrs":{}}]},{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2) Var(FPS):幀率方差(一段時間內FPS方差)  ","attrs":{}}]},{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3) Drop(FPS):降幀次數(平均每小時相鄰兩個FPS點下降大於8幀的次數)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以單從FPS平均值數據的說服力不足以佐證上述第一點內容,因此我們將FPS平均值、FPS最差值作爲參考值呈現在網頁上,待後續完善。目前我們在Flutter幀率上主要採用卡頓指標(即上面的第二個因素幀率是否穩定)來評估頁面渲染性能。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"總結","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 本文主要介紹貝殼早期在Flutter 1.12.13 (1.22、2.0已適配)性能監控實踐過程中的一些思路和實現的方式。APP監控需要深入挖掘運行機制,更深層次的機制原理之後的文章會詳細描述(如詳細的流程,渲染原理等)。此外Flutter版本迭代很頻繁,一些監控時機很可能在下一個穩定版就不適用,這就需要開發者去找到更合理的點。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 最後,頁面加載、頁面幀率、頁面卡頓等性能數據幫助了我們優化提升了APP的使用體驗。如何更精準獲取數據、更合理的處理數據, 並驅動改進提升APP的性能也是我們的終極目標。就像","attrs":{}},{"type":"link","attrs":{"href":"https://mp.weixin.qq.com/s?__biz=MzIyMTg0OTExOQ==&mid=2247487737&idx=1&sn=47a1b8196d6d8db5e73a896812e06201&chksm=e8372389df40aa9f5984bcd1f194d3565867ed4fc1f45ddbd9b8ea74256f5f0650dc4052988d&mpshare=1&scene=1&srcid=1108WIiQjOJUX2UWKwfLV6r5&sharer_sharetime=1636355906002&sharer_shareid=297c951c4cf4d173a55f2f115fabd97a&version=3.1.19.90358&platform=mac#rd","title":"","type":null},"content":[{"type":"text","text":"KeFrame流暢度優化組件","attrs":{}}]},{"type":"text","text":"一樣能幫助解決實際卡頓問題。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章