毫無疑問,基於android平台的設備一定是嵌入式設備。現代的手持設備不僅僅是一部電話那麼簡單,它還是一個小型的手持電腦,但是,即使是最快的最高端的手持設備也遠遠比不上一個中等性能的桌面機。
這就是為什麼在編寫Android程序時要時刻考慮執行的效率,這些系統不是想象中的那麼快,並且你還要考慮它電池的續航能力。這就意味著沒有多少剩余空間給你去浪費了,因此,在你寫android程序的時候,要盡可能的使你的代碼優化而提高效率。
本頁介紹了幾種可以讓開發者的android程序運行的更加有效率的方法。通過下面的一些跳轉的連接,你可以學會怎麼讓你的程序更加有效運行
介紹
對於如何判斷一個系統的不合理,這裡有兩個基本的原則:
·不要做不必要做的事情。
·盡可能的節省內存的使用。
下面的所有方法都是基於這兩項的。
有人會認為本頁花了大量的篇幅去講如何進行“初步優化”( premature optimization)。雖然有時候微觀優化對開發高效的數據結構和算法很困難,但是在嵌入式手持設備上面你毫無選擇。例如,如果把桌面電腦的虛擬機移植到你的android系統中,你會發現你寫的程序會耗盡你的內存。這就會導致程序運行起來極度緩慢,即使不考慮它對系統上其他的運行程序的影響。
這就是為什麼上面兩條原則這麼重要。android的成功在於開發程序提供給用戶的體驗,然而用戶體驗的好壞又決定於你的代碼是否能及時的響應而不至於慢的讓人崩潰。因為我們所有的程序都會在同一個設備上面運行,所以我們把它們作為一個整體來考慮。本文就像你考駕照需要學習的交通規則一樣:如果所有人遵守,事情就會很流暢;但當你不遵守時,你就會撞車。
在我們討論實質問題之前,有一個簡要的說明:無論虛擬機是否是Java編譯器的一個特點,下面介紹的所有觀點都是正確的。如果我們有兩種方法完成同樣的事情,但是foo()的解釋執行要快於bar(),那麼foo()的編譯速度一定不會比bar()慢,僅僅靠編譯器使你的代碼運行速度提升是不明智的做法。
盡可能避免創建對象(Object)
對象的創建並不是沒有代價的。一個帶有線程分配池的generational的內存管理機制會使創建臨時對象的代價減少,不是分配內存總比不上不分配內存好。
如果你在一個用戶界面的循環中分配一個對象,你不得不強制的進行內存回收,那麼就會使用戶體驗出現稍微“打嗝”的現象。
因此,如果沒有必要你就不應該創建對象實例。下面是一件有幫助的例子:
·當從原始的輸入數據中提取字符串時,試著從原始字符串返回一個子字符串,而不是創建一份拷貝。你將會創建一個新的字符串對象,但是它和你的原始數據共享數據空間。
·如果你有一個返回字符串地方法,你應該知道無論如何返回的結果是StringBuffer,改變你的函數的定義和執行,讓函數直接返回而不是通過創建一個臨時的對象。
一個比較激進的方法就是把一個多維數組分割成幾個平行的一維數組:
·一個Int類型的數組要比一個Integer類型的數組要好,但著同樣也可以歸納於這樣一個原則,兩個Int類型的數組要比一個(int,int)對象數組的效率要
高的多。對於其他原始數據類型,這個原則同樣適用。
·m 如果你需要創建一個包含一系列Foo和Bar對象的容器(container)時,記住:兩個平行的Foo[]和Bar[]要比一個(Foo,Bar)對象數組的效率高得多。(這個例子也有一個例外,當你設計其他代碼的接口API時;在這種情況下,速度上的一點損失就不用考慮了。但是,在你的代碼裡面,你應該盡可能的編寫高效代碼。)
一般來說,盡可能的避免創建短期的臨時對象。越少的對象創建意味著越少的垃圾回收,這會提高你程序的用戶體驗質量。
使用自身方法(Use Native Methods)
當處理字符串的時候,不要猶豫,盡可能多的使用諸如String.indexOf()、String.lastIndexOf()這樣對象自身帶有的方法。因為這些方法使用C/C++來實現的,要比在一個Java循環中做同樣的事情快10-100倍。
還有一點要補充說明的是,這些自身方法使用的代價要比那些解釋過的方法高很多,因而,對於細微的運算,盡量不用這類方法。
使用虛擬優於使用接口
假設你有一個HashMap對象,你可以聲明它是一個HashMap或則只是一個Map:
Map myMap1 = new HashMap();
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
訪問對象屬性要比訪問本地變量慢得多。你不應該這樣寫你的代碼:
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);
}
}
這裡有四次mScrollBar的屬性調用,把mScrollBar緩沖到一個堆棧變量之中,四次成員屬性的調用就會變成四次堆棧的訪問,這樣就會提高效率。
附帶說一下,對於方法同樣也可以像本地變量一樣具有相同的特點。
聲明Final常量
我們可以看看下面一個類頂部的聲明:
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!";
這個類將不會調用es a <clinit>方法,因為這些常量直接寫入了類文件靜態屬性初始化中,這個初始化直接由虛擬機來處理。代碼訪問intVal將會使用Integer類型的42,訪問strVal將使用相對節省的“字符串常量”來替代一個屬性調用。
將一個類或者方法聲明為“final”並不會帶來任何的執行上的好處,它能夠進行一定的最優化處理。例如,如果編譯器知道一個Get方法不能被子類重載,那麼它就把該函數設置成Inline。
同時,你也可以把本地變量聲明為final變量。但是,這毫無意義。作為一個本地變量,使用final只能使代碼更加清晰(或者你不得不用,在匿名訪問內聯類時)。
慎重使用增強型For循環語句
增強型For循環(也就是常說的“For-each循環”)經常用於Iterable接口的繼承收集接口上面。在這些對象裡面,一個iterator被分配給對象去調用它的hasNext()和next()方法。在一個數組列表裡面,你可以自己接的敷衍它,在其他的收集器裡面,增強型的for循環將相當於iterator的使用。
盡管如此,下面的源代碼給出了一個可以接受的增強型for循環的例子:
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() 函數在每一次的循環中重新得到靜態屬性兩次,獲得數組長度一次。
one() 函數把所有的東西都變為本地變量,避免類查找屬性調用
two() 函數使用Java語言的1.5版本中的for循環語句,編輯者產生的源代碼考慮到了拷貝數組的引用和數組的長度到本地變量,是例遍數組比較好的方法,它在主循環中確實產生了一個額外的載入和儲存過程(顯然保存了“a”),相比函數one()來說,它有一點比特上的減慢和4字節的增長。
總結之後,我們可以得到:增強的for循環在數組裡面表現很好,但是當和Iterable對象一起使用時要謹慎,因為這裡多了一個對象的創建。
避免列舉類型Avoid Enums
列舉類型非常好用,當考慮到尺寸和速度的時候,就會顯得代價很高,例如:
public class Foo {
public enum Shrubbery { GROUND, CRAWLING, HANGING }
}
這會轉變成為一個900字節的class文件(Foo$Shrubbery.class)。第一次使用時,類的初始化要在獨享上面調用方法去描述列舉的每一項,每一個對象都要有它自身的靜態空間,整個被儲存在一個數組裡面(一個叫做“$VALUE”的靜態數組)。那是一大堆的代碼和數據,僅僅是為了三個整數值。
Shrubbery shrub = Shrubbery.GROUND;
這會引起一個靜態屬性的調用,如果GROUND是一個靜態的Final變量,編譯器會把它當做一個常數嵌套在代碼裡面。
還有一點要說的,通過列舉,你可以得到更好地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);
}
}
}
這裡我們要注意的是我們定義了一個內聯類,它調用了外部類的私有方法和私有屬性。這是合法的調用,代碼應該會顯示"Value is 27"。
問題是Foo$Inner在理論上(後台運行上)是應該是一個完全獨立的類,它違規的調用了Foo的私有成員。為了彌補這個缺陷,編譯器產生了一對合成的方法:
/*package*/ static int Foo.Access$100(Foo foo) {
return foo.mValue;
}
/*package*/ static void Foo.Access$200(Foo foo, int value) {
foo.DOStuff(value);
}
當內聯類需要從外部訪問“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循環,使它成為一個有意而為之的做法,而不是一個無意的疏漏而產生負面影響。
有備無患,搞清楚你在做什麼!你可以假如你自己的一些行為准則,但是一定要注意你的代碼正在做什麼,然後開始尋找方法去優化它。