1. 程式人生 > >Android程式碼記憶體優化建議-Java官方篇

Android程式碼記憶體優化建議-Java官方篇

這篇文章主要是介紹了一些小細節的優化技巧,當這些小技巧綜合使用起來的時候,對於整個App的效能提升還是有作用的,只是不能較大幅度的提升效能而已。選擇合適的演算法與資料結構才應該是你首要考慮的因素,在這篇文章中不會涉及這方面。你應該使用這篇文章中的小技巧作為平時寫程式碼的習慣,這樣能夠提升程式碼的效率。

+

本文的原文為Google官方Training的效能優化部分,這一章節主要講解的是高效能Android程式碼優化建議,建議所有Android應用開發者都仔細閱讀這份文件,並將所提到的編碼思想運用到實際的Android開發中。

正文

+

通常來說,高效的程式碼需要滿足下面兩個規則: +
不要做冗餘的動作如果能避免,儘量不要分配記憶體

+

你會面臨最棘手的一個問題是當你優化一個肯定會在多種型別的硬體上執行的應用程式。不同版本的VM在不同的處理器上執行速度不同。它甚至不是你可以簡單地說“裝置X因為F原因比裝置Y快/慢”那麼簡單,而且也不能簡單地從一個裝置拓展到另一個裝置。特別提醒的是模擬器在效能方面和其他的裝置沒有可比性。通常有JIT優化和沒有JIT優化的裝置之間存在巨大差異:經過JIT程式碼優化的裝置並不一定比沒有經過JIT程式碼優化的裝置好。

+

程式碼的執行效果會受到裝置CPU,裝置記憶體,系統版本等諸多因素的影響。為了確保程式碼能夠在不同裝置上都執行良好,需要最大化程式碼的效率。

1)避免建立不必要的物件

+

雖然GC可以回收不用的物件,可是為這些物件分配記憶體,並回收它們同樣是需要耗費資源的。
因此請儘量避免建立不必要的物件,有下面一些例子來說明這個問題: + 如果你需要返回一個String物件,並且你知道它最終會需要連線到一個StringBuffer,請修改你的實現方式,避免直接進行連線操作,應該採用建立一個臨時物件來做這個操作.當從輸入的資料集中抽取出Strings的時候,嘗試返回原資料的substring物件,而不是建立一個重複的物件。

+

一個稍微激進點的做法是把所有多維的資料分解成1維的陣列: + 一組int資料要比一組Integer物件要好很多。可以得知,兩組1維陣列要比一個2維陣列更加的有效率。同樣的,這個道理可以推廣至其他原始資料型別。如果你需要實現一個數組用來存放(Foo,Bar)的物件,嘗試分解為Foo[]與Bar[]要比(Foo,Bar)好很多。(當然,為了某些好的API的設計,可以適當做一些妥協。但是在自己的程式碼內部,你應該多多使用分解後的容易。

+

通常來說,需要避免建立更多的物件。更少的物件意味者更少的GC動作,GC會對使用者體驗有比較直接的影響。

2)選擇Static而不是Virtual

+

如果你不需要訪問一個物件的值域,請保證這個方法是static型別的,這樣方法呼叫將快15%-20%。這是一個好的習慣,因為你可以從方法宣告中得知呼叫無法改變這個物件的狀態。

3)常量宣告為Static Final

+

先看下面這種宣告的方式
+
+
1
2
+
static int intVal = 42;
static String strVal = "Hello, world!";

+

+

編譯器會在類首次被使用到的時候,使用初始化<clinit>方法來初始化上面的值,之後訪問的時候會需要先到它那裡查詢,然後才返回資料。我們可以使用static final來提升效能:
+
+
1
2
+
static final int intVal = 42;
static final String strVal = "Hello, world!";

+

+

這時再也不需要上面的那個方法來做多餘的查詢動作了。
所以,請儘可能的為常量宣告為static final型別的。

4)避免內部的Getters/Setters

+

像C++等native language,通常使用getters(i = getCount())而不是直接訪問變數(i = mCount).這是編寫C++的一種優秀習慣,而且通常也被其他面向物件的語言所採用,例如C#與Java,因為編譯器通常會做inline訪問,而且你需要限制或者除錯變數,你可以在任何時候在getter/setter裡面新增程式碼。
然而,在Android上,這是一個糟糕的寫法。Virtual method的呼叫比起直接訪問變數要耗費更多。那麼合理的做法是:在面向物件的設計當中應該使用getter/setter,但是在類的內部你應該直接訪問變數。
沒有JIT(Just In Time Compiler)時,直接訪問變數的速度是呼叫getter的3倍。有JIT時,直接訪問變數的速度是通過getter訪問的7倍。
請注意,如果你使用ProGuard, 你可以獲得同樣的效果,因為ProGuard可以為你inline accessors.

5)使用增強的For迴圈寫法

+

請比較下面三種迴圈的方法: +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
+
static class Foo {
int mSplat;
}

Foo[] mArray = ...

public void zero() {
int sum = 0;
for (int i = 0; i < mArray.length; ++i) {
sum += mArray[i].mSplat;
}
}

public void one() {
int sum = 0;
Foo[] localArray = mArray;
int len = localArray.length;

for (int i = 0; i < len; ++i) {
sum += localArray[i].mSplat;
}
}

public void two() {
int sum = 0;
for (Foo a : mArray) {
sum += a.mSplat;
}
}
+ zero()是最慢的,因為JIT沒有辦法對它進行優化。one()稍微快些。two() 在沒有做JIT時是最快的,可是如果經過JIT之後,與方法one()是差不多一樣快的。它使用了增強的迴圈方法for-each。

+

所以請儘量使用for-each的方法,但是對於ArrayList,請使用方法one()。

6)使用包級訪問而不是內部類的私有訪問

+

參考下面一段程式碼 +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+
public class Foo {
private class Inner {
void stuff() {
Foo.this.doStuff(Foo.this.mValue);
}
}

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);
}
}

+

這裡重要的是,我們定義了一個私有的內部類(Foo$Inner),它直接訪問了外部類中的私有方法以及私有成員物件。這是合法的,這段程式碼也會如同預期一樣打印出”Value is 27”。

+

問題是,VM因為Foo和Foo$Inner是不同的類,會認為在Foo$Inner中直接訪問Foo類的私有成員是不合法的。即使Java語言允許內部類訪問外部類的私有成員。為了去除這種差異,編譯器會產生一些仿造函式: +
+
1
2
3
4
5
6
+
/*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()函式時,它都會呼叫這些靜態方法。這意味著,上面的程式碼可以歸結為,通過accessor函式來訪問成員變數。早些時候我們說過,通過accessor會比直接訪問域要慢。所以,這是一個特定語言用法造成效能降低的例子。

+

如果你正在效能熱區(hotspot:高頻率、重複執行的程式碼段)使用像這樣的程式碼,你可以把內部類需要訪問的域和方法宣告為包級訪問,而不是私有訪問許可權。不幸的是,這意味著在相同包中的其他類也可以直接訪問這些域,所以在公開的API中你不能這樣做。

7)避免使用float型別

+

Android系統中float型別的資料存取速度是int型別的一半,儘量優先採用int型別。

8)使用庫函式

+

儘量使用System.arraycopy()等一些封裝好的庫函式,它的效率是手動編寫copy實現的9倍多。

+

Tip: Also see Josh Bloch’s Effective Java, item 47.

9)謹慎使用native函式

+

當你需要把已經存在的native code遷移到Android,請謹慎使用JNI。如果你要使用JNI,請學習JNI Tips

10)關於效能的誤區

+

在沒有做JIT之前,使用一種確切的資料型別確實要比抽象的資料型別速度要更有效率。(例如,使用HashMap要比Map效率更高。) 有誤傳效率要高一倍,實際上只是6%左右。而且,在JIT之後,他們直接並沒有大多差異。

11)關於測量

+

上面文件中出現的資料是Android的實際執行效果。我們可以用Traceview 來測量,但是測量的資料是沒有經過JIT優化的,所以實際的效果應該是要比測量的資料稍微好些。

+

關於如何測量與除錯,還可以參考下面兩篇文章: + Profiling with Traceview and dmtracedumpAnalysing Display and Performance with Systrace