摘自:http://www.cnblogs.com/mudoot/archive/2011/11/16/Writing_Efficient_Android_Code.html ,以便後記.
對於佔用資源的系統,有兩條基本原則:
不要做不必要的事
不要分配不必要的記憶體
1.避免建立對象
除非必要,應盡量避免儘力對象的執行個體。
當你從使用者輸入的資料中截取一段字串時,盡量使用substring函數取得未經處理資料的一個子串,而不是為子串另外建立一份拷貝。這樣你就有一個新的String對象,它與未經處理資料共用一個char數組。
如果你有一個函數返回一個String對象,而你確切的知道這個字串會被附加到一個StringBuffer,那麼,請改變這個函數的參數和實現方式,直接把結果附加到StringBuffer中,而不要再建立一個短命的臨時對象。
一個更極端的例子是,把多維陣列分成多個一維數組:
int數組比Integer數組好,這也概括了一個基本事實,兩個平行的int數組比(int,int)對象數組效能要好很多。同理,這試用於所有基本類型的組合。
如果你想用一種容器儲存(Foo,Bar)元組,嘗試使用兩個單獨的Foo[]數組和Bar[]數組,一定比(Foo,Bar)數組效率更高。(也有例外的情況,就是當你建立一個API,讓別人調用它的時候。這時候你要注重對API借口的設計而犧牲一點兒速度。當然在API的內部,你仍要儘可能的提高代碼的效率)
2.使用本地方法
當你在處理字串的時候,不要吝惜使用String.indexOf(), String.lastIndexOf()等特殊實現的方法。這些方法都是使用C/C++實現的,比起Java迴圈快10到100倍。
但也並非要完全使用本地方法,調用本地方法的代價要高於調用解釋方法。所以如果可以避免,就不應使用本地方法去做一些並不複雜的運算。
3.選擇虛類而不是介面
假設你有一個HashMap對象,你可以將它聲明為HashMap或者Map:
Map myMap1 = new HashMap(); HashMap myMap2 = new HashMap();
哪個更好呢?
按照傳統的觀點Map會更好些,因為這樣你可以改變他的具體實作類別,只要這個類繼承自Map介面。傳統的觀點對於傳統的程式是正確的,但是它並不適合嵌入式系統。調用一個介面的引用會比調用實體類的引用多花費一倍的時間。
如果HashMap完全適合你的程式,那麼使用Map就沒有什麼價值。如果有些地方你不能確定,先避免使用Map,剩下的交給IDE提供的重構功能好了。(當然公用API是一個例外:一個好的API常常會犧牲一些效能)
4.用靜態方法比虛方法好
如果你不需要訪問一個對象的成員變數,那麼請把方法聲明成static。虛方法執行的更快,因為它可以被直接調用而不需要一個虛函數表。
另外你也可以通過聲明體現出這個函數的調用不會改變對象的狀態。
5.不用getter和setter
在很多本地語言如C++中,都會使用getter(比如:i = getCount())來避免直接存取成員變數(i = mCount)。在C++中這是一個非常好的習慣,因為編譯器能夠內聯訪問,如果你需要約束或調試變數,你可以在任何時候添加代碼。
在Android上,這就不是個好主意了。虛方法的開銷比直接存取成員變數大得多。在通用的介面定義中,可以依照OO的方式定義getters和setters,但是在一般的類中,你應該直接存取變數。
6.將成員變數緩衝到本地
訪問成員變數比訪問本地變數慢得多,下面一段代碼:
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次效率更高的棧變數訪問。
順便說明一下,就是方法的參數與本地變數的效能是相同的。
7.使用常量
讓我們來看看這兩段在類前面的聲明:
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"只能使本地變數看起來更清晰些(但是也有些時候這是必須的,比如在使用匿名內部類的時候)。
8.謹慎使用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()中,每次迴圈都會訪問兩次靜態成員變數,取得一次數組的長度。
在one()中,將所有成員變數儲存到本地變數。
在two()中,使用了在java1.5中引入的foreach文法。編譯器會將對數組的引用和數組的長度儲存到本地變數中,這對訪問數組元素非常好。但是編譯器還會在每次迴圈中產生一個額外的對本地變數的儲存操作(對變數a的存取)這樣會比one()多出4個位元組,速度要稍微慢一些。
綜上所述,當foreach在訪問array時效能很好,但是運用於其他集合對象時要小心,因為它會產生額外的對象。
9.避免使用枚舉
枚舉變數非常方便,但不幸的是它會犧牲執行的速度並會大幅增加檔案體積。例如:
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
}
會使效能得到一些改善,但這並不是最終的解決之道。
10.將內部類聲明在包範圍內
考慮如下的類定義:
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的私人成員是非法的。要跨越這個鴻溝,編譯器需要產生一組方法:
/*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"方法時,都會調用這些靜態方法。就是說,上面的代碼說明了一個問題,你是在通過介面方法訪問這些成員變數和函數而不是直接調用它們。在前面我們已經說過,使用介面方法(getter、setter)比直接存取速度要慢。所以這個例子就是在特定文法下面產生的一個“隱性的”效能障礙。
通過將內部類訪問的變數和函式宣告由私人範圍改為包範圍,我們可以避免這個問題。這樣做可以讓代碼運行更快,並且避免產生額外的靜態方法。(遺憾的是,這些域和方法可以被同一個包內的其他類直接存取,這與經典的OO原則相違背。因此當你設計公用API的時候應該謹慎使用這條最佳化原則)
11.避免使用枚舉
枚舉變數非常方便,但不幸的是它會犧牲執行的速度並會大幅增加檔案體積。例如:
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
}
會使效能得到一些改善,但這並不是最終的解決之道。
12.將內部類聲明在包範圍內
考慮如下的類定義:
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的私人成員是非法的。要跨越這個鴻溝,編譯器需要產生一組方法:
/*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"方法時,都會調用這些靜態方法。就是說,上面的代碼說明了一個問題,你是在通過介面方法訪問這些成員變數和函數而不是直接調用它們。在前面我們已經說過,使用介面方法(getter、setter)比直接存取速度要慢。所以這個例子就是在特定文法下面產生的一個“隱性的”效能障礙。
通過將內部類訪問的變數和函式宣告由私人範圍改為包範圍,我們可以避免這個問題。這樣做可以讓代碼運行更快,並且避免產生額外的靜態方法。(遺憾的是,這些域和方法可以被同一個包內的其他類直接存取,這與經典的OO原則相違背。因此當你設計公用API的時候應該謹慎使用這條最佳化原則)
13.避免使用浮點數
在奔騰CPU出現之前,遊戲設計者做得最多的就是整數運算。隨著奔騰的到來,浮點運算處理器成為了CPU內建的特性,浮點和整數配合使用,能夠讓你的遊戲運行得更順暢。通常在案頭電腦上,你可以隨意的使用浮點運算。
但是非常遺憾,嵌入式處理器通常沒有支援浮點運算的硬體,所有對"float"和"double"的運算都是通過軟體實現的。一些基本的浮點運算,甚至需要毫秒級的時間才能完成。
甚至是整數,一些晶片有對乘法的硬體支援而缺少對除法的支援。這種情況下,整數的除法和模數運算也是有軟體來完成的。所以當你在使用雜湊表或者做大量數學運算時一定要小心謹慎。
結束語
為嵌入式系統編寫正確高效的代碼的最佳的方法就是去理解你的代碼究竟要做什麼。如果你的確想要分配一個迭代器或者無論如何都要在Lists上面使用增強迴圈文法,那麼一定是深思熟慮後的選擇,而不是一個不小心心的副作用。凡事預則立,不預則廢。一定要知道你在做什麼。按照你自己的風格去編寫代碼,但一定要仔細考慮代碼所作的事,並找到提升速度的方法。