Android 開發者指南 - 效能提示
前言
這篇文件主要涵蓋了一些微小的優化,組合它們能夠提升應用的整體效能,但是這些變化不會帶來戲劇性的效果。你應該優選選擇正確的演算法和資料結構,但是它超出了本文件要說明的範圍。在一般的開發練習中,你應該使用本文件中的提示,這樣才能把提高程式碼效率當成一種習慣。
編寫高效程式碼的兩個基本原則:
- 不要做不該做的事
- 儘量避免分配記憶體
當你微優化安卓應用時,面對最棘手的問題之一就是,你的應用會執行在各種不同型別的硬體上。不同版本的虛擬機器跑在不同處理器上,執行速度也不同。通常你不能簡單地說,裝置 X 是比裝置 Y 執行快/慢的因素,將結果從一個裝置擴充套件到其他裝置。特別是,關於在其他裝置上的效能,模擬器上的測量結果不全面。有沒有 JIT 的裝置也有非常大的差異:具有 JIT 的裝置的最佳程式碼並不總是沒有裝置的最佳程式碼。
為確保你的應用在各種裝置上都能正常執行,確保你的程式碼在各個級別都高效,並積極優化你的效能。
避免建立不必要的物件
建立物件並不是沒有開銷的。分代垃圾收集器具有用於臨時物件的每個執行緒分配池,這可以使分配更便宜,但是分配記憶體總是比不分配代價要大。
當你在應用中建立更多的物件時,你將被迫進行垃圾收集,對於使用者體驗來說,它就像「打嗝」一樣的。在安卓 2.3 之後引入了併發垃圾收集器,但是也應該避免不必要的工作。
因此,你要避免建立不必要的物件。下面是一些例子:
- 如果你的方法返回一個字串,你知道它的結果總會拼接到 StringBuffer,這時你就該更改簽名和實現,這樣函式會直接追加,而不是建立存活期短的臨時物件。
- 當從輸入資料提取字串時,嘗試返回原始資料到子字串,而不是建立一個拷貝。你會建立一個新的 String 物件,但是它會和原始資料共享 char[]。(需要考慮的是,如果你只使用原始輸入的一小部分,那麼無論如何,如果你用這個方法,你都會在記憶體中保留它。))
一個激進的想法是,把多維陣列切片變成並行的一維陣列。
- int 陣列比 Integer 物件陣列好多了。但是概括來說,兩個並行的 int 陣列同樣比二維陣列 (int,int)高效。對於其他的基本資料型別的組合也是如此。
- 如果你需要實現一個容器,用來儲存二元組 (Foo,Bar) 物件,記住兩個並行的 Foo[] 和 Bar[] 陣列通常比一個常規的 (Foo,Bar) 物件陣列要好得多。(當然例外情況是,你為其他程式碼設計 API 以進行訪問。在這些情況下,為了實現良好的 API 設計,通常最好對速度進行小的折衷。但是在你自己的內部程式碼中,你應該嘗試儘可能高效。)
一般來說,儘量避免建立短期的臨時物件。更少地建立物件意味著更低頻率的垃圾回收,這對使用者體驗有直接影響。
首選靜態虛擬
如果你不需要訪問物件的欄位,請將方法設為靜態,呼叫速度就會提高 15%-20%。這也是很好的做法,因為你可以從方法簽名中看出,呼叫方法不能改變物件的狀態。
考慮下面的在類首部的宣告。
static int intVal = 42; static String strVal = "Hello, world!";
編譯器生成一個類的初始化方法,叫做 <clint>,當第一次使用類的時候,該方法會被執行。這個方法把值 42 存在 intVal 變數中,從類檔案字串常量表中提取一個引用指向 strVal。當稍後引用這些值時,通過欄位可以訪問它們。
我們可以使用 final 關鍵字改善這一步:
static final int intVal = 42; static final String strVal = "Hello, world!";
這樣,類就不需要 <clinit> 方法了,因為常量進入 dex 檔案中的靜態欄位初始值設定項。引用 intVal 的程式碼會直接使用整數值 42,訪問 strVal 會使用相對划算的「字串常量」指令,而不是欄位查詢。
注意:此優化僅適用於基本型別和字串常量,而不適用於任意引用型別。儘管如此,最好儘可能地宣告常量 static final 值。
使用增強型 for 迴圈
增強型 for 迴圈(也就是 for-each 迴圈)可以遍歷實現了 Iterable 介面的集合和陣列。對於集合,迭代器被分配用於建立叫做 hasNext() 和 next() 的介面。對於 ArrayList,一個手寫的計數迴圈比 for-each 快約 3 倍,但是對於其他集合,增強型 for 迴圈完全等同於顯式迭代器用法。
這裡有幾個遍歷陣列的方案:
static class Foo { int splat; } Foo[] array = ... public void zero() { int sum = 0; for (int i = 0; i < array.length; ++i) { sum += array[i].splat; } } public void one() { int sum = 0; Foo[] localArray = array; int len = localArray.length; for (int i = 0; i < len; ++i) { sum += localArray[i].splat; } } public void two() { int sum = 0; for (Foo a : array) { sum += a.splat; } }
zero() 最慢,因為每次通過迴圈迭代獲得陣列長度是有成本的,JIT 還不會優化。
one() 快一些,它將所有內容都拉到區域性變數中,從而避免了查詢。只有陣列的長度才能提供效能優勢。
two() 在沒有 JIT 的裝置上是最快的,與具有 JIT 的裝置的 one() 無法區分。它使用了 Java 語言 1.5 版本後引入的增強型 for 迴圈語法。
所以,你應該預設使用增強型 for 迴圈,但是考慮一個手寫的計數迴圈,用於效能關鍵的 ArrayList 迭代。
考慮包而不是私有內部類的私有訪問
來看下面的類的定義:
public class Foo { private class Inner { void stuff() { Foo.this.doStuff(Foo.this.mValue); } } private int mValue; public void run() { Inner in = new Inner(); mValue = 27; in.stuff(); } private void doStuff(int value) { System.out.println("Value is " + value); } }
重要的是,我們定義了一個私有的內部類 Foo$Inner,它可以直接訪問外部類的私有方法和私有成員變數。這是合法的,程式碼會列印 「Value is27」。
問題是,虛擬機器認為從 Foo
Inner 是不同的類,即使 Java 語言允許內部類訪問外部類的私有成員。為了彌合差距,編譯器會生成一對合成方法:
/*package*/ static int Foo.access$100(Foo foo) { return foo.mValue; } /*package*/ static void Foo.access$200(Foo foo, int value) { foo.doStuff(value); }
當內部類要訪問外部類的 mValue 欄位或者呼叫 doStuff() 方法時,它會呼叫這些靜態方法。這意味著上面的程式碼實際上歸結為,你通過訪問器方法訪問成員欄位的情況。之前我們討論到訪問器如何比直接訪問欄位更慢。所以這是一個特定語言習語的例子,導致「看不見」的表演。
避免使用浮點型
根據經驗,浮點數 比Android 裝置上的整數慢約 2 倍。
在速度方面,現代硬體上的 float 和 double 沒有區別。在空間方面,double 大 2 倍。與桌面計算機一樣,假設空間不是問題,您應該更喜歡 double。
此外,即使對於整數,一些處理器也有硬體乘法但缺乏硬體除法。在這種情況下,整數除法和模數運算在軟體中執行 - 如果您正在設計雜湊表或進行大量數學運算,則需要考慮。
瞭解並使用庫
除了喜歡庫程式碼而不是自己編寫程式碼,請記住系統可以自由地用手動編譯彙編程式替換對庫方法的呼叫,這可能比 JIT 可以生成的等效的 Java 最佳程式碼更好。這裡典型的例子是 String.indexOf() 和相關的 API,Dalvik 用內聯的內在代替。類似地,System.arraycopy() 方法比帶有 JIT 的 Nexus One 上的手動編碼迴圈快約 9 倍。
小心使用原生方法
使用 Android NDK 的原生程式碼開發應用,不一定比用 Java 語言開發的更高效。一方面,Java 和 原生之間傳遞有損耗,JIT 不會跨越這些邊界優化。如果你分配了原生資源(原生堆上的記憶體,檔案描述符,或其他內容),安排及時收集這些資源可能要困難得多。你還需要為要執行的每個體系結構編譯程式碼(而不是依賴於具有 JIT 的體系結構)。你可能甚至需要為相同的架構編譯多個版本:為 G1 中的 ARM 處理器編譯的原生程式碼無法充分利用 Nexus One 中的 ARM,以及為 Nexus One 中的 ARM 編譯的程式碼不會在 G1 中的 ARM 上執行。
效能神話
在沒有 JIT 的裝置上,通過具有精確型別而不是介面的變數呼叫方法確實更有效。(因此例如,呼叫 HashMap 對映上的方法比使用 Map 對映更便宜,即使在這兩種情況下對映都是 HashMap。)情況並非如此慢 2 倍,實際差異更像是慢了 6%。此外,JIT 使兩者有效地難以區分。
在沒有 JIT 的裝置上,快取欄位訪問比重複訪問欄位快約 20%。使用 JIT,欄位訪問的成本與本地訪問大致相同,因此除非您覺得它使程式碼更易於閱讀,否則這不值得進行優化。(對於 final,static 和 static final 欄位也是如此。)
總是測量
在開始優化之前,請確保你遇到需要解決的問題。確保你可以準確衡量現有的績效,否則你將無法衡量嘗試的替代方案的好處。
你可能還會發現 Traceview 對於分析很有用,但重要的是要知道當前會禁用 JIT,這可能會導致它錯誤地將時間錯誤歸結為 JIT 可能能夠贏回的程式碼。在 Traceview 資料建議進行更改以確保在沒有 Traceview 的情況下執行時生成的程式碼實際執行得更快時,這一點尤其重要。
最後
在現在這個金三銀四的面試季,我自己在網上也蒐集了很多資料做成了文件和架構視訊資料免費分享給大家【包括高階UI、效能優化、架構師課程、NDK、Kotlin、混合式開發(ReactNative+Weex)、Flutter等架構技術資料 】,希望能幫助到您面試前的複習且找到一個好的工作,也節省大家在網上搜索資料的時間來學習。
資料獲取方式:加入Android架構交流QQ群聊:513088520 ,進群即領取資料!!!
點選連結加入群聊【Android移動架構總群】:加入群聊

資料大全