優化雲課堂直播間性能的一些思考與總結

本文來自網易雲社區

 

1、本文背景

雲課堂Android端的Native直播間模塊,聊天面板滑動有些卡頓,在彈起、收起輸入鍵盤的時候頁面有明顯的閃動,另外在橫豎屏切換的時候也不流暢。同時在播放視頻的時候,在對比其餘APP產品,發如今CPU及電量使用上有稍許劣勢。android

 

2、問題分析

流暢的樣子應該是,系統每秒60幀的渲染頻率的話,也就是16ms一幀的速度。而出現卡頓,閃動,就是說應用在16ms內沒有完成相應的數據更新操做,致使這幾幀的畫面無法及時更新或者直接被丟棄了。另外,也可能存在內存抖動,由於在虛擬機GC的時候,全部其餘線程都會暫停,這也會致使畫面丟幀。因此,咱們須要找尋系統在進行每幀渲染前的耗時操做以及查看內存狀況。canvas

 

3、Android性能優化方向

在考慮優化方案時,先讓咱們回顧下Android的渲染機制,內存與GC。性能優化

 

一、關於渲染工具

Android系統每隔16ms發出VSYNC信號,觸發對UI進行渲染,若是每次渲染均可以繪製成功的話,這樣就能達到流暢的畫面所須要的60fps,這就意味着,程序在更新畫面的時候,操做必須在16ms內完成。若是你的某個操做時間超過了16ms,系統在獲得VSYNC信號的時候就沒法進行正常渲染,這樣就發生了丟幀現象,用戶在32ms內看到的會是同一幀畫面。佈局

在面對這樣的問題,咱們一般能夠經過一些工具進行定位問題,好比可使用HierarchyViewer來查找Activity中的佈局是否過於複雜,也能夠打開手機的Show GPU Overdraw選項,查看是否存在過分繪製。另外也可使用TraceView、Systrace來觀察CPU的執行狀況。性能

 

二、關於過分繪製優化

對於在多層次的UI結構裏面,若是不可見的UI也在作繪製的操做,這樣就會致使某些像素區域被繪製了屢次。這樣會浪費大量的CPU以及GPU資源。動畫

 

 

好比Activity有一個白色的背景,而後Layout又有本身的背景,同時子View又分別有本身的背景。這樣就繪製了三次!!!ui

 

三、關於GPU繪製圖形信息。spa

在開發這工具中有一個選項用於打開Profile GPU Rendering。選擇了之後,咱們能夠在手機畫面上看到豐富的GPU繪製圖形信息,分別關於StatusBar,NavBar,激活的程序Activity區域的GPU Rending信息。 隨着滾動界面,界面上會滾動顯示垂直的柱狀圖來表示每幀畫面所須要渲染的時間,柱狀圖越高表示花費的時間越長。中間會有一根綠色的橫線,表明16ms,咱們須要確保每一幀花費的總時間都低於這條橫線,這樣才能避免出現卡頓的問題。

每一條柱狀線包含三部分,藍色表明測量繪製Display List的時間紅色表明OpenGL渲染Display List所須要的時間黃色表明CPU等待GPU處理的時間

 

四、理解GPU工做

activity的畫面是如何繪製到屏幕上的?複雜的xml佈局文件是如何可以被識別並繪製出來的?

Resterization柵格化,它把那些組件拆分到不一樣的像素上進行顯示。 CPU負責把UI組件計算成Polygons,Texture紋理,而後交給GPU進行柵格化渲染。 然而每次從CPU轉移到GPU是一件很麻煩的事情,所幸的是OpenGL ES能夠把那些須要渲染的紋理保存在GPU Memory裏面,在下次須要渲染的時候直接進行操做。因此若是更新了GPU所保存住的紋理內容,那麼以前保存的狀態就丟失了。

 

 

在Android裏面那些由主題所提供的資源,例如Bitmaps,Drawable都是一塊兒打包到統一的Textture紋理當中,而後在傳遞到GPU裏面,這就意味每次須要使用這些資源的時候,都是直接從紋理裏面進行獲取渲染的。當隨着UI組件的愈來愈豐富,有了更多演變的形態。例如顯示圖片的時候,須要先通過CPU的計算加載到內存中,而後再傳遞給GPU進行渲染。

 

五、Invalidations,Layouts, and Performance(系統如何處理UI組件的更新操做)

當更新可視化物品的時候,Android在繪製圖案前,都會將高級的XML文件轉化爲GPU可接受的文件,而後進行屏幕渲染。這裏要藉助DisplayList(顯示列表),它基本包含了全部GPU渲染所需信息,GPU用到的資產,執行的命令。

  • 在某個View第一次須要被渲染時,DisplayList會所以而被建立,當這個View要顯示到屏幕上時,咱們會執行GPU的繪製指令進行渲染。
  • 若是在後續有執行相似移動這個View的位置等操做而須要再次渲染這個View時,僅僅須要額外一次渲染指令就夠了。
  • 若是修改了View中的某些可見組件,那麼以前的DisplayList就沒法繼續使用了,須要從新建立一個DisplayList而且從新執行渲染指令並更新到屏幕上。
  • 可怕的是,以上的流程的表現性能取決於你的View的複雜程度,View的狀態變化以及渲染管道的執行性能。好比某個Button的大小須要增大到目前的兩倍,在增大Button大小以前,須要經過父View從新計算並擺放其餘子View的位置。修改View的大小會觸發整個HierarchView的從新計算大小的操做。若是佈局複雜,這很容易致使嚴重的性能問題

這裏能夠經過打開Show GPU view Updates 來查看你的應用出現了什麼類型的失效。

 

六、Overdraw,Cliprect,QuickReject

對於非可見的高度自定義UI組件,有幾個APIs方法能夠顯著提高繪製操做的性能。

咱們能夠經過canvas.clipRect()來幫助系統識別那些可見的區域。這個方法能夠指定一塊矩形區域,只有在這個區域內纔會被繪製,其餘的區域會被忽視。這個API能夠很好的幫助那些有多組重疊組件的自定義View來控制顯示的區域。同時clipRect方法還能夠幫助節約CPU與GPU資源,在clipRect區域以外的繪製指令都不會被執行,那些部份內容在矩形區域內的組件,仍然會獲得繪製。 除了clipRect方法以外,咱們還可使用canvas.quickreject()來判斷是否沒和某個矩形相交,從而跳過那些非矩形區域內的繪製操做。

 

七、關於內存模型,Memory Churn(內存抖動)

在同一幀裏面建立過多的對象是件須要特別引發注意的事情。

Android系統裏面有一個Generational Heap Memory的模型,系統會根據內存中不一樣的內存數據類型分別執行不一樣的GC操做。例如,最近剛分配的對象會放在Young Generation區域,這個區域的對象一般都是會快速被建立而且很快被銷燬回收的,同時這個區域的GC操做速度也是比Old Generation區域的GC操做速度更快的。

 

Android內存模型

原始JVM中的GC機制在Android中獲得了很大程度上的優化。Android裏面是一個三級Generation的內存模型,最近分配的對象會存放在Young Generation區域,當這個對象在這個區域停留的時間達到必定程度,它會被移動到Old Generation,最後到Permanent Generation區域。

 

 

除了速度差別以外,執行GC操做的時候,任何線程的任何操做都會須要暫停,等待GC操做完成以後,其餘操做纔可以繼續運行。

一般來講,單個的GC並不會佔用太多時間,可是大量不停的GC操做則會顯著佔用幀間隔時間(16ms)。若是在幀間隔時間裏面作了過多的GC操做,那麼天然其餘相似計算,渲染等操做的可用時間就變得少了。

致使GC頻繁執行有兩個緣由:

  • Memory Churn內存抖動,內存抖動是由於大量的對象被建立又在短期內立刻被釋放。
  • 瞬間產生大量的對象會嚴重佔用Young Generation的內存區域,當達到閥值,剩餘空間不夠的時候,也會觸發GC。即便每次分配的對象佔用了不多的內存,可是他們疊加在一塊兒會增長Heap的壓力,從而觸發更多其餘類型的GC。這個操做有可能會影響到幀率,並使得用戶感知到性能問題。

當你大體定位問題以後,接下去的問題修復也就顯得相對直接簡單了。例如,你須要避免在for循環裏面分配對象佔用內存,須要嘗試把對象的建立移到循環體以外,自定義View中的onDraw方法也須要引發注意,每次屏幕發生繪製以及動畫執行過程當中,onDraw方法都會被調用到,避免在onDraw方法裏面執行復雜的操做,避免建立對象。對於那些沒法避免須要建立對象的狀況,咱們能夠考慮對象池模型,經過對象池來解決頻繁建立與銷燬的問題,可是這裏須要注意結束使用以後,須要手動釋放對象池中的對象。

 

八、總結

最壞狀況下16ms內須要作的事情:

  • View進行Measure,進行Layout,進行Draw
  • 爲View建立DisplayList
  • 將數據從CPU傳遞到GPU
  • GPU執行DisplayList
  • CPU等待GPU執行完畢的回調
  • 其中若是進行了GC,還會暫停全部線程。
  • ListView,綁定數據還會有各類業務邏輯。

4、優化方案

而後參照上面的幾個方面,咱們對直播間進行優化。

 

一、佈局及繪製優化

要作到讓佈局平鋪,減小嵌套。

如圖,這些都是能夠優化的地方。

 

由於ActivitLive自己就是一個殼,只須要一層FrameLayout用於添加Fragment,所以能夠減小ActivityLive的一層嵌套。

 

移除聊天室xml背景,由於聊天室的背景色和主題色是同樣的,所以不必再繪製一次。

 

某些自定義的ViewBox,自身就是RelativeLayout的子類了,所以對應的xml應該用merge標籤。

以上都是很直觀的從xml裏面修改,是最早可以想到的優化地方

接着咱們藉助Android提供的工具,使用Systrace。打開直播間,點擊彈起輸入框。獲得以下一些問題

這裏提示了過分的耗時的測量和佈局

 

首先,咱們探究一下View.GONE對佈局性能的影響。 由於在雲課堂,直播間是嵌套在一個課時模塊頁面裏面。課時頁面有2個頭圖封面CoverBox。咱們將這個頭圖設置爲GONE屬性,由於直播間用不到這個控件。

發現彈起鍵盤和收起鍵盤都會有一次的從新測量和佈局。 當咱們改爲INVISIBLE。發現不止一次的測量。

 

結論是,雖然,VIEW.GONE會觸發View樹的從新測量及佈局,但對於頁面的VIEW比較穩定在不顯示的狀態下,應該設置成GONE,減小測量和佈局。

而後課時頁有一個ViewPager,它會保存左右兩邊的View,因此當鍵盤彈起或收起的時候,會觸發整個根View的測量和佈局,從而ViewPager保存的全部的View也一樣進行。

 

 

發現左右兩邊的View,在OnMeasure和OnLayout階段並不耗時,主要耗時在當前的View。 接着,將未嵌入課時頁的直播間和嵌入課時頁的直播間進行了測量耗時的對比。

發現每一次測量rootView佈局,會屢次測量直播間view。原來是RelativeLayout致使的,這裏嵌套了兩層RelativeLayout。因此足足多了4倍的測量。

課時頁的佈局,在裏面的Relative裏面塞着直播間模塊。

 

 

儘量少用Relative嵌套太多複雜的佈局。

接着發現ListView在滾動更新View的時候,報出的警告。這說明getView()方法返回的太慢。而後經過查看方法調用時間工具

就拿聊天面板展現文本消息的這個ChatRoomViewHolderText,在綁定數據所進行的操做來分析。如圖,紅色的都是耗時的。

 

 

 

由於bingContentView方法實在每次getView()裏綁定數據的時候調用的。這個方法會比較頻繁調用,所以一些日常看起來不那麼耗時的操做,在這裏都會被放大幾十倍。如findViewById、setText、NTLog.d、getDrawableByName、ScreenUtils.dip2px。另外還有些邏輯重複,在綁定數據的最開始setText,替換Emoji表情。而後在setLinkClickIntercept方法中,爲了尋找超連接的文本,又進行了一次setText和替換Emoji。這裏其實能夠歸併在一塊兒。而對於ScreenUtils.dip2px每次都是一個固定的值,能夠用靜態變量代替。getDrawableByName能夠用成員變量代替。

 

5、優化總結

關於佈局方面:

  • 儘可能減小布局的嵌套,尤爲對於RelativeLayout,應該使用ConstrainLayout來緩解嵌套佈局。
  • 當一個頁面聚合着不少模塊內容控件,但卻依賴業務需求而只須要展現對應的部分控件。應該竟可能避免在xml裏面寫死,而應該經過動態加入或移除的方式來管理View。好處是減小View樹的複雜性,避免生成一些沒必要要的View,當頁面改動,如setVisible(GONE),觸發View樹重測時候,View樹越簡單,測量的耗時就越少。
  • 若是這個頁面某個邏輯分支裏,這個模塊是永不可見的,應該儘量設置成GONE,讓它不要參與測量和佈局。
  • 若是這個頁面控件位置都是穩定的,應該儘量使用INVISIBLE,由於setVisible(GONE)會觸發重測。
  • 一些自定義的View,如XXXView extends FrameLayout,它的xml裏應該使用merge標籤,不要增長沒必要要的View層級
  • 儘可能在頁面減小View的大小更變,這會觸發Invalidations,致使View樹重測,從新生成DisplayList。

 

繪製方面:

  • 能夠定一個這個頁面的主題色,而後刪去全部沒必要要的Background顏色繪製。
  • android對於原生的View控件有一套優化機制,就是在view不可見的狀況下,不會去繪製。而咱們自定義的view應該經過canvas.Cliprect,canvas.QuickReject等APIs進行優化
  • 避免在onDraw的操做裏進行大量的耗時或者建立對象的操做

 

對於一些耗時方法的:

  • 一些看起來不耗時的方法,放在for循環裏,onDraw,listView.getView()裏,當被平反調用時,耗時都是被放大的。
  • 方法內調用的邏輯應該清晰,合併一些方法,減小重複調用。
  • 在方法內,調用了其餘方法,而且頻繁須要使用它的返回對象的時候,應該用一個臨時變量保存起來,減小該方法的調用。如if(list.get(index)!=null){list.get(index).getName()}能夠優化。
  • 一些調用一次,而且結果就固定的,應該用靜態變量或成員變量。

 

參考文章:

LinearLayout和RelativeLayout測量分析

關於隱藏軟鍵盤出現黑屏問題記錄

Android Performance Patterns: Invalidations, Layouts, and Performance

 

本文來自網易雲社區,經做者陳柏寧受權發佈。

原文地址:優化雲課堂Android端直播間性能的一些思考與總結

更多網易研發、產品、運營經驗分享請訪問網易雲社區

相關文章
相關標籤/搜索