編寫高效的Android程式碼

鴨脖發表於2012-08-06
寫道
毫無疑問,基於Android平臺的裝置一定是嵌入式裝置。現代的手持裝置不僅僅是一部電話那麼簡單,它還是一個小型的手持電腦,但是,即使是最快的最高階的手持裝置也遠遠比不上一個中等效能的桌面機。

這就是為什麼在編寫Android程式時要時刻考慮執行的效率,這些系統不是想象中的那麼快,並且你還要考慮它電池的續航能力。這就意味著沒有多少剩餘空間給你去浪費了,因此,在你寫Android程式的時候,要儘可能的使你的程式碼優化而提高效率。

本頁介紹了幾種可以讓開發者的Android程式執行的更加有效率的方法。通過下面的一些跳轉的連線,你可以學會怎麼讓你的程式更加有效執行
 

內容

介紹

對於如何判斷一個系統的不合理,這裡有兩個基本的原則:

  • 不要做不必要做的事情。
  • 儘可能的節省記憶體的使用。

下面的所有方法都是基於這兩項的。

有人會認為本頁花了大量的篇幅去講如何進行“初步優化”( premature optimization)。雖然有時候微觀優化對開發高效的資料結構和演算法很困難,但是在嵌入式手持裝置上面你毫無選擇。例如,如果把桌面電腦的虛擬機器 移植到你的Android系統中,你會發現你寫的程式會耗盡你的記憶體。這就會導致程式執行起來極度緩慢,即使不考慮它對系統上其他的執行程式的影響。

這就是為什麼上面兩條原則這麼重要。Android的成功在於開發程式提供給使用者的體驗,然而使用者體驗的好壞又決定於你的程式碼是否能及時的響應而不 至於慢的讓人崩潰。因為我們所有的程式都會在同一個裝置上面執行,所以我們把它們作為一個整體來考慮。本文就像你考駕照需要學習的交通規則一樣:如果所有 人遵守,事情就會很流暢;但當你不遵守時,你就會撞車。

在我們討論實質問題之前,有一個簡要的說明:無論虛擬機器是否是Java編譯器的一個特點,下面介紹的所有觀點都是正確的。如果我們有兩種方法完成同 樣的事情,但是foo()的解釋執行要快於bar(),那麼foo()的編譯速度一定不會比bar()慢,僅僅靠編譯器使你的程式碼執行速度提升是不明智的 做法。

儘可能避免建立物件(Object)

物件的建立並不是沒有代價的。一個帶有執行緒分配池的generational的記憶體管理機制會使建立臨時物件的代價減少,不是分配記憶體總比不上不分配記憶體好。

如果你在一個使用者介面的迴圈中分配一個物件,你不得不強制的進行記憶體回收,那麼就會使使用者體驗出現稍微“打嗝”的現象。

因此,如果沒有必要你就不應該建立物件例項。下面是一件有幫助的例子:

  • 當從原始的輸入資料中提取字串時,試著從原始字串返回一個子字串,而不是建立一份拷貝。你將會建立一個新的字串物件,但是它和你的原始資料共享資料空間。
  • 如果你有一個返回字串地方法,你應該知道無論如何返回的結果是StringBuffer,改變你的函式的定義和執行,讓函式直接返回而不是通過建立一個臨時的物件。

一個比較激進的方法就是把一個多維陣列分割成幾個平行的一維陣列:

  • 一個Int型別的陣列要比一個Integer型別的陣列要好,但著同樣也可以歸納於這樣一個原則,兩個Int型別的陣列要比一個(int,int)物件陣列的效率要高的多 。對於其他原始資料型別,這個原則同樣適用。
  •  如果你需要建立一個包含一系列Foo和Bar物件的容器(container)時,記住:兩個平行的Foo[]和Bar[]要比一個(Foo,Bar)對 象陣列的效率高得多。(這個例子也有一個例外,當你設計其他程式碼的介面API時;在這種情況下,速度上的一點損失就不用考慮了。但是,在你的程式碼裡面,你 應該儘可能的編寫高效程式碼。)

一般來說,儘可能的避免建立短期的臨時物件。越少的物件建立意味著越少的垃圾回收,這會提高你程式的使用者體驗質量。

使用自身方法(Use Native Methods)

當處理字串的時候,不要猶豫,儘可能多的使用諸如String.indexOf()、String.lastIndexOf()這樣物件自身帶有的方法。因為這些方法使用C/C++來實現的,要比在一個java迴圈中做同樣的事情快10-100倍。

還有一點要補充說明的是,這些自身方法使用的代價要比那些解釋過的方法高很多,因而,對於細微的運算,儘量不用這類方法。

使用虛擬優於使用介面

假設你有一個HashMap物件,你可以宣告它是一個HashMap或則只是一個Map:

Java程式碼  收藏程式碼
  1. Map myMap1 = new HashMap();  
  2. HashMap myMap2 = new HashMap();   
 

哪一個更好呢?

一般來說明智的做法是使用Map,因為它能夠允許你改變Map介面執行上面的任何東西,但是這種“明智”的方法只是適用於常規的程式設計,對於嵌入式系統並不適合。通過介面引用來呼叫會花費2倍以上的時間,相對於通過具體的引用進行虛擬函式的呼叫。

如果你選擇使用一個HashMap,因為它更適合於你的程式設計,那麼使用Map會毫無價值。假定你有一個能重構你程式碼的整合編碼環境,那麼呼叫Map 沒有什麼用處,即使你不確定你的程式從哪開頭。(同樣,public的API是一個例外,一個好的API的價值往往大於執行效率上的那點損失)

使用靜態優於使用虛擬

如果你沒有必要去訪問物件的外部,那麼使你的方法成為靜態方法。它會被更快的呼叫,因為它不需要一個虛擬函式導向表。這同時也是一個很好的實踐,因為它告訴你如何區分方法的性質(signature),呼叫這個方法不會改變物件的狀態。

儘可能避免使用內在的Get、Set方法

像C++iyangde程式語言,通常會使用Get方法(例如 i = getCount() )去取代直接訪問這個屬性(i=mCount)。 這在C++程式設計裡面是一個很好的習慣,因為編譯器會把訪問方式設定為Inline,並且如果想約束或除錯屬性訪問,你只需要在任何時候新增一些程式碼。

在Android程式設計中,這不是一個很不好的主意。虛方法的呼叫會產生很多代價,比例項屬性查詢的代價還要多。我們應該在外部呼叫時使用Get和Set函式,但是在內部呼叫時,我們應該直接呼叫。

緩衝屬性呼叫Cache Field Lookups

訪問物件屬性要比訪問本地變數慢得多。你不應該這樣寫你的程式碼:

Java程式碼  收藏程式碼
  1. for (int i = 0; i < this.mCount; i++)  
  2.       dumpItem(this.mItems[i]);  
 

而是應該這樣寫:

Java程式碼  收藏程式碼
  1. int count = this.mCount;  
  2. Item[] items = this.mItems;  
  3.   
  4. for (int i = 0; i < count; i++)  
  5.     dumpItems(items[i]);  
 

(我們直接使用“this”表明這些是它的成員變數)

一個相似的原則就是:決不在一個For語句中第二次呼叫一個類的方法。例如,下面的程式碼就會一次又一次地執行getCount()方法,這是一個極大地浪費相比你把它直接隱藏到一個Int變數中。

Java程式碼  收藏程式碼
  1. for (int i = 0; i < this.getCount(); i++)  
  2.     dumpItems(this.getItem(i));  
 

這是一個比較好的辦法,當你不止一次的呼叫某個例項時,直接本地化這個例項,把這個例項中的某些值賦給一個本地變數。例如:

Java程式碼  收藏程式碼
  1.  protected void drawHorizontalScrollBar(Canvas canvas, int width, int height) {  
  2.         if (isHorizontalScrollBarEnabled()) {  
  3.             int size = mScrollBar.getSize(false  
  4.   
  5.   
  6. );  
  7.             if (size <= 0) {  
  8.                 size = mScrollBarSize;  
  9.             }  
  10.             mScrollBar.setBounds(0, height- size, width, height);  
  11.             mScrollBar.setParams(  
  12.                     computeHorizontalScrollRange(),  
  13.                     computeHorizontalScrollOffset(),  
  14.                     computeHorizontalScrollExtent(), false  
  15.   
  16.   
  17. );  
  18.             mScrollBar.draw(canvas);  
  19.         }  
  20.     }  
 

這裡有四次mScrollBar 的屬性呼叫,把mScrollBar 緩衝到一個堆疊變數之中,四次成員屬性的呼叫就會變成四次堆疊的訪問,這樣就會提高效率。

附帶說一下,對於方法同樣也可以像本地變數一樣具有相同的特點。

宣告Final常量

我們可以看看下面一個類頂部的宣告:

Java程式碼  收藏程式碼
  1. static int intVal = 42;  
  2. static String strVal = "Hello, world!";  
 

當一個類第一次使用時,編譯器會呼叫一個類初始化方法——<clinit> ,這個方法將42存入變數intVal ,並且為strVal 在類檔案字串常量表中提取一個引用,當這些值在後面引用時,就會直接屬性呼叫。

我們可以用關鍵字“final”來改進程式碼:

Java程式碼  收藏程式碼
  1. static final int intVal = 42;  
  2. static final String strVal = "Hello, world!";  
 

這個類將不會呼叫es a <clinit> 方法,因為這些常量直接寫入了類檔案靜態屬性初始化中,這個初始化直接由虛擬機器來處理。程式碼訪問intVal 將會使用Integer型別的42,訪問strVal 將使用相對節省的“字串常量”來替代一個屬性呼叫。

將一個類或者方法宣告為“final”並不會帶來任何的執行上的好處,它能夠進行一定的最優化處理。例如,如果編譯器知道一個Get方法不能被子類過載,那麼它就把該函式設定成Inline。

同時,你也可以把本地變數宣告為final變數。但是,這毫無意義。作為一個本地變數,使用final只能使程式碼更加清晰(或者你不得不用,在匿名訪問內聯類時)。

慎重使用增強型For迴圈語句

增強型For迴圈(也就是常說的“For-each迴圈”)經常用於Iterable介面的繼承收集介面上面。在這些物件裡面,一個 iterator被分配給物件去呼叫它的hasNext()和next()方法。在一個陣列列表裡面,你可以自己接的敷衍它,在其他的收集器裡面,增強型 的for迴圈將相當於iterator的使用。

儘管如此,下面的原始碼給出了一個可以接受的增強型for迴圈的例子:

Java程式碼  收藏程式碼
  1. public class Foo {  
  2.     int mSplat;  
  3.     static Foo mArray[] = new Foo[27];  
  4.   
  5.     public static void zero() {  
  6.         int sum = 0;  
  7.         for (int i = 0; i < mArray.length; i++) {  
  8.             sum += mArray[i].mSplat;  
  9.         }  
  10.     }  
  11.   
  12.     public static void one() {  
  13.         int sum = 0;  
  14.         Foo[] localArray = mArray;  
  15.         int len = localArray.length;  
  16.   
  17.         for (int i = 0; i < len; i++) {  
  18.             sum += localArray[i].mSplat;  
  19.         }  
  20.     }  
  21.   
  22.     public static void two() {  
  23.         int sum = 0;  
  24.         for (Foo a: mArray) {  
  25.             sum += a.mSplat;  
  26.         }  
  27.     }  
  28. }  
 

zero() 函式在每一次的迴圈中重新得到靜態屬性兩次,獲得陣列長度一次。

one() 函式把所有的東西都變為本地變數,避免類查詢屬性呼叫

two() 函式使用Java語言的1.5版本中的for迴圈語句,編輯者產生的原始碼考慮到了拷貝陣列的引用和陣列的長度到本地變數,是例遍陣列比較好的方法,它在 主迴圈中確實產生了一個額外的載入和儲存過程(顯然儲存了“a”),相比函式one()來說,它有一點位元上的減慢和4位元組的增長。

總結之後,我們可以得到:增強的for迴圈在陣列裡面表現很好,但是當和Iterable物件一起使用時要謹慎,因為這裡多了一個物件的建立。

避免列舉型別Avoid Enums

列舉型別非常好用,當考慮到尺寸和速度的時候,就會顯得代價很高,例如:

Java程式碼  收藏程式碼
  1. public class Foo {  
  2.    public enum Shrubbery { GROUND, CRAWLING, HANGING }  
  3. }  
 

這會轉變成為一個900位元組的class檔案(Foo$Shrubbery.class)。第一次使用時,類的初始化要在獨享上面呼叫方法去描述列舉的每一項,每一個物件都要有它自身的靜態空間,整個被儲存在一個陣列裡面(一個叫做“$VALUE”的靜態陣列)。那是一大堆的程式碼和資料,僅僅是為了三個整數值。

Java程式碼  收藏程式碼
  1. Shrubbery shrub = Shrubbery.GROUND;  
 

這會引起一個靜態屬性的呼叫,如果GROUND是一個靜態的Final變數,編譯器會把它當做一個常數巢狀在程式碼裡面。

還有一點要說的,通過列舉,你可以得到更好地API和一些編譯時間上的檢查。因此,一種比較平衡的做法就是:你應該盡一切方法在你的公用API中使用列舉型變數,當處理問題時就儘量的避免。

在一些環境下面,通過ordinal ()方法獲取一個列舉變數的整數值是很有用的,例如:把下面程式碼

Java程式碼  收藏程式碼
  1. for (int n = 0; n < list.size(); n++) {  
  2.     if (list.items[n].e == MyEnum.VAL_X)  
  3.        // do stuff 1  
  4.     else if (list.items[n].e == MyEnum.VAL_Y)  
  5.        // do stuff 2  
  6. }  
 

替換為:

Java程式碼  收藏程式碼
  1. int valX = MyEnum.VAL_X.ordinal();  
  2. int valY = MyEnum.VAL_Y.ordinal();  
  3. int count = list.size();  
  4. MyItem items = list.items();  
  5.   
  6. for (int  n = 0; n < count; n++)  
  7. {  
  8.      int  valItem = items[n].e.ordinal();  
  9.   
  10.      if (valItem == valX)  
  11.        // do stuff 1  
  12.      else if (valItem == valY)  
  13.        // do stuff 2  
  14. }  
 

在一些條件下,這會執行的更快,雖然沒有保障。

通過內聯類使用包空間

我們看下面的類宣告

Java程式碼  收藏程式碼
  1. public class Foo {  
  2.     private int mValue;  
  3.   
  4.     public void run() {  
  5.         Inner in = new Inner();  
  6.         mValue = 27;  
  7.         in.stuff();  
  8.     }  
  9.   
  10.     private void doStuff(int value) {  
  11.         System.out.println("Value is " + value);  
  12.     }  
  13.   
  14.     private class Inner {  
  15.         void stuff() {  
  16.             Foo.this.doStuff(Foo.this.mValue);  
  17.         }  
  18.     }  
  19. }  
 

這裡我們要注意的是我們定義了一個內聯類,它呼叫了外部類的私有方法和私有屬性。這是合法的呼叫,程式碼應該會顯示"Value is 27"。

問題是Foo$Inner在理論上(後臺執行上)是應該是一個完全獨立的類,它違規的呼叫了Foo的私有成員。為了彌補這個缺陷,編譯器產生了一對合成的方法:

Java程式碼  收藏程式碼
  1. /*package*/ static int Foo.access$100(Foo foo) {  
  2.     return foo.mValue;  
  3. }  
  4. /*package*/ static void Foo.access$200(Foo foo, int value) {  
  5.     foo.doStuff(value);  
  6. }  
 

當內聯類需要從外部訪問“mValue”和呼叫“doStuff”時,內聯類就會呼叫這些靜態的方法,這就意味著你不是直接訪問類成員,而是通過公共的方法來訪問的。前面我們談過間接訪問要比直接訪問慢,因此這是一個按語言習慣無形執行的例子。

讓擁有包空間的內聯類直接宣告需要訪問的屬性和方法,我們就可以避免這個問題,哲理詩是包空間而不是私有空間。這執行的更快並且去除了生成函式前面 東西。(不幸的是,它同時也意味著該屬性也能夠被相同包下面的其他的類直接訪問,這違反了標準的物件導向的使所有屬性私有的原則。同樣,如果是設計公共的 API你就要仔細的考慮這種優化的用法)

避免浮點型別的使用

在奔騰CPU釋出之前,遊戲作者儘可能的使用Integer型別的數學函式是很正常的。在奔騰處理器裡面,浮點數的處理變為它一個突出的特點,並且浮點數與整數的互動使用相比單獨使用整數來說,前者會使你的遊戲執行的更快,一般的在桌面電腦上面我們可以自由的使用浮點數。

不幸的是,嵌入式的處理器通常並不支援浮點數的處理,陰齒所有的“float”和“double”操作都是通過軟體進行的,一些基本的浮點數的操作就需要花費毫秒級的時間。

同事,即使是整數,一些晶片也只有乘法而沒有除法。在這些情況下,整數的除法和取模操作都是通過軟體實現。當你建立一個Hash表或者進行大量的數學運算時,這都是你要考慮的。

一些標準操作的時間比較

為了距離說明我們的觀點,下面有一張表,包括一些基本操作所使用的大概時間。注意這些時間並不是絕對的時間,絕對時間要考慮到CPU和時脈頻率。系 統不同,時間的大小也會有所差別。當然,這也是一種有意義的比較方法,我們可以比叫不同操作花費的相對時間。例如,新增一個成員變數的時間是新增一個本地 變數的四倍。

Action Time
Add a local variable 1
Add a member variable 4
Call String.length() 5
Call empty static native method 5
Call empty static method 12
Call empty virtual method 12.5
Call empty interface method 15
Call Iterator:next() on a HashMap 165
Call put() on a HashMap 600
Inflate 1 View from XML 22,000
Inflate 1 LinearLayout containing 1 TextView 25,000
Inflate 1 LinearLayout containing 6 View objects 100,000
Inflate 1 LinearLayout containing 6 TextView objects 135,000
Launch an empty activity 3,000,000

結束語

寫高效的嵌入式程式的最好方法就是要搞清楚你寫的程式究竟做了些什麼。如果你真的想分配一個iterator類,進一切方法的在一個List中使用增強型的for迴圈,使它成為一個有意而為之的做法,而不是一個無意的疏漏而產生負面影響。

有備無患,搞清楚你在做什麼!你可以假如你自己的一些行為準則,但是一定要注意你的程式碼正在做什麼,然後開始尋找方法去優化它。


原文轉載於:http://www.chinaup.org/docs/toolbox/performance.html

相關文章