天天看點

[轉]編寫高效的Android代碼

雖然如此說,但似乎并沒有什麼好的辦法:Android裝置是嵌入式裝置。現代的手持裝置,與其說是電話,更像一台拿在手中的電腦。但是,即使是“最快”的手持裝置,其性能也趕不上一台普通的台式電腦。

這就是為什麼我們在書寫Android應用程式的時候要格外關注效率。這些裝置并沒有那麼快,并且受電池電量的制約。這意味着,裝置沒有更多的能力,我們必須把程式寫的盡量有效。

本章讨論了很多能讓開發者使他們的程式運作更有效的方法,遵照這些方法,你可以使你的程式發揮最大的效力。

簡介

對于占用資源的系統,有兩條基本原則:

  • 不要做不必要的事
  • 不要配置設定不必要的記憶體

所有下面的内容都遵照這兩個原則。

有些人可能馬上會跳出來,把本節的大部分内容歸于“草率的優化”(xing:參見[ The Root of All Evil ]), 不可否認微優化(micro-optimization。xing:代碼優化,相對于結構優化)的确會帶來很多問題,諸如無法使用更有效的資料結構和算 法。但是在手持裝置上,你别無選擇。假如你認為Android虛拟機的性能與桌上型電腦相當,你的程式很有可能一開始就占用了系統的全部記憶體(xing:記憶體 很小),這會讓你的程式慢得像蝸牛一樣,更遑論做其他的操作了。

Android的成功依賴于你的程式提供的使用者體驗。而這種使用者體驗,部分依賴于你的程式是響應快速 而靈活的,還是響應緩慢而僵化的。因為所有的程式都運作在同一個裝置之上,都在一起,這就如果在同一條路上行駛的汽車。而這篇文檔就相當于你在取得駕照之 前必須要學習的交通規則。如果大家都按照這些規則去做,駕駛就會很順暢,但是如果你不這樣做,你可能會車毀人亡。這就是為什麼這些原則十分重要。

當我們開門見山、直擊主題之前,還必須要提醒大家一點:不管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的第二個條件中調用任何方法。如下面方法所示,在每次循環的時候都會調用getCount()方法,這樣做比你在一個int先把結果儲存起來開銷大很多。

for (int i = 0; i < this.getCount(); i++)

   dumpItems(this.getItem(i));


      

同樣如果你要多次通路一個變量,也最好先為它建立一個本地變量,例如:

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!";


      

必以其會生成一個叫做<clinit>的初始化類的方法,當類第一次被使用的時候這個方 法會被執行。方法會将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"的運算都是通過軟體實作的。一些基本的浮點運算,甚至需要毫秒級的時間才能完成。

甚至是整數,一些晶片有對乘法的硬體支援而缺少對除法的支援。這種情況下,整數的除法和取模運算也是有軟體來完成的。是以當你在使用哈希表或者做大量數學運算時一定要小心謹慎。