翻譯自 Android 開發(fā)者訓(xùn)練課程望抽,原文鏈接:Performance tips
這篇文檔主要涵蓋了一些微小的優(yōu)化翼闹,組合它們能夠提升應(yīng)用的整體性能,但是這些變化不會帶來戲劇性的效果。你應(yīng)該優(yōu)選選擇正確的算法和數(shù)據(jù)結(jié)構(gòu),但是它超出了本文檔要說明的范圍尿扯。在一般的開發(fā)練習(xí)中,你應(yīng)該使用本文檔中的提示焰雕,這樣才能把提高代碼效率當(dāng)成一種習(xí)慣衷笋。
編寫高效代碼的兩個基本原則:
- 不要做不該做的事
- 盡量避免分配內(nèi)存
當(dāng)你微優(yōu)化安卓應(yīng)用時,面對最棘手的問題之一就是矩屁,你的應(yīng)用會運行在各種不同類型的硬件上辟宗。不同版本的虛擬機跑在不同處理器上,運行速度也不同吝秕。通常你不能簡單地說泊脐,設(shè)備 X 是比設(shè)備 Y 運行快/慢的因素,將結(jié)果從一個設(shè)備擴(kuò)展到其他設(shè)備烁峭。特別是容客,關(guān)于在其他設(shè)備上的性能,模擬器上的測量結(jié)果不全面约郁。有沒有 JIT 的設(shè)備也有非常大的差異:具有 JIT 的設(shè)備的最佳代碼并不總是沒有設(shè)備的最佳代碼缩挑。
為確保你的應(yīng)用在各種設(shè)備上都能正常運行,確保你的代碼在各個級別都高效鬓梅,并積極優(yōu)化你的性能供置。
避免創(chuàng)建不必要的對象
創(chuàng)建對象并不是沒有開銷的。分代垃圾收集器具有用于臨時對象的每個線程分配池绽快,這可以使分配更便宜芥丧,但是分配內(nèi)存總是比不分配代價要大。
當(dāng)你在應(yīng)用中創(chuàng)建更多的對象時坊罢,你將被迫進(jìn)行垃圾收集续担,對于用戶體驗來說,它就像「打嗝」一樣的活孩。在安卓 2.3 之后引入了并發(fā)垃圾收集器物遇,但是也應(yīng)該避免不必要的工作。
因此诱鞠,你要避免創(chuàng)建不必要的對象挎挖。下面是一些例子:
- 如果你的方法返回一個字符串这敬,你知道它的結(jié)果總會拼接到
StringBuffer
航夺,這時你就該更改簽名和實現(xiàn),這樣函數(shù)會直接追加崔涂,而不是創(chuàng)建存活期短的臨時對象阳掐。 - 當(dāng)從輸入數(shù)據(jù)提取字符串時,嘗試返回原始數(shù)據(jù)到子字符串,而不是創(chuàng)建一個拷貝缭保。你會創(chuàng)建一個新的
String
對象汛闸,但是它會和原始數(shù)據(jù)共享char[]
。(需要考慮的是艺骂,如果你只使用原始輸入的一小部分诸老,那么無論如何,如果你用這個方法钳恕,你都會在內(nèi)存中保留它别伏。))
一個激進(jìn)的想法是,把多維數(shù)組切片變成并行的一維數(shù)組忧额。
-
int
數(shù)組比Integer
對象數(shù)組好多了厘肮。但是概括來說,兩個并行的int
數(shù)組同樣比二維數(shù)組(int,int)
高效睦番。對于其他的基本數(shù)據(jù)類型的組合也是如此类茂。 - 如果你需要實現(xiàn)一個容器,用來存儲二元組
(Foo,Bar)
對象托嚣,記住兩個并行的Foo[]
和Bar[]
數(shù)組通常比一個常規(guī)的(Foo,Bar)
對象數(shù)組要好得多巩检。(當(dāng)然例外情況是,你為其他代碼設(shè)計 API 以進(jìn)行訪問注益。在這些情況下碴巾,為了實現(xiàn)良好的 API 設(shè)計,通常最好對速度進(jìn)行小的折衷丑搔。但是在你自己的內(nèi)部代碼中厦瓢,你應(yīng)該嘗試盡可能高效。)
一般來說啤月,盡量避免創(chuàng)建短期的臨時對象煮仇。更少地創(chuàng)建對象意味著更低頻率的垃圾回收,這對用戶體驗有直接影響谎仲。
首選靜態(tài)虛擬
如果你不需要訪問對象的字段浙垫,請將方法設(shè)為靜態(tài),調(diào)用速度就會提高 15%-20%郑诺。這也是很好的做法夹姥,因為你可以從方法簽名中看出,調(diào)用方法不能改變對象的狀態(tài)辙诞。
考慮下面的在類首部的聲明辙售。
static int intVal = 42;
static String strVal = "Hello, world!";
編譯器生成一個類的初始化方法,叫做 <clint>
飞涂,當(dāng)?shù)谝淮问褂妙惖臅r候旦部,該方法會被執(zhí)行祈搜。這個方法把值 42 存在 intVal
變量中,從類文件字符串常量表中提取一個引用指向 strVal
士八。當(dāng)稍后引用這些值時容燕,通過字段可以訪問它們。
我們可以使用 final
關(guān)鍵字改善這一步:
static final int intVal = 42;
static final String strVal = "Hello, world!";
這樣婚度,類就不需要 <clinit>
方法了蘸秘,因為常量進(jìn)入 dex 文件中的靜態(tài)字段初始值設(shè)定項。引用 intVal
的代碼會直接使用整數(shù)值 42蝗茁,訪問 strVal
會使用相對劃算的「字符串常量」指令秘血,而不是字段查找。
注意:此優(yōu)化僅適用于基本類型和字符串常量评甜,而不適用于任意引用類型灰粮。盡管如此,最好盡可能地聲明常量 static final
值忍坷。
使用增強型 for 循環(huán)
增強型 for
循環(huán)(也就是 for-each 循環(huán))可以遍歷實現(xiàn)了 Iterable
接口的集合和數(shù)組粘舟。對于集合,迭代器被分配用于創(chuàng)建叫做 hasNext()
和 next()
的接口佩研。對于 ArrayList
柑肴,一個手寫的計數(shù)循環(huán)比 for-each 快約 3 倍,但是對于其他集合旬薯,增強型 for 循環(huán)完全等同于顯式迭代器用法晰骑。
這里有幾個遍歷數(shù)組的方案:
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()
最慢,因為每次通過循環(huán)迭代獲得數(shù)組長度是有成本的绊序,JIT 還不會優(yōu)化硕舆。
one()
快一些,它將所有內(nèi)容都拉到局部變量中骤公,從而避免了查找抚官。只有數(shù)組的長度才能提供性能優(yōu)勢。
two()
在沒有 JIT 的設(shè)備上是最快的阶捆,與具有 JIT 的設(shè)備的 one() 無法區(qū)分凌节。它使用了 Java 語言 1.5 版本后引入的增強型 for 循環(huán)語法。
所以洒试,你應(yīng)該默認(rèn)使用增強型 for 循環(huán)倍奢,但是考慮一個手寫的計數(shù)循環(huán),用于性能關(guān)鍵的 ArrayList 迭代垒棋。
考慮包而不是私有內(nèi)部類的私有訪問
來看下面的類的定義:
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);
}
}
重要的是卒煞,我們定義了一個私有的內(nèi)部類 Foo$Inner
,它可以直接訪問外部類的私有方法和私有成員變量捕犬。這是合法的跷坝,代碼會打印 「Value is27」。
問題是碉碉,虛擬機認(rèn)為從 Foo$Inner
直接訪問 Foo
的私有成員是非法的柴钻,因為 Foo
和 Foo$Inner
是不同的類,即使 Java 語言允許內(nèi)部類訪問外部類的私有成員垢粮。為了彌合差距贴届,編譯器會生成一對合成方法:
/*package*/ static int Foo.access$100(Foo foo) {
return foo.mValue;
}
/*package*/ static void Foo.access$200(Foo foo, int value) {
foo.doStuff(value);
}
當(dāng)內(nèi)部類要訪問外部類的 mValue
字段或者調(diào)用 doStuff()
方法時,它會調(diào)用這些靜態(tài)方法蜡吧。這意味著上面的代碼實際上歸結(jié)為毫蚓,你通過訪問器方法訪問成員字段的情況。之前我們討論到訪問器如何比直接訪問字段更慢昔善。所以這是一個特定語言習(xí)語的例子元潘,導(dǎo)致「看不見」的表演。
避免使用浮點型
根據(jù)經(jīng)驗君仆,浮點數(shù) 比Android 設(shè)備上的整數(shù)慢約 2 倍翩概。
在速度方面,現(xiàn)代硬件上的 float
和 double
沒有區(qū)別返咱。在空間方面钥庇,double
大 2 倍。與桌面計算機一樣咖摹,假設(shè)空間不是問題评姨,您應(yīng)該更喜歡 double
。
此外萤晴,即使對于整數(shù)吐句,一些處理器也有硬件乘法但缺乏硬件除法。在這種情況下店读,整數(shù)除法和模數(shù)運算在軟件中執(zhí)行 - 如果您正在設(shè)計哈希表或進(jìn)行大量數(shù)學(xué)運算蕴侧,則需要考慮。
了解并使用庫
除了喜歡庫代碼而不是自己編寫代碼两入,請記住系統(tǒng)可以自由地用手動編譯匯編程序替換對庫方法的調(diào)用净宵,這可能比 JIT 可以生成的等效的 Java 最佳代碼更好。這里典型的例子是 String.indexOf()
和相關(guān)的 API裹纳,Dalvik 用內(nèi)聯(lián)的內(nèi)在代替择葡。類似地,System.arraycopy()
方法比帶有 JIT 的 Nexus One 上的手動編碼循環(huán)快約 9 倍剃氧。
小心使用原生方法
使用 Android NDK 的原生代碼開發(fā)應(yīng)用敏储,不一定比用 Java 語言開發(fā)的更高效。一方面朋鞍,Java 和 原生之間傳遞有損耗已添,JIT 不會跨越這些邊界優(yōu)化妥箕。如果你分配了原生資源(原生堆上的內(nèi)存,文件描述符更舞,或其他內(nèi)容)畦幢,安排及時收集這些資源可能要困難得多。你還需要為要運行的每個體系結(jié)構(gòu)編譯代碼(而不是依賴于具有 JIT 的體系結(jié)構(gòu))缆蝉。你可能甚至需要為相同的架構(gòu)編譯多個版本:為 G1 中的 ARM 處理器編譯的原生代碼無法充分利用 Nexus One 中的 ARM宇葱,以及為 Nexus One 中的 ARM 編譯的代碼不會在 G1 中的 ARM 上運行。
性能神話
在沒有 JIT 的設(shè)備上刊头,通過具有精確類型而不是接口的變量調(diào)用方法確實更有效黍瞧。(因此例如,調(diào)用 HashMap
映射上的方法比使用 Map
映射更便宜原杂,即使在這兩種情況下映射都是 HashMap
印颤。)情況并非如此慢 2 倍,實際差異更像是慢了 6%穿肄。此外膀哲,JIT 使兩者有效地難以區(qū)分。
在沒有 JIT 的設(shè)備上被碗,緩存字段訪問比重復(fù)訪問字段快約 20%某宪。使用 JIT,字段訪問的成本與本地訪問大致相同锐朴,因此除非您覺得它使代碼更易于閱讀兴喂,否則這不值得進(jìn)行優(yōu)化。(對于 final焚志,static 和 static final 字段也是如此衣迷。)
總是測量
在開始優(yōu)化之前,請確保你遇到需要解決的問題酱酬。確保你可以準(zhǔn)確衡量現(xiàn)有的績效壶谒,否則你將無法衡量嘗試的替代方案的好處。
你可能還會發(fā)現(xiàn) Traceview 對于分析很有用膳沽,但重要的是要知道當(dāng)前會禁用 JIT汗菜,這可能會導(dǎo)致它錯誤地將時間錯誤歸結(jié)為 JIT 可能能夠贏回的代碼。在 Traceview 數(shù)據(jù)建議進(jìn)行更改以確保在沒有 Traceview 的情況下運行時生成的代碼實際運行得更快時挑社,這一點尤其重要陨界。