攜程移動端 UI 界面性能優化實踐

lhcr3350 8年前發布 | 7K 次閱讀 性能優化 安卓開發 移動開發

人類大腦與眼睛對一個畫面的連貫性感知其實是有一個界限的,譬如我們看電影會覺得畫面很自然連貫,其幀率通常為 24fps;那么,用手機當然也需要感知屏幕操作的連貫性(尤其是動畫過渡),所以在手機領域 Android/iOS 索性就把達到這種流暢的幀率規定為 60fps。

基于上面的背景,我們開發 App 的幀率性能目標就是保持在 60fps(16ms/幀),即我們在進行 App 性能優化時,要遵循如下準則:

盡量保證每幀在 16ms 內處理完所有的 CPU 與 GPU 計算、繪制、渲染等操作,否則會造成丟幀卡頓問題。

基于上面的卡頓原理,我們知道所謂的卡頓其實是可以量化的,每次是否能夠成功渲染是非常重要的問題,即 16ms 能否完整的做完一次操作直接決定了卡頓性能問題。

引起 UI 卡頓的常見原因有如下幾種:

主線程做了阻塞 UI 的耗時操作;

同一時刻動畫執行多次導致 GPU 和 CPU 過度繪制;

View 過度繪制導致 GPU 和 CPU 過度繪制;

頻繁地進行布局繪制、文本計算等操作導致 View 需要重新渲染;

頻繁的對象創建和銷毀;

過度復雜的業務邏輯,耗時函數。

關于攜程 App 各個 UI 界面優化,我們主要是基于上述 UI 卡頓的原因圍繞著提高幀率、減少嵌套布局層次、減少對象創建等角度去解決問題的。攜程酒店和機票的幾個主流程界面,都相對比較復雜。業務邏輯功能越復雜,就越容易產生性能問題,所以常遇到布局復雜、過度繪制、UI Thread 函數耗時、內容加載慢、界面重新布局(Layout)、GC 次數多等問題。在各個版本的迭代開發過程中,我們主要分平臺 Android 和 iOS,從平臺的特性角度有針對性地去優化 UI。

攜程App Android UI 優化措施

Android 平臺主要通過優化 Layout 布局層次角度:減少層級和 Overdraw、防止不必要的重新 Layout 和 Measure、加快界面顯示速度、減少系統 GC 次數等措施去進行 UI 優化。

優化 GPU Overdraw

通過開發者選項的“Show GPU Overdraw”可以顯示檢查界面的過度繪制情況。該優化并不復雜,通過去掉層疊布局中多余的背景設置、圖片控件,有前景內容的時候不顯示背景、界面背景定義到 Activity 的主題中、減少 Drawable 的復雜 Shape 使用等手段就可以基本消除過度繪制,減少對 GPU 和 CPU 的浪費。

我們對于UI性能的優化,通過開發者選項中的GPU過度繪制工具來進行分析,在設置->開發者選項->調試GPU過度繪制(不同設備可能位置或者叫法不同)中打開調試后可以看見如下圖1所示(對攜程當前界面過度繪制進行分析)

圖1 攜程酒店列表Overdraw

colormeaning

No colorwebview

blue1*x overDraw

green2*x overDraw

Dark red3*x overDraw

red4*x overDraw

表1 顏色- overdraw示意圖

可以發現,開啟后在我們想要調試的應用界面中可以看到各種顏色的區域,具體含義如上表格所示。

由于過度繪制指在屏幕的一個像素上繪制多次(譬如一個設置了背景色的TextView就會被繪制兩次,一次背景一次文本;這里需要強調的是Activity設置的Theme主題的背景不被算在過度繪制層級中),所以最理想的就是繪制一次,也就是藍色(當然這在很多絢麗的界面是不現實的,所以大家有個度即可,我們開發性能優化標準要求:紅色區域不能長期持續超過屏幕三分之一),因此我們需要依據此顏色分布進行代碼優化,譬如優化布局層級、減少沒必要的背景、暫時不顯示的View設置為GONE而不是INVISIBLE、自定義View的onDraw方法設置canvas.clipRect()指定繪制區域或通過canvas.quickreject()減少繪制區域等措施去優化。

如上圖1所示,我們酒店列表 ListView列表的Item 經過優化之后,基本是呈現綠色,在優化之前大部分是紅色。

優化布局層級

UI布局嵌套層級越多,測量和布局的時間就會相應增加,同時底層 Framework 創建硬件列表的時間也會相應增加。因為歷史遺留的原因,有時為了增加布局的可讀性,我們會嵌套不同層次的父布局來實現原本只要簡單布局就可以實現的功能,有時還會添加一些測試階段才會使用的布局。通過刪除無用的層級,或者對整個布局進行改造使用RealtiveLayout替換LinerLayout減少布局層級;此外,使用Merge標簽或ViewStub標簽來優化整個布局性能,比如一些顯示錯誤界面、加載提示框界面等,不是必須顯示的這些布局可以使用ViewStub標簽來提升性能。

在做具體優化工作時,我們借助系統自帶的 HirectViewer、Dump UI Hierarchy for UI Automator 等工具去分析系統 UI 的層級,類似的分析結果圖如下圖2所示:

圖2 攜程首頁Dump UI Hierarchy for UI Automator

通過上圖2所示,可以分析當前界面View的渲染嵌套層級,一般不建議超過11層,如果超過11層,說明需要進行布局優化,如我們當時通過減少不必要的parent layout 、ViewStub 等機制去優化。

加快界面加載

除了從 XML Layout 文件里面角度減少布局層級,還通過提前加載布局,即在線程中做一些必要的 inflate 等來提前初始化布局,減少實際顯示時的耗時。對于一些復雜的布局,我們還會自己做View對象復用池,減少 inflate 帶來的性能損耗,特別是在列表控件中。

可以通過 TraceView 工具找出主線程的耗時操作和其他耗時的線程并作優化,另外減少主線程的 GC 停頓。因為即使并行 GC,也會對 heap 加鎖,如果主線程請求分配內存的話,也會被掛起,所以盡量避免在主線程分配較多對象和較大的對象,特別是在onDraw等函數中,以減少被掛起的時間。另外可以通過去掉 ListView、ScrollView 等控件的 EdgeEffect 效果,來減少內存分配和加快控件的創建時間。

利用本地緩存,主要界面緩存上次的數據,并配合增量的更新和刪除,能做到數據和服務端同步,這樣可以直接展示本地數據,不用等到網絡返回數據。

減少不必要的數據協議字段,減少名字長度等,并作壓縮。還可以通過分頁加載數據來加快傳輸解析時間。因為數據越大,傳輸和解析時間也會越久,引發的內存對象分配也會越多。

注意線程的優先級,對于占用 CPU 較多時間的函數,也要判斷線程的優先級。

自定義控件防止重新布局

在 ListView 滑動、廣告動畫變化等過程中,圖片和文字有變化,經常會發現整個界面被重新布局,影響了性能。尤其布局復雜時,測量過程很費時,導致明顯卡頓。比如對于大小基本固定的控件和布局例如 TextView、ImageView 來說,這是多余的損耗。采取優化措施,我們使用自定義控件來阻斷,重寫方法requestLayout、onSizeChanged,如果大小沒有變化就阻斷這次請求。對于 ViewPager 等廣告條,可以設置緩存子 View 的數量為廣告的數量。

減少系統GC次數

Android 上的 GC 會引起性能卡頓,必須重點優化。除了于圖片內存引起 GC 的優化,我們還做了如下工作:

減少對象分配,找出不必要的對象分配,如可以使用非包裝類型時,使用了包裝類型,避免 Autoboxing -> unboxing 的過程,同時避免大量對象字符串的+號操作,如果不考慮線程安全引起的問題時,優先使用 StringBuilder,而非StringBuffer去進行字符串操作,Handler.post(Runnable r)等頻繁使用。

對象的復用,對于頻繁分配的對象需要使用復用池。

盡早釋放無用對象的引用,特別是大對象和集合對象,通過置為,及時回收。

防止泄露,除了最基本的文件、流、數據庫、網絡訪問等都要記得關閉以及unRegister自己注冊的一些事件外,還要盡量少地使用靜態變量和單例。此外通過系統提供的 Android Lint 靜態掃描工具可以去提前分析類似的 Handler 泄露和 Thread 造成的內存泄露。

控制finalize方法的使用,在高頻率函數中使用重寫了finalize的類,會加重 GC 負擔,使得性能上有幾倍的差別。

合理選擇容器,在性能上優先考慮。

數組,即使我們現在習慣了使用容器,也要注意頻繁使用容器在性能上的隱患點:首先是擴容開銷,HashMap 擴容時重新 Hash 的開銷較大。其次是內存開銷,HashMap 需要額外的 Map.Entry 對象分配,需要額外內存,也容易產生更多的內存碎片。SparseArray 和 ArrayList 等在內存方面更有優勢。再次是遍歷,對于實現了RandomAccess接口的容器如 ArryList 的遍歷,不應該使用foreach循環。在移動設備中,盡量避免使用枚舉,通過自定義的表意清晰的int常量去替代。

用工具監控和精雕細琢:在頁面滑動過程中,通過 Android Studio 自帶的 Memory Monitor 工具查看內存波動和 GC 情況,還可通過 Allocation Tracker 工具觀察分析內存的分配,發現很多小對象的分配問題以及是否存在內存泄露問題。在我們平時的開發工作中,我們還集成了 LeakCannary 去監測內存泄露情況。

利用 Trace For OpenGL 工具找出界面上導致硬件加速耗時的點,例如一些圓角圖片的處理等。

其他細節方面的優化

通過 TraceView 工具發現,一些 Banner 輪播廣告和文字動畫在移出可視區域后,仍然存在定時刷新,不僅耗電也影響幀率。優化措施是在移出可視區域后停止動畫輪播。

中間件的代碼被上層業務方調用得比較頻繁,容易有較多的高頻率函數,也容易產生細節上的問題。除了頻繁分配對象外,例如類初始化性能、同步鎖的額外開銷、接口的調用時間、枚舉的使用等都是不能忽視的問題。

攜程 App iOS UI 優化措施

iOS 平臺也是主要通過如何減少 GPU 和 CPU 資源消耗機制去優化 UI,主要是通過避免頻繁對象的創建、調整、銷毀、布局計算、圖片渲染等角度去分析和定位解決問題。我們使用 Instuments 的 GPU Driver 預設,能夠實時查看到 CPU 和 GPU 的資源消耗。在這個預設內,你能查看到幾乎所有與顯示有關的數據,比如 Texture 數量、CA 提交的頻率、GPU 消耗等,在定位界面卡頓產生問題的原因。

優化對象創建和對象銷毀

對象的創建會分配內存、調整屬性,甚至還有讀取文件等操作,比較消耗 CPU 資源。盡量用輕量的對象代替重量的對象,可以對性能有所優化。比如 CALayer 比 UIView 要輕量許多,那么不需要響應觸摸事件的控件,用 CALayer 顯示會更加合適。如果對象不涉及 UI 操作,則盡量放到后臺線程去創建。

對象的銷毀雖然消耗資源不多,但累積起來也是不容忽視的。通常當容器類持有大量對象時,其銷毀時的資源消耗就非常明顯。同樣的,如果對象可以放到后臺線程去釋放,那就挪到后臺線程去。這里有個小 Tip:把對象捕獲到 block中,然后扔到后臺隊列去隨便發送個消息以避免編譯器警告,就可以讓對象在后臺線程銷毀了。

避免頻繁對象調整

對象的調整也經常是消耗 CPU 資源的地方。這里特別說一下CALayer:CALayer內部并沒有屬性,當調用屬性方法時,它內部是通過運行時 resolveInstanceMethod為對象臨時添加一個方法,并把對應屬性值保存到內部的一個 Dictionary 里,同時還會通知delegate、創建動畫等等,非常消耗資源。UIView 的關于顯示相關的屬性(比如 frame/bounds/transform)等實際上都是CALayer屬性映射來的,所以對 UIView 的這些屬性進行調整時,消耗的資源要遠大于一般的屬性。對此你在應用中,應該盡量減少不必要的屬性修改。

當視圖層次調整時,UIView、CALayer 之間會出現很多方法調用與通知,所以在優化性能時,我們從代碼角度去優化,即盡量避免調整視圖層次、添加和移除視圖。

TableView 控件優化

對于一個大型 App 來說,很多地方都需要使用 TableView 這個控件去完成,所以針對性的 TableView 優化是個重點。

當獲取到網絡列表數據后,我們會把每條 Cell 需要的數據都在后臺線程計算并封裝為一個布局對象 CellLayout。CellLayout 包含所有文本的 CoreText 排版結果、Cell 內部每個控件的高度、Cell 的整體高度等。每個 CellLayout 的內存占用并不多,所以當生成后,可以全部緩存到內存,以供稍后的其他模塊和地方使用。這樣做的好處就是當 TableView 在請求各個高度函數時,不會消耗任何多余計算量;當把 CellLayout 設置到 Cell 內部時,Cell 內部也不用再計算布局了。對于通常的 TableView 來說,提前在后臺計算好布局結果是非常重要的一個性能優化點,因為heightForRowAtIndexPath:是調用最頻繁的方法。

對于 TableView 來說,Cell 內容的離屏渲染會帶來較大的 GPU 消耗。由于歷史遺留,之前為了方便快簡潔,用到了不少 Layer 的圓角屬性,你可以在低性能的設備(比如iPad 3)上快速滑動一下這個列表,能感受到雖然列表并沒有較大的卡頓,但是整體的平均幀數降了下來。用 Instuments 查看時能夠看到 GPU 已經滿負荷運轉,而 CPU 卻比較清閑。為了避免 TableView 離屏渲染,我們后期優化過程中盡量避免使用 Layer 的border、corner、shadow、mask等技術,而是盡量在后臺線程預先繪制好對應內容,也就是我們按需異步繪制。遇到復雜界面需求的性能瓶頸時,為了提升酒店和機票的列表性能要求時候,就使用了異步繪制技術。

此外,列表控件統一優化的一個點就是滑動時按需加載,這個在大量圖片按需加載的時候很有效,我們之前使用SDWebImage實現異步加載。除了上述優化點,其他大家熟知的優化點有:

正確使用reuseIdentifier來重用 Cells;

盡量使所有的 view opaque,包括 Cell 自身;

盡量少用或不用透明圖層;

如果 Cell 內顯示的內容來自Web,使用異步加載,緩存請求結果;

減少subViews的數量;

盡量少用addView給 Cell 動態添加 View,可以初始化時就添加,然后通過hide來控制是否顯示。

圖片控件的優化

攜程 App 剛開始使用SDWebImage加載圖片,當時通過 profile 工具分析發現會產生少量性能問題,并且有些地方不能滿足需求,所以我們自己實現了一個性能更高的圖片加載庫。在顯示簡單的單張圖片時,利用UIView.layer.contents就足夠了,沒必要使用UIImageView帶來額外的資源消耗,為此在CALayer上添加了setImageWithURL等方法。除此之外,還把圖片解碼等操作通過 YYDispatchQueuePool進行管理,控制了 App 總線程數量。

關于圖形繪制優化

大家知道當我們為一個UIButton設置背景圖片時,對于這個背景圖片的處理有很多種方案,比如可以使用全尺寸圖片直接設置,還可以用 resizable images,或者使用 CALayer、CoreGraphics,甚至 OpenGL 來繪制。當然,不同的方案的編碼復雜度不一樣,性能也不一樣。關于圖形繪制的不同方案的性能問題,可以看看:Designing for iOS: Graphics Performance。

簡而言之,使用pre-rendered的圖片會更快,因為這樣就不需要在程序中去創建一個圖像,并在上面繪制各種形狀了(Offscreen Rendering,離屏渲染)。但是缺點是你必須把這些圖片資源打包到代碼包,從而需要增加程序包的體積。這就是為什么resizable images是一個很棒的選擇:不需要全尺寸圖,讓 iOS 為你繪制圖片中那些可以拉伸的部分,從而減小了圖片體積;并且你不需要為不同大小的控件準備不同尺寸的圖片。比如兩個按鈕的大小不一樣,但是它們的背景圖樣式是一樣的,你只需要準備一個對應樣式的resizable image,然后在設置這兩個按鈕的背景圖時分別做拉伸就可以了。

關于圖形動畫

圖形性能對用戶體驗有直接的影響,Instruments中的Core Animation工具用于測量物理機上的圖形性能,通過視圖的刷新頻率大小來判斷應用的圖形性能。例如一個復雜的列表滾動時它的刷新率應該努力趨近于 60fps才能讓用戶覺得夠流暢,從這個數字也可以算出run loop最長的響應時間應該是16毫秒。

啟動Instruments的Core Animation工具后可以發現有Color Blended Layers, Instruments可以在物理機上顯示出被混合的圖層Blended Layer(用紅色標注),Blended Layer是因為這些Layer是透明的(Transparent),系統在渲染這些view時需要將該view和下層view混合(Blend)后才能 計算出該像素點的實際顏色,如果這種blended layer很多,那么在滾動列表時肯定不會流暢,如下圖所示:

解決blended layer問題也很簡單,檢查紅色區域view的opaque屬性,記得設置成YES;檢查backgroundColor屬性是不是[UIColor clearColor],如果背景顏色為clear color那可是圖形性能的大敵,說明需要進行優化。

如上圖中被標注為黃色的圖層,這是由于圖層顯示的是被縮放后的圖片,如果這些圖片是通過網絡下載的,可以通過程序更新為確定的繪制大小來解決。還有些系統Navigation Bar和Tool Bar的背景圖片使用的是拉伸(Streched)圖片,也會被表示為黃色,這是屬于正常情況,通常無需修改。這種問題一般對性能影響不大,而是可能會在邊緣處虛化

合理使用線程

由于 GCD 實在太方便了,如果不加控制,大部分需要拋到子線程操作都會被直接加到 global 隊列,這樣會導致兩個問題:

開的子線程越來越多,線程的開銷逐漸明顯。因為開啟線程需要占用一定的內存空間(默認的情況下,主線程占 1M,子線程占用 512KB);

多線程情況下,網絡回調的時序問題,導致數據處理錯亂,而且不容易發現。

為此,我們定了一些基本原則:

UI 操作和 DataSource 的操作一定在主線程;

DB 操作、日志記錄、網絡回調都在各自的固定線程;

不同業務,可以通過創建隊列保證數據一致性。例如,酒店列表的數據加載、機票數據列表的加載等。

合理的線程分配,最終目的就是保證主線程盡量少地處理非 UI 操作,同時控制整個 App 的子線程數量在合理的范圍內。

合理使用數據結構

根據不同的業務場景選擇合適的數據結構,可能在數據量不是很大的情況下看不出來,但是假如你存儲的數據量較大并且數據結構比較復雜的情況下,這就有可能會影響你程序的性能,一般用的比較多的數據結構是array,它的查找時間復雜度是o(n),如果為了快速查找某個元素,建議使用map數據結構去代替。

重用和延遲加載Views

如果子view更多意味著需要進行系統更多的渲染實現,也就是需要消耗系統更多的CPU和內存,對于那種嵌套了很多view在UIScrollView里邊的app更是如此。

這里我們優化的小技巧就是模仿UITableView和UICollectionView的操作: 不需要一次創建所有的subview,而是當需要時才創建,當它們完成了使命,把他們放進一個可重用的隊列中,即自己實現了View對象池復用技術,這樣的話就只需要在滾動發生時創建你的views,避免了不劃算的內存分配,從而節省APP的使用內存分配。關于對象池復用技術和延遲加載適用于APP很多的業務場景,比如酒店房型列表等業務的分段延時加載。

總結

Android 和 iOS 平臺, Android studio 和 xcode都自帶關于UI性能分析工具,Android 有 HirectView、TraceView 等工具,Xcode 有Instruments(Core Animation)等工具,最后我們通過以上多種工具和技術手段配合,攜程 App 各個界面性能上有了較大的提高,平均幀率提高了 25% 以上,界面加載時間提高了 20% 以上。

隨著移動端技術的不斷成熟發展,以及各公司業務的成熟穩定發展,APP性能優化成為各大公司重點關注的問題,目的就是為了提升用戶的使用體驗。并且性能優化是一個持續發展的實踐課題,可以持續貫穿于我們日常的開發工作中,即隨著手機機型的日益碎片化,程序功能的復雜化多樣化,性能調優是沒有止境的。

從持續不斷的優化中,我們雖然積累了不少優化經驗,但是在 Android 平臺部分,較低端低配置機型上攜程 App 性能問題依然不容樂觀。千里之行始于足下,千里之堤毀于蟻穴,欲窮千里目,還需更上一層樓,接下來我們會繼續努力通過更多更細致的優化方案來來提升用戶體驗。

未來我們基于之前積累的歷史優化經驗會形成一套性能優化的經驗閉環,由觀察問題現象到分析原因,建立監控,定下量化目標,執行優化方案,驗證結果數據再回到觀察新問題。每一次閉環只能解決部分問題,不積硅步無以至千里 不積小溪無以成江海,只有不斷抓住細微的優化點持續“啃”下去,才能得到螺旋上升的良好結果。

 

 

來自:http://www.chinacloud.cn/show.aspx?id=24383&cid=16

 

 本文由用戶 lhcr3350 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!