翻譯自 Android 開發者訓練課程,原文鏈接:Performance tips
這篇文檔主要涵蓋了一些微小的優化,組合它們能夠提升應用的整體性能,但是這些變化不會帶來戲劇性的效果。你應該優選選擇正確的算法和數據結構,但是它超出了本文檔要說明的範圍。在一般的開發練習中,你應該使用本文檔中的提示,這樣才能把提高代碼效率當成一種習慣。
編寫高效代碼的兩個基本原則:
- 不要做不該做的事
- 儘量避免分配內存
當你微優化安卓應用時,面對最棘手的問題之一就是,你的應用會運行在各種不同類型的硬件上。不同版本的虛擬機跑在不同處理器上,運行速度也不同。通常你不能簡單地說,設備 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
直接訪問 Foo
的私有成員是非法的,因爲 Foo
和 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 的情況下運行時生成的代碼實際運行得更快時,這一點尤其重要。