我們在書(shū)寫(xiě)Android軟件開(kāi)發(fā)應用程序的時(shí)候要格外關(guān)注效率。這些設備并沒(méi)有那么快,Android設備是嵌入式設備?,F代的手持設備,與其說(shuō)是電話(huà),更像一臺拿在手中的電腦。但是,即使是“最快”的手持設備,其性能也趕不上一臺普通的臺式電腦,并且受電池電量的制約。這意味著(zhù),設備沒(méi)有更多的能力,我們必須把程序寫(xiě)的盡量有效。很多能讓開(kāi)發(fā)者使他們的程序運行更有效的方法,遵照這些方法,你可以使你的程序發(fā)揮最大的效力。
簡(jiǎn)介
對于占用資源的系統,有兩條基本原則:
不要做不必要的事
不要分配不必要的內存
所有下面的內容都遵照這兩個(gè)原則。
有些人可能馬上會(huì )跳出來(lái),把本節的大部分內容歸于“草率的優(yōu)化”(xing:參見(jiàn)[The Root of All Evil]),不可否認微優(yōu)化(micro-optimization。xing:代碼優(yōu)化,相對于結構優(yōu)化)的確會(huì )帶來(lái)很多問(wèn)題,諸如無(wú)法使用更有效的數據結構和算法。但是在手持設備上,你別無(wú)選擇。假如你認為Android虛擬機的性能與臺式機相當,你的程序很有可能一開(kāi)始就占用了系統的全部?jì)却?xing:內存很小),這會(huì )讓你的程序慢得像蝸牛一樣,更遑論做其他的操作了。
Android的成功依賴(lài)于你的程序提供的用戶(hù)體驗。而這種用戶(hù)體驗,部 分依賴(lài)于你的程序是響應快速而靈活的,還是響應緩慢而僵化的。因為所有的程序都運行在同一個(gè)設備之上,都在一起,這就如果在同一條路上行駛的汽車(chē)。而這篇 文檔就相當于你在取得駕照之前必須要學(xué)習的交通規則。如果大家都按照這些規則去做,駕駛就會(huì )很順暢,但是如果你不這樣做,你可能會(huì )車(chē)毀人亡。這就是為什么 這些原則十分重要。
當我們開(kāi)門(mén)見(jiàn)山、直擊主題之前,還必須要提醒大家一點(diǎn):不管VM是否支持 實(shí)時(shí)(JIT)編譯器(xing:它允許實(shí)時(shí)地將Java解釋型程序自動(dòng)編譯成本機機器語(yǔ)言,以使程序執行的速度更快。有些JVM包含JIT編譯器。), 下面提到的這些原則都是成立的。假如我們有目標完全相同的兩個(gè)方法,在解釋執行時(shí)foo()比bar()快,那么編譯之后,foo()依然會(huì )比bar() 快。所以不要寄希望于編譯器可以拯救你的程序。
避免建立對象
世界上沒(méi)有免費的對象。雖然GC為每個(gè)線(xiàn)程都建立了臨時(shí)對象池,可以使創(chuàng )建對象的代價(jià)變得小一些,但是分配內存永遠都比不分配內存的代價(jià)大。
如果你在用戶(hù)界面循環(huán)中分配對象內存,就會(huì )引發(fā)周期性的垃圾回收,用戶(hù)就會(huì )覺(jué)得界面像打嗝一樣一頓一頓的。
所以,除非必要,應盡量避免盡力對象的實(shí)例。下面的例子將幫助你理解這條原則:
當你從用戶(hù)輸入的數據中截取一段字符串時(shí),盡量使用substring函數取得原始數據的一個(gè)子串,而不是為子串另外建立一份拷貝。這樣你就有一個(gè)新的String對象,它與原始數據共享一個(gè)char數組。
如果你有一個(gè)函數返回一個(gè)String對象,而你確切的知道這個(gè)字符串會(huì )被附加到一個(gè)StringBuffer,那么,請改變這個(gè)函數的參數和實(shí)現方式,直接把結果附加到StringBuffer中,而不要再建立一個(gè)短命的臨時(shí)對象。
一個(gè)更極端的例子是,把多維數組分成多個(gè)一維數組。
int數組比Integer數組好,這也概括了一個(gè)基本事實(shí),兩個(gè)平行的int數組比(int,int)對象數組性能要好很多。同理,這試用于所有基本類(lèi)型的組合。
如果你想用一種容器存儲(Foo,Bar)元組,嘗試使用兩個(gè)單獨的 Foo[]數組和Bar[]數組,一定比(Foo,Bar)數組效率更高。(也有例外的情況,就是當你建立一個(gè)API,讓別人調用它的時(shí)候。這時(shí)候你要注 重對API借口的設計而犧牲一點(diǎn)兒速度。當然在A(yíng)PI的內部,你仍要盡可能的提高代碼的效率)
總體來(lái)說(shuō),就是避免創(chuàng )建短命的臨時(shí)對象。減少對象的創(chuàng )建就能減少垃圾收集,進(jìn)而減少對用戶(hù)體驗的影響。
使用本地方法
當你在處理字串的時(shí)候,不要吝惜使用String.indexOf(), String.lastIndexOf()等特殊實(shí)現的方法(specialty methods)。這些方法都是使用C/C++實(shí)現的,比起Java循環(huán)快10到100倍。
使用實(shí)類(lèi)比接口好
假設你有一個(gè)HashMap對象,你可以將它聲明為HashMap或者M(jìn)ap:
Map myMap1 = new HashMap();HashMap myMap2 = new HashMap();
哪個(gè)更好呢?
按照傳統的觀(guān)點(diǎn)Map會(huì )更好些,因為這樣你可以改變他的具體實(shí)現類(lèi),只要這個(gè)類(lèi)繼承自Map接口。傳統的觀(guān)點(diǎn)對于傳統的程序是正確的,但是它并不適合嵌入式系統。調用一個(gè)接口的引用會(huì )比調用實(shí)體類(lèi)的引用多花費一倍的時(shí)間。
如果HashMap完全適合你的程序,那么使用Map就沒(méi)有什么價(jià)值。如果有些地方你不能確定,先避免使用Map,剩下的交給IDE提供的重構功能好了。(當然公共API是一個(gè)例外:一個(gè)好的API常常會(huì )犧牲一些性能)
用靜態(tài)方法比虛方法好
如果你不需要訪(fǎng)問(wèn)一個(gè)對象的成員變量,那么請把方法聲明成static。虛方法執行的更快,因為它可以被直接調用而不需要一個(gè)虛函數表。另外你也可以通過(guò)聲明體現出這個(gè)函數的調用不會(huì )改變對象的狀態(tài)。
不用getter和setter
在很多本地語(yǔ)言如C++中,都會(huì )使用getter(比如:i = getCount())來(lái)避免直接訪(fǎng)問(wèn)成員變量(i = mCount)。在C++中這是一個(gè)非常好的習慣,因為編譯器能夠內聯(lián)訪(fǎng)問(wèn),如果你需要約束或調試變量,你可以在任何時(shí)候添加代碼。
在A(yíng)ndroid上,這就不是個(gè)好主意了。虛方法的開(kāi)銷(xiāo)比直接訪(fǎng)問(wèn)成員變量大得多。在通用的接口定義中,可以依照OO的方式定義getters和setters,但是在一般的類(lèi)中,你應該直接訪(fǎng)問(wèn)變量。
將成員變量緩存到本地
訪(fǎng)問(wèn)成員變量比訪(fǎng)問(wèn)本地變量慢得多,下面一段代碼:
for (int i = 0; i < this.mCount; i++)dumpItem(this.mItems[i]);
再好改成這樣:
int count = this.mCount;Item[] items = this.mItems; for (int i = 0; i < count; i++)dumpItems(items[i]);
(使用"this"是為了表明這些是成員變量)
另一個(gè)相似的原則是:永遠不要在for的第二個(gè)條件中調用任何方法。如下面方法所示,在每次循環(huán)的時(shí)候都會(huì )調用getCount()方法,這樣做比你在一個(gè)int先把結果保存起來(lái)開(kāi)銷(xiāo)大很多。
for (int i = 0; i < this.getCount(); i++)dumpItems(this.getItem(i));
同樣如果你要多次訪(fǎng)問(wèn)一個(gè)變量,也最好先為它建立一個(gè)本地變量,例如:
protected void drawHorizontalScrollBar(Canvas canvas, int width, int height) {if (isHorizontalScrollBarEnabled()) {int size = mScrollBar.getSize(false);if (size <= 0) {size = mScrollBarSize;}mScrollBar.setBounds(0, height - size, width, height);mScrollBar.setParams(computeHorizontalScrollRange(),computeHorizontalScrollOffset(),computeHorizontalScrollExtent(), false);mScrollBar.draw(canvas);}}
這里有4次訪(fǎng)問(wèn)成員變量mScrollBar,如果將它緩存到本地,4次成員變量訪(fǎng)問(wèn)就會(huì )變成4次效率更高的棧變量訪(fǎng)問(wèn)。
另外就是方法的參數與本地變量的效率相同。
使用常量
讓我們來(lái)看看這兩段在類(lèi)前面的聲明:
static int intVal = 42;static String strVal = "Hello, world!";
必以其會(huì )生成一個(gè)叫做的初始化類(lèi)的方法,當 類(lèi)第一次被使用的時(shí)候這個(gè)方法會(huì )被執行。方法會(huì )將42賦給intVal,然后把一個(gè)指向類(lèi)中常量表的引用賦給strVal。當以后要用到這些值的時(shí)候,會(huì ) 在成員變量表中查找到他們。下面我們做些改進(jìn),使用“final"關(guān)鍵字:
static final int intVal = 42;static final String strVal = "Hello, world!";
現在,類(lèi)不再需要方法,因為在成員變量初始化的時(shí)候,會(huì )將常量直接保存到類(lèi)文件中。用到intVal的代碼被直接替換成42,而使用strVal的會(huì )指向一個(gè)字符串常量,而不是使用成員變量。
將一個(gè)方法或類(lèi)聲明為"final"不會(huì )帶來(lái)性能的提升,但是會(huì )幫助編譯器優(yōu)化代碼。舉例說(shuō),如果編譯器知道一個(gè)"getter"方法不會(huì )被重載,那么編譯器會(huì )對其采用內聯(lián)調用。
你也可以將本地變量聲明為"final",同樣,這也不會(huì )帶來(lái)性能的提 升。使用"final"只能使本地變量看起來(lái)更清晰些(但是也有些時(shí)候這是必須的,比如在使用匿名內部類(lèi)的時(shí)候)(xing:原文是 or you have to, e.g. for use in an anonymous inner class)
謹慎使用foreach
foreach可以用在實(shí)現了Iterable接口的集合類(lèi)型上。 foreach會(huì )給這些對象分配一個(gè)iterator,然后調用 hasNext()和next()方法。你最好使用foreach處理ArrayList對象,但是對其他集合對象,foreach相當于使用 iterator。
下面展示了foreach一種可接受的用法:
public class Foo {int mSplat;static Foo mArray[] = new Foo[27]; public static void zero() {int sum = 0;for (int i = 0; i < mArray.length; i++) {sum += mArray[i].mSplat;}} public static void one() {int sum = 0;Foo[] localArray = mArray;int len = localArray.length;for (int i = 0; i < len; i++) {sum += localArray[i].mSplat;}} public static void two() {int sum = 0;for (Foo a: mArray) {sum += a.mSplat;}}}
在zero()中,每次循環(huán)都會(huì )訪(fǎng)問(wèn)兩次靜態(tài)成員變量,取得一次數組的長(cháng) 度。 retrieves the static field twice and gets the array length once for every iteration through the loop.
在one()中,將所有成員變量存儲到本地變量。 pulls everything out into local variables, avoiding the lookups.
two()使用了在java1.5中引入的foreach語(yǔ)法。編譯器會(huì ) 將對數組的引用和數組的長(cháng)度保存到本地變量中,這對訪(fǎng)問(wèn)數組元素非常好。但是編譯器還會(huì )在每次循環(huán)中產(chǎn)生一個(gè)額外的對本地變量的存儲操作(對變量a的存 取)這樣會(huì )比one()多出4個(gè)字節,速度要稍微慢一些。
綜上所述:foreach語(yǔ)法在運用于array時(shí)性能很好,但是運用于其他集合對象時(shí)要小心,因為它會(huì )產(chǎn)生額外的對象。
避免使用枚舉
枚舉變量非常方便,但不幸的是它會(huì )犧牲執行的速度和并大幅增加文件體積。例如:
public class Foo {public enum Shrubbery { GROUND, CRAWLING, HANGING }}
會(huì )產(chǎn)生一個(gè)900字節的.class文件 (Foo$Shubbery.class)。在它被首次調用時(shí),這個(gè)類(lèi)會(huì )調用初始化方法來(lái)準備每個(gè)枚舉變量。每個(gè)枚舉項都會(huì )被聲明成一個(gè)靜態(tài)變量,并被賦 值。然后將這些靜態(tài)變量放在一個(gè)名為"$VALUES"的靜態(tài)數組變量中。而這么一大堆代碼,僅僅是為了使用三個(gè)整數。
這樣:
Shrubbery shrub = Shrubbery.GROUND;會(huì )引起一個(gè)對靜態(tài)變量的引用,如果這個(gè)靜態(tài)變量是final int,那么編譯器會(huì )直接內聯(lián)這個(gè)常數。
一方面說(shuō),使用枚舉變量可以讓你的API更出色,并能提供編譯時(shí)的檢查。所以在通常的時(shí)候你毫無(wú)疑問(wèn)應該為公共API選擇枚舉變量。但是當性能方面有所限制的時(shí)候,你就應該避免這種做法了。
有些情況下,使用ordinal()方法獲取枚舉變量的整數值會(huì )更好一些,舉例來(lái)說(shuō),將:
for (int n = 0; n < list.size(); n++) {if (list.items[n].e == MyEnum.VAL_X)// do stuff 1else if (list.items[n].e == MyEnum.VAL_Y)// do stuff 2}
替換為:
int valX = MyEnum.VAL_X.ordinal();int valY = MyEnum.VAL_Y.ordinal();int count = list.size();MyItem items = list.items(); for (int n = 0; n < count; n++){int valItem = items[n].e.ordinal(); if (valItem == valX)// do stuff 1else if (valItem == valY)// do stuff 2}
會(huì )使性能得到一些改善,但這并不是最終的解決之道。
將與內部類(lèi)一同使用的變量聲明在包范圍內
請看下面的類(lèi)定義:
public class Foo {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);} private class Inner {void stuff() {Foo.this.doStuff(Foo.this.mValue);}}}
這其中的關(guān)鍵是,我們定義了一個(gè)內部類(lèi)(Foo$Inner),它需要訪(fǎng)問(wèn)外部類(lèi)的私有域變量和函數。這是合法的,并且會(huì )打印出我們希望的結果"Value is 27"。
問(wèn)題是在技術(shù)上來(lái)講(在幕后)Foo$Inner是一個(gè)完全獨立的類(lèi),它要直接訪(fǎng)問(wèn)Foo的私有成員是非法的。要跨越這個(gè)鴻溝,編譯器需要生成一組方法:
static int Foo.access$100(Foo foo) {return foo.mValue;}static void Foo.access$200(Foo foo, int value) {foo.doStuff(value);}
內部類(lèi)在每次訪(fǎng)問(wèn)"mValue"和"doStuff"方法時(shí),都會(huì )調用 這些靜態(tài)方法。就是說(shuō),上面的代碼說(shuō)明了一個(gè)問(wèn)題,你是在通過(guò)接口方法訪(fǎng)問(wèn)這些成員變量和函數而不是直接調用它們。在前面我們已經(jīng)說(shuō)過(guò),使用接口方法 (getter、setter)比直接訪(fǎng)問(wèn)速度要慢。所以這個(gè)例子就是在特定語(yǔ)法下面產(chǎn)生的一個(gè)“隱性的”性能障礙。
通過(guò)將內部類(lèi)訪(fǎng)問(wèn)的變量和函數聲明由私有范圍改為包范圍,我們可以避免這 個(gè)問(wèn)題。這樣做可以讓代碼運行更快,并且避免產(chǎn)生額外的靜態(tài)方法。(遺憾的是,這些域和方法可以被同一個(gè)包內的其他類(lèi)直接訪(fǎng)問(wèn),這與經(jīng)典的OO原則相違 背。因此當你設計公共API的時(shí)候應該謹慎使用這條優(yōu)化原則)
避免使用浮點(diǎn)數
在奔騰CPU出現之前,游戲設計者做得最多的就是整數運算。隨著(zhù)奔騰的到來(lái),浮點(diǎn)運算處理器成為了CPU內置的特性,浮點(diǎn)和整數配合使用,能夠讓你的游戲運行得更順暢。通常在桌面電腦上,你可以隨意的使用浮點(diǎn)運算。
但是非常遺憾,嵌入式處理器通常沒(méi)有支持浮點(diǎn)運算的硬件,所有對"float"和"double"的運算都是通過(guò)軟件實(shí)現的。一些基本的浮點(diǎn)運算,甚至需要毫秒級的時(shí)間才能完成。
甚至是整數,一些芯片有對乘法的硬件支持而缺少對除法的支持。這種情況下,整數的除法和取模運算也是有軟件來(lái)完成的。所以當你在使用哈希表或者做大量數學(xué)運算時(shí)一定要小心謹慎。