現代的手持設備,與其說是電話,更像一臺拿在手中的電腦。可是,即便是「最快」的手持設備,其性能也趕不上一臺普通的臺式電腦。java
這就是爲何咱們在書寫Android應用程序的時候要格外關注效率。這些設備並無那麼快,而且受電池電量的制約。這意味着,設備沒有更多的能力,咱們必須把程序寫的儘可能有效。算法
本文討論了不少能讓開發者使他們的程序運行更有效的方法,遵守這些方法,你可使你的程序發揮最大的效力。canvas
簡介數組
對於佔用資源的系統,有兩條基本原則:緩存
不要作沒必要要的事
不要分配沒必要要的內存數據結構
全部下面的內容都遵守這兩個原則。數據結構和算法
有些人可能立刻會跳出來,把本節的大部份內容歸於「草率的優化」(xing:參見[The Root of All Evil]),不能否認微優化(micro-optimization。xing:代碼優化,相對於結構優化)的確會帶來不少問題,諸如沒法使用更有效的數據結構和算法。可是在手持設備上,你別無選擇。假如你認爲Android虛擬機的性能與臺式機至關,你的程序頗有可能一開始就佔用了系統的所有內存(xing:內存很小),這會讓你的程序慢得像蝸牛同樣,更遑論作其餘的操做了。函數
Android的成功依賴於你的程序提供的用戶體驗。而這種用戶體驗,部分依賴於你的程序是響應快速而靈活的,仍是響應緩慢而僵化的。由於全部的程序都運行在同一個設備之上,都在一塊兒,這就若是在同一條路上行駛的汽車。而這篇文檔就至關於你在取得駕照以前必需要學習的交通規則。若是你們都按照這些規則去作,駕駛就會很順暢,可是若是你不這樣作,你可能會車毀人亡。這就是爲何這些原則十分重要。oop
當咱們開門見山、直擊主題以前,還必需要提醒你們一點:無論VM是否支持實時(JIT)編譯器(xing:它容許實時地將Java解釋型程序自動編譯成本機機器語言,以使程序執行的速度更快。有些JVM包含JIT編譯器。),下面提到的這些原則都是成立的。假如咱們有目標徹底相同的兩個方法,在解釋執行時foo()比bar()快,那麼編譯以後,foo()依然會比bar()快。因此不要寄但願於編譯器能夠拯救你的程序。性能
避免創建對象
世界上沒有免費的對象。雖然GC爲每一個線程都創建了臨時對象池,可使建立對象的代價變得小一些,可是分配內存永遠都比不分配內存的代價大。
若是你在用戶界面循環中分配對象內存,就會引起週期性的垃圾回收,用戶就會以爲界面像打嗝同樣一頓一頓的。
因此,除非必要,應儘可能避免盡力對象的實例。下面的例子將幫助你理解這條原則:
當你從用戶輸入的數據中截取一段字符串時,儘可能使用substring函數取得原始數據的一個子串,而不是爲子串另外創建一份拷貝。這樣你就有一個新的String對象,它與原始數據共享一個char數組。
若是你有一個函數返回一個String對象,而你確切的知道這個字符串會被附加到一個StringBuffer,那麼,請改變這個函數的參數和實現方式,直接把結果附加到StringBuffer中,而不要再創建一個短命的臨時對象。
一個更極端的例子是,把多維數組分紅多個一維數組。
int數組比Integer數組好,這也歸納了一個基本事實,兩個平行的int數組比(int,int)對象數組性能要好不少。同理,這試用於全部基本類型的組合。
若是你想用一種容器存儲(Foo,Bar)元組,嘗試使用兩個單獨的Foo[]數組和Bar[]數組,必定比(Foo,Bar)數組效率更高。(也有例外的狀況,就是當你創建一個API,讓別人調用它的時候。這時候你要注重對API藉口的設計而犧牲一點兒速度。固然在API的內部,你仍要儘量的提升代碼的效率)
整體來講,就是避免建立短命的臨時對象。減小對象的建立就能減小垃圾收集,進而減小對用戶體驗的影響。
使用本地方法
當你在處理字串的時候,不要吝惜使用String.indexOf(), String.lastIndexOf()等特殊實現的方法(specialty methods)。這些方法都是使用C/C++實現的,比起Java循環快10到100倍。
使用實類比接口好
假設你有一個HashMap對象,你能夠將它聲明爲HashMap或者Map:
Map myMap1 = new HashMap(); HashMap myMap2 = new HashMap();
哪一個更好呢?
按照傳統的觀點Map會更好些,由於這樣你能夠改變他的具體實現類,只要這個類繼承自Map接口。傳統的觀點對於傳統的程序是正確的,可是它並不適合嵌入式系統。調用一個接口的引用會比調用實體類的引用多花費一倍的時間。
若是HashMap徹底適合你的程序,那麼使用Map就沒有什麼價值。若是有些地方你不能肯定,先避免使用Map,剩下的交給IDE提供的重構功能好了。(固然公共API是一個例外:一個好的API經常會犧牲一些性能)
用靜態方法比虛方法好
若是你不須要訪問一個對象的成員變量,那麼請把方法聲明成static。虛方法執行的更快,由於它能夠被直接調用而不須要一個虛函數表。另外你也能夠經過聲明體現出這個函數的調用不會改變對象的狀態。
不用getter和setter
在不少本地語言如C++中,都會使用getter(好比:i = getCount())來避免直接訪問成員變量(i = mCount)。在C++中這是一個很是好的習慣,由於編譯器可以內聯訪問,若是你須要約束或調試變量,你能夠在任什麼時候候添加代碼。
在Android上,這就不是個好主意了。虛方法的開銷比直接訪問成員變量大得多。在通用的接口定義中,能夠依照OO的方式定義getters和setters,可是在通常的類中,你應該直接訪問變量。
將成員變量緩存到本地
訪問成員變量比訪問本地變量慢得多,下面一段代碼:
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"是爲了代表這些是成員變量)
另外一個類似的原則是:永遠不要在for的第二個條件中調用任何方法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次訪問成員變量mScrollBar,若是將它緩存到本地,4次成員變量訪問就會變成4次效率更高的棧變量訪問。
另外就是方法的參數與本地變量的效率相同。
使用常量
讓咱們來看看這兩段在類前面的聲明:
static int intVal = 42; static String strVal = "Hello, world!";
必以其會生成一個叫作的初始化類的方法,當類第一次被使用的時候這個方法會被執行。方法會將42賦給intVal,而後把一個指向類中常量表的引用賦給strVal。當之後要用到這些值的時候,會在成員變量表中查找到他們。
下面咱們作些改進,使用「final"關鍵字:
static final int intVal = 42; static final String strVal = "Hello, world!";
如今,類再也不須要<clinit>方法,由於在成員變量初始化的時候,會將常量直接保存到類文件中。用到intVal的代碼被直接替換成42,而使用strVal的會指向一個字符串常量,而不是使用成員變量。
將一個方法或類聲明爲"final"不會帶來性能的提高,可是會幫助編譯器優化代碼。舉例說,若是編譯器知道一個"getter"方法不會被重載,那麼編譯器會對其採用內聯調用。
你也能夠將本地變量聲明爲"final",一樣,這也不會帶來性能的提高。使用"final"只能使本地變量看起來更清晰些(可是也有些時候這是必須的,好比在使用匿名內部類的時候)(xing:原文是 or you have to, e.g. for use in an anonymous inner class)
謹慎使用foreach
foreach能夠用在實現了Iterable接口的集合類型上。foreach會給這些對象分配一個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()中,每次循環都會訪問兩次靜態成員變量,取得一次數組的長度。
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語法。編譯器會將對數組的引用和數組的長度保存到本地變量中,這對訪問數組元素很是好。可是編譯器還會在每次循環中產生一個額外的對本地變量的存儲操做(對變量a的存取)這樣會比one()多出4個字節,速度要稍微慢一些。
綜上所述:foreach語法在運用於array時性能很好,可是運用於其餘集合對象時要當心,由於它會產生額外的對象。
避免使用枚舉
枚舉變量很是方便,但不幸的是它會犧牲執行的速度和並大幅增長文件體積。例如:
public class Foo {public enum Shrubbery { GROUND, CRAWLING, HANGING }}
會產生一個900字節的.class文件(Foo$Shubbery.class)。在它被首次調用時,這個類會調用初始化方法來準備每一個枚舉變量。每一個枚舉項都會被聲明成一個靜態變量,並被賦值。而後將這些靜態變量放在一個名爲"$VALUES"的靜態數組變量中。而這麼一大堆代碼,僅僅是爲了使用三個整數。
這樣:
Shrubbery shrub = Shrubbery.GROUND;會引發一個對靜態變量的引用,若是這個靜態變量是final int,那麼編譯器會直接內聯這個常數。
一方面說,使用枚舉變量可讓你的API更出色,並能提供編譯時的檢查。因此在一般的時候你毫無疑問應該爲公共API選擇枚舉變量。可是當性能方面有所限制的時候,你就應該避免這種作法了。
有些狀況下,使用ordinal()方法獲取枚舉變量的整數值會更好一些,舉例來講,將:
for (int n = 0; n < list.size(); n++) { if (list.items[n].e == MyEnum.VAL_X) { // do stuff 1 } else 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 1 } else if (valItem == valY) { // do stuff 2 } }
會使性能獲得一些改善,但這並非最終的解決之道。
將與內部類一同使用的變量聲明在包範圍內
請看下面的類定義:
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); } }
這其中的關鍵是,咱們定義了一個內部類(Foo$Inner),它須要訪問外部類的私有域變量和函數。這是合法的,而且會打印出咱們但願的結果"Value is 27"。
問題是在技術上來說(在幕後)Foo$Inner是一個徹底獨立的類,它要直接訪問Foo的私有成員是非法的。要跨越這個鴻溝,編譯器須要生成一組方法:
static int Foo.access$100(Foo foo) { return foo.mValue; } static void Foo.access$200(Foo foo, int value) { foo.doStuff(value); }
內部類在每次訪問"mValue"和"doStuff"方法時,都會調用這些靜態方法。就是說,上面的代碼說明了一個問題,你是在經過接口方法訪問這些成員變量和函數而不是直接調用它們。在前面咱們已經說過,使用接口方法(getter、setter)比直接訪問速度要慢。因此這個例子就是在特定語法下面產生的一個「隱性的」性能障礙。
經過將內部類訪問的變量和函數聲明由私有範圍改成包範圍,咱們能夠避免這個問題。這樣作可讓代碼運行更快,而且避免產生額外的靜態方法。(遺憾的是,這些域和方法能夠被同一個包內的其餘類直接訪問,這與經典的OO原則相違背。所以當你設計公共API的時候應該謹慎使用這條優化原則)
避免使用浮點數
在奔騰CPU出現以前,遊戲設計者作得最多的就是整數運算。隨着奔騰的到來,浮點運算處理器成爲了CPU內置的特性,浮點和整數配合使用,可以讓你的遊戲運行得更順暢。一般在桌面電腦上,你能夠隨意的使用浮點運算。
可是很是遺憾,嵌入式處理器一般沒有支持浮點運算的硬件,全部對"float"和"double"的運算都是經過軟件實現的。一些基本的浮點運算,甚至須要毫秒級的時間才能完成。
甚至是整數,一些芯片有對乘法的硬件支持而缺乏對除法的支持。這種狀況下,整數的除法和取模運算也是有軟件來完成的。因此當你在使用哈希表或者作大量數學運算時必定要當心謹慎。