1. 程式人生 > >[Android 效能優化系列]那些不能忽略的小細節

[Android 效能優化系列]那些不能忽略的小細節

轉載請標明出處(http://blog.csdn.net/kifile),再次感謝


在接下來的一段時間裡,我會每天翻譯一部分關於效能提升的Android官方文件給大家

寫在開頭的話:

在下文中,會有一個經常出現的術語,叫做 JIT,他的全寫是 Just In Time compiler,指的是Java 裡的即時編譯,他能夠極大的優化你的程式碼執行速度。

下面是本次的正文:

################

本文將主要介紹一些能夠提升整個應用效能的細節優化,但是他們並不會引起太過顯著的效能提升。選擇正確的演算法和資料結構才應該是你的首選,但這就不在本文的討論範圍之類了。你應該將本文的這些小竅門作為一種程式設計習慣,這樣你的程式設計會更加有效。

這裡有兩條最基本的規則

1.不要做你不需要做的工作

2.當你不需要的時候就把記憶體釋放掉

你能碰到的最棘手的問題之一可能是當在不同硬體環境下進行細節優化。不同的虛擬機器版本他們的處理器速度不盡相同,你不能簡單地認為 X 裝置比 Y 裝置快或慢F倍。然後就把一臺裝置上的結果按照這個比例搬到另一個裝置上。尤其是,在虛擬機器上執行的效率和真機上是完全不一樣的。一臺有著JIT的裝置和沒有的裝置也是非常不一樣的。對於JIT而言是很好的程式碼,並不意味著對沒有JIT的裝置也是這樣。

請確保你的應用在不同裝置,不同SDK版本之間執行良好之後再來優化他的效率。

不要建立不需要的物件

建立物件永遠不會是免費的,垃圾收集器雖然在每個執行緒中都有一個針對臨時變數的分配池,這會使得分配臨時變數的消耗變得更低,但是分配記憶體的消耗總是要比不分配記憶體高.

如果你在你的應用中分配更多的物件,你會讓垃圾回收器的工作週期變短,導致使用者使用時卡頓。同步垃圾回收器在Android2.3的幫助文件中有介紹,但是我們應該儘可能避免不必要的工作

因此,你不應該建立你不需要的物件,這裡有一些例子:

1.如果你有一個方法需要返回String,並且你知道他的結果是通過StringBuffer進行疊加的。那麼請改變你的引數和實現方式,直接新增字元上去,而不要建立一個短命的臨時變數

2.當你準備從一組輸入資料中獲取一個string時,不要建立一個備份。你建立一個新的String物件,但是他會跟原有資料共享char[]物件。(如果你這麼做了,即使你只是用了原始輸入的一小部分,但是系統將保留所有這個物件的記憶體資料)

一個更加有用的想法是將多維陣列改成多個一維陣列

1.ints的陣列會比Integer物件的陣列要好,兩個ints型別的陣列相比於一個(int,int)型別的陣列效率更高,這對所有基本型別都是通用的。

2.如果你需要實現一個容器來儲存(Foo,Bar)這類元組,那麼記住建立兩個類似的Foo[],Bar[]陣列比單獨建立一個(Foo,Bar)物件的陣列要 更好。(當然,當你設計一個API供人訪問的時候,你最好為了實現一個優雅的API介面而對速度進行妥協,但是在內部呼叫的時候,你還是應該儘量讓他變得更有效)、

綜上所述,儘量不要建立臨時變數,更少的建立可能減小垃圾回收的頻率,提高你的使用者體驗

儘量使用靜態方法

如果你不需要訪問一個物件的具體欄位,那麼讓這個方法稱為靜態方法,將會提升15%-20%的效率,因為你你告訴了這個方法他需要的引數,並且不會改變這個物件本身

為常量使用Static Final

看一下的定義

static int intVal = 42;
static String strVal = "Hello, world!";
編譯器會生成一個初始化方法,叫做clinit,這個方法會在類第一次被用到的時候執行。這個方法會將intVal賦值為42,並且為strVal何一個字串建立對映。當你之後使用這些變數的時候,它們可以通過欄位查詢被訪問

我們可以通過使用final關鍵詞提升速度

static final int intVal = 42;
static final String strVal = "Hello, world!";

現在類不再需要一個clinit方法,因為所有的常量都在dex檔案中被靜態初始化了。intVal直接同42繫結在一起,strVal也同一個字元繫結在一起,你不再需要通過欄位查詢來訪問他們。

注意:這個優化只針對基本型別和String型別。對於其他的變數型別,並沒有直接進行關聯。但是,如果可能的話還是儘量使用static final來宣告常量

拒絕在內部使用Getter/Setter

在類似於C++的本地語言中,我們通常使用getter,而不是直接訪問欄位、對於C++,這是一個很好的習慣,以至於在面向物件的語言,類似於C#,Java中也被這樣使用了,因為編譯器可以使用內聯訪問,如果你需要限制,或者除錯欄位,你可以在任何時間加上這些程式碼。

但是,這在Android 上是一個壞喜歡。呼叫方法的代價很大,還不如去尋找對應欄位。這也是為什麼面向物件的語言會讓Getter和Setter方法作為一個公開的介面,但是在類裡你可以訪問他的原因。

如果沒有 JIT,直接訪問一個欄位要比通過getter訪問快三倍。但是如果有了JIT(他可以直接訪問欄位,消耗更小),直接訪問欄位將比通過getter呼叫快7倍

注意,如果你使用了ProGuard,那就隨便你了,因為Proguard能夠幫你實現內聯

使用增強的For迴圈語法

增加的For迴圈(通常被稱作遍歷迴圈for-each)可以被用到實現了Iterable介面的集合以及陣列的迴圈中。在集合裡,他可以通過訪問hasNext()和next()方法來實現遍歷。但是在ArrayList方法中,一個老式的迴圈會快3倍。而對於其他的集合,增強後的迴圈語法效率會相同,並且目標顯得更加明確。

這裡是使用陣列的迴圈的替代方式:

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()的速度也基本相同。它使用了從java1.5開始支援的增強的迴圈語法。

因此你應該預設應用增強語法,但是在使用ArrayList的時候選擇使用手寫的

詳情可以參看Josh Bloch's Effective Java, item 46.

針對私有內部類使用包訪問許可權而不是私有訪問許可權

看看下面的這個類

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),他可以直接訪問外不能的私有方法和私有欄位。他可以正對的呼叫stuff方法,輸出Value is 27.

那麼問題來了,虛擬機器認為從Foo$Inner中直接訪問Foo的私有成員是非法的,因為Foo和Foo$Inner是不同的類,即使在Java語言中允許內部類訪問外部類的私有成員。為了解決這個問題,編譯器生成了一些額外的方法:

/*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訪問和直接訪問的效率問題,因此,這裡就是一個由於語法問題導致的不可見的效能優化點

如果你在一個性能消耗大戶上使用上面的程式碼,你可以通過給相關欄位和方法包訪問許可權來優化他。不幸的是,那樣的話,擁有同樣包名的類也可以直接訪問他,所以你不應該在一個公共的Api中這麼做

拒絕使用浮點型

經驗之談,浮點型在Android裝置上的速度會比整型慢兩倍。

在速度方面,浮點型和雙精度型在現在的裝置上沒有什麼區別。當然在空間上,雙精度型是浮點型的兩倍。在電腦裝置上,這或許不是什麼問題,你會更加喜歡雙精度型。

同樣的,相對於整型,一些處理器能夠使用硬體來進行他的乘法。但是通常整型的除法和取模操作是通過軟體實現的,因此當你準備設計雜湊表或者做數學運算的時候就要額外注意了。

瞭解和使用依賴庫

一般而言,使用三方庫而不自己實現的原因是三方庫的執行效率比自己實現的好。就比如說 String 的 indexOf()方法,及相關的一些 API,在這裡 Dalvik 使用內聯的方式做了替換。同樣的,Systtem 的 arrayCopy()方法大概比 NexusOne上使用 JIT 之後的程式碼效率提升了九倍。

小心的使用本地方法

通過Android NDK來為你程式碼開發原生代碼有時候並不代表著比使用Java更有效。例如,在Java本地化呼叫的時候需要消耗,並且JIT並不能優化這些。如果你企圖分配本地資源(記憶體在本地堆上)納悶你很難管理這些資源開銷,你還必須為你希望執行的各個平臺分別編譯程式碼。你甚至可能要為同一種平臺編譯不同版本,例如同樣是ARM架構的G1和Nexus One,兩者間的本地化程式碼不能通用。

本地化程式碼在你有一個已經存在的本地化庫,並且希望將它移植到Android上的時候是有用的,但是不要因為提升速度二區將你的Java程式碼移植到本地區

如果你的確需要使用原生代碼,你應該讀一下這個JNI Tips

效能神話

在一個沒有JIT的裝置上,通過一個實際型別的變數來訪問方法要比通過一個介面型別來訪問方法要有效。(舉個例子,通過HashMap map來訪問方法,比用Map map來訪問效率更高)。雖然它們之間的效率差別不至於慢兩倍,其實大概也有6%左右的差別。而 JIT 可以使他們的效率差別幾乎無法覺察。

一個沒有JIT的裝置,他會快取之前訪問過的欄位,使得下次訪問的時候能夠提升20%的效率。但是如果有了JIT,那麼訪問全域性欄位和訪問區域性變數的花費就一樣了,因此,除非你覺得這樣能夠使得你的程式碼更容易閱讀,否則這不是一個值得優化的點。(這對那些靜態或者常量的欄位同樣有效)

不要忘記檢視你的效能

當你開始優化之前,請確保你知道你需要處理哪些問題。最好先檢測一下你目前的效能情況,否則你無法和優化後的效果進行對比。

而設計的標準是來源於 Caliper 的Java標準框架。微控制檯很難說名一個正確的方案,於是 Caliper 就用它的方式幫你解決所有困難的問題,甚至於你不要要檢測那裡你認為需要檢測的東西,因為虛擬機器已經優化過了你的所有程式碼。我們強烈推薦你使用 Caliper 來作為你自己的控制檯

你也許在尋找 Traceview 的相關資訊,但是在執行他的時候,會關閉掉 JIT 功能,所以你檢測到的東西和開啟 JIT 功能之後可能會大不相同。當然根據 Traceview 的資料 來進行優化也能夠使你在不使用 Traceview 的時候的效能提升

更過關於檢測和除錯你應用的資訊,請參看下面的文章: