Android應用程式啟動並執行行動裝置受限於其運算能力,儲存空間,及電池續航。由此,它必須是高效的。電池續航可能是一個促使你最佳化程式的原因,即使他看起來已經啟動並執行足夠快了。由於續航對使用者的重要性,當電量耗損陡增時,意味這使用者遲早會發現是由於你的程式。
雖然這份文檔主要包含著細微的最佳化,但這些絕不能成為你軟體成敗的關鍵。選擇合適的演算法和資料結構永遠是你最先應該考慮的事情,但這超出這份文檔之外。
1. 介紹
寫出高效的代碼有兩條基本的原則:
◆ 不作沒有必要的工作
◆ 盡量避免記憶體配置。
2. 明智的最佳化
這份文檔是關於Android規範的細微最佳化,所以先確保你已經瞭解哪些代碼需要最佳化,並且知道如何去衡量你所做修改所帶來的效果(好或壞)。用開投資開發的時間是有限的,所以明智的時間規劃很重要。
這份文檔同時確保你在演算法和資料結構上作出最佳選擇,同時考慮了API選擇所帶來的潛在影響。使用恰當的資料結構和演算法比這裡的任何建議都有價值,考慮API版本帶來的影響會如實你選擇更好的實現。
當你最佳化Android程式時會遇到的一個棘手問題是確保你的程式能在不同的硬體平台上運行。不同版本的虛擬機器在不同處理器上的運行速度各不相同。並且不是簡單的裝置A比裝置B快或者慢,並針對一個裝置與其他裝置之間做出排列。特別的,模擬器上只能評測小部分可以在裝置上體現的東西。有無JIT的裝置間也有著巨大差異:對於有JIT裝置好的代碼有時對無JIT的裝置並不是最好的。
如果你想知道程式在裝置上的表現,就必須在上面進行測試
3. 避免建立不必要的對象
對象建立永遠不會免費的。每個線程的分代GC給臨時對象分配一個位址集區能降低分配開銷,但分配記憶體往往需要比不分配記憶體高的代價。
如果在使用者介面周期內指派至,會強制一個周期性的記憶體回收,給使用者體驗造成小小的停頓間隙。Gingerbread中介紹的並發回收也許有用,但應該避免不必要的工作。
因此,避免建立不需要的對象執行個體。下面是幾個例子:
◆ 如果有一個返回String的方法,他的傳回值通常附加在一個StringBuffer上,改變聲明和實現,這樣函數直接在其後面附加,而非建立一個短暫存在的臨時變數。
◆ 當從輸入的資料集合中讀取資料時,考慮返回未經處理資料的子串,而非建立一個拷貝。這樣你會建立一個新的對象,但是他們共用該資料的char數組。換來的是即使你僅僅使用原始輸入的一部分,你也需要保證它一直存在於記憶體中。
一個更徹底的觀點是將多維陣列切割成一維數組:
◆ Int類型的數組比Integer類型的好。推而廣之,兩個平行的int數組要比一個(int,int)型的對象數組高效。這個定理對於任何基礎資料型別 (Elementary Data Type)的組合都通用。
◆ 如果需要實現存放元組(Foo,Bar)對象的容器,記住兩個平行數組Foo[], Bar[]會優於一個(Foo,Bar)對象的數組。(例外情況是:當你設計API給其他代碼調用時,最好用好的API設計來換取小的速度提升。但在自己的內部代碼中,盡量嘗試高效的實現。)
通常來說,盡量避免建立短時臨時對象。少的對象建立意味著低頻的記憶體回收。這對於使用者體驗產生直接的影響。
4. 效能之謎
前一個版本的文檔給出了好多誤導人的主張,這裡做一些澄清:
◆ 在沒有JIT的裝置上,調用方法所傳遞的對象採用具體的類型而非介面類型會更有效(比如,傳遞HashMap map比傳遞Map map調用一個方法耗費的開銷小,儘管兩種情況下的map都是HashMap)。但這並不是兩倍慢的情形,事實上,只相差6%,而JIT使這兩種調用的效率不分伯仲。
◆ 在沒有JIT的裝置上,訪問緩衝後的欄位比直接存取欄位快大概20%。在有JIT的情況下,欄位訪問和局部訪問耗費是一樣的 。所以這裡不值得最佳化,除非你覺得他會讓你的代碼更易讀(對於final,static,及static final 變數同樣適用).
5. 用靜態代替虛擬
如果不需要訪問某對象的欄位,將方法設定為靜態,調用會加速15%到20%。這也是一種好的做法,因為你可以通過方法聲明知曉調用該方法不需要更新此對象的狀態。
6. 避免內部的Getters/Setters
在源生語言像C++中,通常做法是用Getters(i=getCount())代替直接存取欄位(i=mCount)。這是C++中一個好的習慣,因為編譯器會內聯這些訪問,如果需要約束或者調試這些域的訪問,你可以在任何時間添加代碼。
在Android中,這是個不好的想法。虛方法調用代價比直接存取欄位高昂的多。按照通常物件導向語言的做法在公用介面中使用Getters和Setters是有原因的,但應該在一個經常訪問其欄位的類中採用直接存取。
無JIT時,直接欄位訪問大約比調用無關緊要的getter來訪問快3倍。有JIT時(直接存取欄位開銷和訪問局部變數是一樣的),要快7倍。在Froyo版本中確實如此,但以後會在JIT中改進Getter方法的內聯。
7. 對常量使用Static Final修飾符
考慮下面類首的聲明:
static int intVal = 42;
static String strVal = "Hello, world!";
編譯器產生一個類初始化方法clinit, 當類初次被使用時執行,這個方法將42存入intVal中,並得到類字串常量strVal的引用。當這些值在後面被引用時,他們通過欄位尋找進行訪問。
我們改進實現,採用 final關鍵字:
static final int intVal = 42;
static final String strVal = "Hello, world!";
類不再需要clinit方法,因為常量進入了dex檔案中的靜態欄位初始化器中。引用intVal的代碼,直接調用整形值42,而訪問strVal時也會採用相對開銷較小的 string constant(字串常量)指令替代欄位尋找。(這種最佳化僅僅是針對基礎資料型別 (Elementary Data Type)和String類型常量的,而非任意的參考型別。但儘可能的將常量聲明為static final類型是一種好的做法。
8. 使用改進的For迴圈文法
改進的for迴圈(有時被稱為for-each迴圈)能夠用於實現了iterable介面的集合類及數組中。在集合類中,迭代器促使介面訪問hasNext()和next()方法,在ArrayList中,計數迴圈迭代要快3倍(無論有沒有JIT),但其他集合類中,改進的for迴圈文法和迭代器具有相同的效率。
這裡有一些迭代數組的實現:static class Foo {
int mSplat;
}
Foo[] mArray = ...
public void zero() {
int sum = 0;
for (int i = 0; i mArray.length; ++i) {
sum += mArray.mSplat;
}
}
public void one() {
int sum = 0;
Foo[] localArray = mArray;
int len = localArray.length;
for (int i = 0; i len; ++i) {
sum += localArray.mSplat;
}
}
public void two() {
int sum = 0;
for (Foo a : mArray) {
sum += a.mSplat;
}
}
zero()是當中最慢的,因為對於這個遍曆中的曆次迭代,JIT不能最佳化擷取數組長度的開銷。
One()稍快,將所有東西都放進局部變數中,避免了尋找。但僅只有數組長度促使了效能的改善。
Two()是在無JIT的裝置上運行最快的,對於有JIT的裝置則和one()不分上下。他採用了JDK1.5中的改進for迴圈文法。
結論:優先採用改進的for迴圈,但在效能要求苛刻的ArrayList迭代中考慮採用手寫計數迴圈。
9. 在私人內部內中,考慮用包存取權限替代私人存取權限
考慮下面的定義: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 is 27。
但問題是虛擬機器認為從Foo$Inner中直接存取Foo的私人成員是非法的,因為他們是兩個不同的類,儘管Java語言允許內部類訪問外部類的私人成員,編譯器產生幾個綜合方法來橋接這些間隙。
Java代碼
static int Foo.access$100(Foo foo) {
return foo.mValue;
}
static void Foo.access$200(Foo foo, int value) {
foo.doStuff(value);
}
內部類會在外部類中任何需要訪問mValue欄位或者doStuff方法的地方調用這些靜態方法。這意味著這些代碼將直接存取成員變數歸結為通過存取器方法訪問。之前提到存取器訪問如何比直接存取慢,這例子說明,某些語言約定導致了不可見的效能問題。
如果你在高效能的Hotspot中使用這些代碼,可以通過聲明被內部類訪問的欄位和成員為包存取權限,而非私人。不幸的是這意味著這些欄位會被其他處於同一個包中的類訪問,因此在公用API中不宜採用。
10. 合理利用浮點數
通常的經驗是,在Android裝置中,浮點數會比整型慢兩倍,在缺少FPU,或是JIT的G1以及有FPU和JIT的Nexus One中確實如此(兩種裝置間算數運算的絕對速度差大約是10倍).
速度術語中,在現代硬體上,float和double之間並沒有不同。更廣泛的講,double大約2倍大。在沒有儲存空間問題的案頭機器中,double的優先順序高於float。
但即使是整型,有些晶片擁有硬體乘法,卻缺少除法。這種情況下,整型除法和求模運算是通過軟體實現的,考慮下當你設計Hash表,或是做大量的算術。