android 效能最佳化

來源:互聯網
上載者:User

標籤:

本章介紹android進階開發中,對於效能方面的處理。主要包括電量,視圖,記憶體三個效能方面的知識點。

1.視圖效能(1)Overdraw簡介

    Overdraw就是過度繪製,是指在一幀的時間內(16.67ms)像素被繪製了多次,理論上一個像素每次只繪製一次是最優的,但是由於重疊的布 局導致一些像素會被多次繪製,而每次繪製都會對應到CPU的一組繪圖命令和GPU的一些操作,當這個操作耗時超過16.67ms時,就會出現掉幀現象,表現為應用卡頓,所以對重疊隱藏元素的重複繪製會產生額外的開銷,需要盡量減少Overdraw的發生。

(2)Overdraw檢測

    Android提供了測量Overdraw的選項,在開發人員選項-調試GPU過度繪製(Show GPU Overdraw),開啟選項就可以看到當前頁面Overdraw的狀態,就可以觀察螢幕的繪製狀態。該工具會使用三種不同的顏色繪製螢幕,來指示 overdraw發生在哪裡以及程度如何,其中:

  • 沒有顏色: 意味著沒有overdraw。像素只畫了一次。
  • 藍色: 意味著overdraw 1倍。像素繪製了兩次。大片的藍色還是可以接受的(若整個視窗是藍色的,可以擺脫一層)。
  • 綠色: 意味著overdraw 2倍。像素繪製了三次。中等大小的綠色地區是可以接受的但你應該嘗試最佳化、減少它們。
  • 淺紅: 意味著overdraw 3倍。像素繪製了四次,小範圍可以接受。
  • 暗紅: 意味著overdraw 4倍。像素繪製了五次或者更多。這是錯誤的,要修複它們。

提高程式在視圖方面的效能, 總的原則就是:盡量避免重疊隱藏元素的繪製。

(3)Overdraw改良1)合理選擇控制項容器

    Android提供的Layout控制項主要包括 LinearLayout、TableLayout、FrameLayout、RelativeLayout。同一個介面可以使用不同的容器控制項來表達,但是各個容器控制項描述介面的複雜度是不一樣的。一般來LinearLayout最易,RelativeLayout較複雜。 LinearLayout只能用來描述一個方向上連續排列的控制項,而RelativeLayout幾乎可以用於描述任意複雜度的介面。。綜上所述: LinearLayout易用,效率高,表達能力有限。RelativeLayout複雜,表達能力強,效率低。從減少overdraw的角度來看,LinearLayout會增加控制項數的層級,自然是RelativeLayout 更優,但是當某一介面在使用LinearLayout並不會比RelativeLayout帶來更多的控制項數和控制項層級時,LinearLayout則是首選。

2)去掉window的預設背景

    當使用Android內建的一些主題時,window會被預設添加一個純色的背景,這個背景是被DecorView持有的。當自訂布局時又添加了一張背景圖或者設定背景色,那麼DecorView的background此時是無用的,但是它會產生一次Overdraw,帶來繪製效能損耗。去掉window的背景可以在onCreate()中setContentView()之後調用 getWindow().setBackgroundDrawable(null);或者在theme中添加 android:windowbackground="null"。

3)去掉其他不必要的背景

    父容器若已經有了背景,可不設定對應子控制項的背景,及大的布局背景已經設定,應避免設定局部重複的背景。

4)自訂View處理

    對於自訂的view視圖,可以通過canvas.clipRect()來協助系統識別那些可見的地區。這個方法可以指定一塊矩形地區,只有在這個地區內才會被繪製,其他的地區會被忽視。這個API可以很好的協助那些有多組重疊組件的自訂View來控制顯示的地區。同時clipRect方法還可以協助節約CPU與GPU資源,在clipRect地區之外的繪製指令都不會被執行,那些部分內容在矩形地區內的組件,仍然會得到繪製。除了clipRect方法之外,我們還可以使用canvas.quickreject()來判斷是否沒和某個矩形相交,從而跳過那些非矩形地區內的繪製操作。

5)ViewStub高效預留位置

    當遇到這樣的情況,運行時動態根據條件來決定顯示哪個View或布局。常用的做法是把View都寫在上面,先把它們的可見度都設為 View.GONE,然後在代碼中動態更改它的可見度。這種模式的缺點是耗費資源。雖然把View 的初始View.GONE但是在Inflate布局的時候View仍然會被Inflate,程式運行時仍然會建立對象,會被執行個體化,會被設定屬性,會耗費記憶體等資源。

    推薦的做法是使用android.view.ViewStub,ViewStub是一個輕量級的View,它一個看不見的,不佔布局位置,佔用資源非常小的控制項。可以為ViewStub指定一個布局,在Inflate布局的時候,只有ViewStub會被初始化,然後當ViewStub被設定為可見的時候,或是調用了ViewStub.inflate()的時候,ViewStub所向的布局就會被Inflate和執行個體化,然後ViewStub的布局屬性都會傳給它所指向的布局。這樣,就可以使用ViewStub來方便的在運行時,要還是不要顯示某個布局。

6)善用draw9patch

    給ImageView加一個邊框,通常在ImageView後面設定一張背景圖,露出邊框便完美解決問題,此時這個 ImageView,設定了兩層drawable,兩層drawable的重疊地區去繪製了兩次,導致 overdraw。最佳化方案: 將背景drawable製作成draw9patch,並且將和前景重疊的部分設定為透明。由於Android的2D渲染器會最佳化draw9patch中的透明地區,從而最佳化了這次overdraw。

7)Merge 

    使用Merge標籤來做容器控制項。第一種子視圖不需要指定任何針對父視圖的布局屬性,就是說父容器僅僅是個容器,子視圖只需要直接添加到父視圖上用於顯示 就 行。另外一種是假如需要在LinearLayout裡面嵌入一個布局 (或者視圖),而恰恰這個布局(或者視圖)的根節點也是LinearLayout,這樣就多了一層沒有用的嵌套,無疑這樣只會拖慢程式速度。而這個時候如 果我們使用merge根標籤就可以避免那樣的問題。

2.記憶體效能(1)記憶體配置與回收

    每一個進程的Dalvik Heap都反映了使用記憶體的佔用範圍。這就是通常邏輯意義上提到的Dalvik Heap Size,它可以隨著需要進行增長,但是增長行為會有一個系統為它設定上限。

    邏輯上講的Heap Size和實際物理意義上使用的記憶體大小是不對等的,Proportional Set Size(PSS)記錄了應用程式自身佔用以及與其他進程進行共用的記憶體。

    Android 系統並不會對Heap中空閑記憶體地區做磁碟重組。系統僅僅會在新的記憶體配置之前判斷Heap的尾端剩餘空間是否足夠,如果空間不夠會觸發GC操作,從而騰 出更多閒置記憶體空間。在Android的進階系統版本裡面針對Heap空間有一個Generational Heap Memory的模型,最近分配的對象會存放在Young Generation地區。當這個對象在該地區停留的時間達到一定程度,它會被移動到Old Generation,最後累積一定時間再移動到Permanent Generation地區。系統會根據記憶體中不同的記憶體資料類型分別執行不同的GC操作。例如,剛分配到Young Generation地區的對象通常更容易被銷毀回收,同時在Young Generation地區的GC操作速度會比Old Generation地區的GC操作速度更快(1所示)。

 

圖1  根據不同記憶體資料類型執行不同GC操作

    每一個Generation的記憶體地區都有固定的大小。隨著新的對象陸續被分配到此地區,當對象總的大小臨近這一層級記憶體地區的閥值時,會觸發GC操作,以便騰出空間來存放其他新的對象(2所示)。

 

圖2  對象值臨近閥值觸發GC操作

    通常情況下,GC發生的時候,所有的線程都是會被暫停。執行GC所佔用的時間和它發生在哪一個Generation也有關係,Young Generation中的每次GC操作時間是最短的,Old Generation其次,Permanent Generation最長。執行時間的長短也和當前Generation中的對象數量有關,遍曆樹結構尋找20000個對象比起遍曆50個對象自然是要慢 很多的。

(2)記憶體測試外掛程式1)LeakCanary簡介

    LeakCanary是一個用於檢測記憶體流失的工具,可以用於Java和Android,是由著名開源組織Square貢獻。

2)LeakCanary工作原理
  • RefWatcher.watch()建立一個KeyedWeakReference到北監控的對象。
  • 接下來,在後台線程中檢測這個引用是否被清除,如果沒有將會觸發GC。
  • 如果引用仍然沒有清除,將heap記憶體dump到一個.hprof的檔案存放到手機系統裡。
  • HeapAnalyzerService在另外一個獨立的進程中啟動,使用HeapAnalyzer解析heap記憶體通過HAHA這個項目
  • HeapAnalyzer計算出到GC ROOTs的最短強引用路徑決定是否發生Leak,然後建立導致泄漏的引用鏈。 結果被回傳到應用程式進程的DisplayLeakService中,然後顯示一個泄漏的通知。
(3)記憶體最佳化1)謹慎使用large heap

    Android裝置根據硬體與軟體的設定差異而存在不同大小的記憶體空間,他們為應用程式設定了不同大小的Heap限制閾值。設計時可以通過調用getMemoryClass()來擷取應用的可用Heap大小。在一些特殊的情景下,你可以通過在manifest的application標籤下添加 largeHeap=true的屬性來為應用聲明一個更大的heap空間。然後,你可以通過getLargeMemoryClass()來擷取到這個更大的heap size閾值。

    然而,聲明得到更大Heap閾值的本意是為了一小部分會消耗大量RAM的應用(例如一個大圖片的編輯應用)。不要輕易的因為你需要使用更多的記憶體而去請求一個大的Heap Size。只有當你清楚的知道哪裡會使用大量的記憶體並且知道為什麼這些記憶體必須被保留時才去使用large heap。因此請謹慎使用large heap屬性。使用額外的記憶體空間會影響系統整體的使用者體驗,並且會使得每次gc的已耗用時間更長。

2)綜合考慮裝置記憶體閾值與其他因素設計合適的緩衝大小

    例如,在設計ListView或者GridView的Bitmap LRU緩衝的時候,需要考慮的點有:

  • 應用程式剩下了多少可用的記憶體空間?
  • 有多少圖片會被一次呈現到螢幕上?有多少圖片需要事先緩衝好以便快速滑動時能夠立即顯示到螢幕?
  • 裝置的螢幕大小與密度是多少? 一個xhdpi的裝置會比hdpi需要一個更大的Cache來hold住同樣數量的圖片。
  • 不同的頁面針對Bitmap的設計的尺寸與配置是什麼,大概會花費多少記憶體?
  • 頁面圖片被訪問的頻率?是否存在其中的一部分比其他的圖片具有更高的訪問頻繁?如果是,也許你想要儲存那些最常訪問的到記憶體中,或者為不同組別的位元影像(按訪問頻率分組)設定多個LruCache容器。
3)資源檔需要選擇合適的檔案夾進行存放

    hdpi/xhdpi/xxhdpi等等不同dpi的檔案夾下的圖片在不同的裝置上會經過scale的處理。例如我們只在hdpi的目錄下放置了一 張100100的圖片,那麼根據換算關係,xxhdpi的手機去引用那張圖片就會被展開到200200。需要注意到在這種情況下,記憶體佔用是會顯著提高 的。對於不希望被展開的圖片,需要放到assets或者nodpi的目錄下。

4)Try catch某些大記憶體配置的操作

    在某些情況下,我們需要事先評估那些可能發生OOM的代碼,對於這些可能發生OOM的代碼,加入catch機制,可以考慮在catch裡面嘗試一次降級的記憶體配置操作。例如decode bitmap的時候,catch到OOM,可以嘗試把採樣比例再增加一倍之後,再次嘗試decode。

5)謹慎使用static對象

    因為static的生命週期過長,和應用的進程保持一致,使用不當很可能導致對象泄漏,在Android中應該謹慎使用static對象。

6)特別留意單例對象中不合理的持有

    雖然單例模式簡單實用,提供了很多便利性,但是因為單例的生命週期和應用保持一致,使用不合理很容易出現持有對象的泄漏。

7)珍惜Services資源

    應用需要在後台使用service,除非它被觸發並執行一個任務,否則其他時候Service都應該是停止狀態。另外需要注意當這個service 完成任務之後因為停止service失敗而引起的記憶體流失。 當你啟動一個Service,系統會傾向為了保留這個Service而一直保留Service所在的進程。這使得進程的運行代價很高,因為系統沒有辦法把 Service所佔用的RAM空間騰出來讓給其他組件,另外Service還不能被Paged out。這減少了系統能夠存放到LRU緩衝當中的進程數量,它會影響應用之間的切換效率,甚至會導致系統記憶體使用量不穩定,從而無法繼續保持住所有目前正在啟動並執行service。 建議使用IntentService,它會在處理完交代給它的任務之後儘快結束自己。

8)使用ProGuard來剔除不需要的代碼

    ProGuard能夠通過移除不需要的代碼,重新命名類,域與方法等等對代碼進行壓縮,最佳化與混淆。使用ProGuard可以使得你的代碼更加緊湊,這樣能夠減少mapping代碼所需要的記憶體空間。

9)使用更加輕量的資料結構

    考慮使用ArrayMap/SparseArray而不是HashMap等傳統資料結構。

    HashMap的容器,相比起 Android專門為移動作業系統編寫的ArrayMap容器,在大多數情況下,都顯示效率低下,更占記憶體。通常的HashMap的實現方式更加消耗記憶體,因為它需要一個額外的執行個體對象來記錄Mapping操作。另外,SparseArray更加高效,在於他們避免了對key與value的自動裝箱 (autoboxing),並且避免了裝箱後的解箱。

10)避免在Android裡面使用Enum

    Android 官方培訓課程提到過“Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.”,具體原理請參考《Android效能最佳化典範(三)》,所以請避免在Android裡面使用到枚舉。

11)減小Bitmap對象的記憶體佔用

    Bitmap是一個極容易消耗記憶體的大胖子,減小建立出來的Bitmap的記憶體佔用可謂是重中之重,通常來說有以下2個措施:

  • inSampleSize:縮放比例,在把圖片載入記憶體之前,我們需要先計算出一個合適的縮放比例,避免不必要的大圖載入。
  • decode format:解碼格式,選擇ARGB_8888/RBG_565/ARGB_4444/ALPHA_8,存在很大差異。
12)使用更小的圖片

    在涉及給到資源圖片時,我們需要特別留意這張圖片是否存在可以壓縮的空間,是否可以使用更小的圖片。盡量使用更小的圖片不僅可以減少記憶體的使用,還能避免出現大量的InflationException。假設有一張很大的圖片被XML檔案直接引用,很有可能在初始化視圖時會因為記憶體不足而發生InflationException,這個問題的根本原因其實是發生了OOM。

13)記憶體對象的重複利用

    大多數對象的複用,最終實施的方案都是利用對象池技術,要麼是在編寫代碼時顯式地在程式裡建立對象池,然後處理好複用的實現邏輯。要麼就是利用系統架構既有的某些複用特性,減少對象的重複建立,從而降低記憶體的分配與回收。

14)複用系統內建的資源

    Android 系統本身內建了很多的資源,比如字串、顏色、圖片、動畫、樣式以及簡單布局等,這些資源都可以在應用程式中直接引用。這樣做不僅能減少應用程式的自身負重,減小APK的大小,還可以在一定程度上減少記憶體的開銷,複用性更好。但是也有必要留意Android系統的版本差異性,對那些不同系統版本上表現存在很大差異、不符合需求的情況,還是需要應用程式自身內建進去。

15)注意Cursor對象是否及時關閉

    在程式中我們經常會進行查詢資料庫的操作,但時常會存在不小心使用Cursor之後沒有及時關閉的情況。這些Cursor的泄露,反覆多次出現的話會對記憶體管理產生很大的負面影響,我們需要謹記對Cursor對象的及時關閉。

16)避免在onDraw方法裡面執行對象的建立

    類似onDraw等頻繁調用的方法,一定需要注意避免在這裡做建立對象的操作,因為他會迅速增加記憶體的使用,而且很容易引起頻繁的gc,甚至是記憶體抖動。

17StringBuilder

    在有些時候,代碼中會需要使用到大量的字串拼接的操作,這種時候有必要考慮使用StringBuilder來替代頻繁的“+”。

18)注意Activity的泄漏

通常來說,Activity的泄漏是記憶體流失裡面最嚴重的問題,它佔用的記憶體多,影響面廣,需要特別注意以下兩種情況導致的Activity泄漏:

  • 內部類引用導致Activity的泄漏

    最典型的情境是Handler導致的Activity泄漏,如果Handler中有延遲的任務或者是等待執行的任務隊列過長,都有可能因為Handler繼 續執行而導致Activity發生泄漏。此時的參考關聯性鏈是Looper -> MessageQueue -> Message -> Handler -> Activity。為瞭解決這個問題,可以在UI退出之前,執行remove Handler訊息佇列中的訊息與runnable對象。或者是使用Static + WeakReference的方式來達到斷開Handler與Activity之間存在參考關聯性的目的。

  • Activity Context被傳遞到其他執行個體中,這可能導致自身被引用而發生泄漏。

    內部類引起的泄漏不僅僅會發生在Activity上,其他任何內部類出現的地方,都需要特別留意!可以考慮盡量使用static類型的內部類,同時使用WeakReference的機制來避免因為互相引用而出現的泄露。

19)考慮使用Application Context而不是Activity Context

    對於大部分非必須使用Activity Context的情況(Dialog的Context就必須是Activity Context),都可以考慮使用Application Context而不是Activity的Context,這樣可以避免不經意的Activity泄露。

(3)電量最佳化

    電量其實是目前手持功能最寶貴的資源之一,大多數裝置都需要不斷的充電來維持繼續使用。不幸的是,對於開發人員來說,電量最佳化是他們最後才會考慮的的事情。但是可以確定的是,千萬不能讓你的應用成為消耗電量的大戶。

有下面一些措施能夠顯著減少電量的消耗:

  • 我們應該盡量減少喚醒螢幕的次數與持續的時間,使用WakeLock來處理喚醒的問題,能夠正確執行喚醒操作並根據設定及時關閉操作進入睡眠狀態。
  • 某些非必須馬上執行的操作,例如上傳歌曲,圖片處理等,可以等到裝置處於充電狀態或者電量充足的時候才進行。
  • 觸發網路請求的操作,每次都會保持無線訊號持續一段時間,我們可以把零散的網路請求打包進行一次操作,避免過多的無線訊號引起的電量消耗。關於網路請求引起無線訊號的電量消耗。
1)消耗電量的幾個主要原因、功能
  • 大資料量的網路傳輸(網路)
  • 不停的網路切換(網路)
  • 解析大量的資料(CPU)
2)關於網路方面的最佳化
  • 網路請求之前,檢查網路連接。沒有網路連接不進行請求
  • 判斷網路類型,針對特定的資料在特定的網路下請求。例如:大量資料轉送的時候,在wifi下請求。wifi下下載資料耗電量只有2、3、4G的1/3.
  • 使用效率高的解析工具。根據具體業務資料量的大小,選擇合適的解析工具。例如android上面的協議解析一般推薦json。
  • 使用GZIP壓縮方式下載資料,能減少網路流量,縮短下載時間
  • 合理使用緩衝,避免重複操作
  • 使用推送,代替迴圈請求
  • 觸發網路請求的操作,每次都會保持無線訊號持續一段時間,我們可以把零散的網路請求打包進行一次操作,避免過多的無線訊號引起的電量消耗。
  • 是JobScheduler API所做的事情。它會根據當前的情況與任務,組合出理想的喚醒時間,例如等到正在充電或者串連到WiFi的時候,或者集中任務一起執行。我們可以通過這個API實現很多免費的調度演算法。
3)電量最佳化策略
  • 檢查全部喚醒鎖, 是否存在冗餘或者無用的位置.
  • 集中相關的資料請求, 統一發送; 精簡資料, 減少無用資料的傳輸.
  • 分析和統計等非重要操作, 可以在電量充足或串連WIFI時進行, 參考JobScheduler.
  • 精簡冗餘的服務(Service), 避免長時間執行耗電操作.
  • 注意定位資訊的擷取, 使用後及時關閉.

android 效能最佳化

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.