Android高性能編碼最佳實踐

本文主要講一些代碼級別的細微優化,但別小看這些,當它們組合起來的時候就能提高App的整體性能。這類的優化不同於算法與數據結構優化所能達到的顯著效果,但我們應該把它作爲自己的編碼習慣從而寫出高效的代碼。

寫出高效代碼的兩個基本原則:

  • 不要做不必要的事情
  • 不要分配不必要的內存

優化一個App時最棘手的問題在於它可能運行於不同的硬件設備上,不同的虛擬機版本、不同的處理器從而導致不同的運行速度;設備有無JIT也將導致不同的性能。爲了保證在不同的設備上都有較好的性能,我們就需要從代碼層面進行優化,確保代碼可以高效地執行。

避免創建不必要的對象

對象創建是有開銷的,我們需要儘可能避免創建過多臨時性的對象,一旦爲App創建了過多的對象,那就意味着頻繁地垃圾回收。頻繁地垃圾回收會對用戶體驗造成不良影響,雖然Android 2.3之後,垃圾回收不再是“Stop-The-World”,可以併發執行了,但我們仍需要避免不必要的對象創建。以下示例可作爲參考:

  • 如果一個方法返回String結果並且該結果將會被附加到一個StringBuffer上,則可以修改方法的實現直接處理附加操作,從而避免創建一個臨時對象。
  • 如果從一個輸入中提取字符串,儘可能返回原始數據的一個子串而不是原始數據的拷貝。你將創建一個新的String對象,但可以與原始數據共享char[]。

一種比較激進的方法是將多維數組分割成多個平行的一維數組:

  • int數組比Integer數組更高效,也可以推廣到兩個平行的int數組比二維數組(int, int)更加高效,對於任何其他原始類型的組合也一樣。
  • 如果想實現類似(Foo,Bar)的元組對象,儘量使用兩個平行的數組:Foo[]與Bar[]。當然如果我們是在實現對外的API,則需要對此做一個折中,犧牲一點速度,從而實現一個好的API設計。但對於我們內部代碼來說,應該儘可能地高效。

總的來說,儘量避免不必要的對象創建,創建的對象越少,意味着垃圾回收的頻率也越低,這將直接影響到用戶的體驗。

優先使用Static方法

如果不需要訪問一個對象的屬性,可以將方法聲明爲static,這樣做可以使該方法的調用速度提高15%-20%。這是一種好的做法,從而可以通知方法簽名調用該方法並不會改變對象的屬性狀態。

使用static和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!";

這樣一來該類不再需要<clinit>方法,因爲上述常量將會使用靜態字段初始化。調用intVal時將會直接使用整型值42,訪問strVal時將會使用開銷更小的字符串常量而不是字段查找。

注意:這個優化只針對原始類型及String常量。

避免內部的Getters/Setters

在C++這樣的語言中,經常使用getters(如i=getCount())來代替直接的字段訪問(如i=mCount),這個特性同樣被用於C#、Java等面向對象的語言中。

然而在Android中,這樣使用並不是一個好習慣。方法調用比字段查找的開銷更大,雖然從面向對象編程的角度來看應該使用getters與setters,但在一個類的內部,應該儘可能直接訪問字段。

在沒有JIT的情況下,直接對字段進行訪問比調用getter方法大概要快3倍;有JIT的情況下,直接對字段進行訪問比getter大概要快7倍。

使用增強的for循環語法

增強的for循環如for-each,可以用於實現了Iterable接口的集合、數組。以下示例是幾種遍歷數組的方案:

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()比zero()快,它把所有變量都存儲爲局部變量,避免了查找,優化了獲取數組長度的性能開銷。

方法two()使用了增強的for循環語法,在無JIT時是最快的;在有JIT的情況下,它跟方法one()的性能大概類似。

對於私有內部類,使用包訪問權限代替私有訪問權限

看下面這個類的定義:

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”。

但是從虛擬機角度來看,從內部類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);
}

當Inner需要訪問外部類的私有方法及成員變量時就會調用上述靜態方法。這意味着需要通過合成的訪問器方法來訪問變量,因爲會比直接訪問要慢,這也可以作爲一個示例,說明了一些不可見的性能損耗。

爲了避免上述情況的性能開銷,可以將被內部類訪問方法及變量聲明爲包訪問權限而不是私有訪問權限。但這就意味着可以被同包的其他類直接訪問,因此最好不要出現在公有API中。

避免使用浮點型

衆所周知,在Android設備上浮點型比整型大概慢2倍。

從速度方面來看,float與double在現代硬件設備上幾乎沒有什麼區別。從空間開銷角度來看,double是float的2倍。對於臺式機來說,假如空間不是問題,那麼應該優先選擇double。

對於整型來說,一些處理器有硬件乘法而缺少硬件除法支持,這種情況下,整型除法以及求模運算需要在軟件層面處理,比如設計一個哈希表或進行大量的數學運算。

瞭解並使用Library

In addition to all the usual reasons to prefer library code over rolling your own, bear in mind that the system is at liberty to replace calls to library methods with hand-coded assembler, which may be better than the best code the JIT can produce for the equivalent Java. The typical example here is String.indexOf() and related APIs, which Dalvik replaces with an inlined intrinsic. Similarly, the System.arraycopy() method is about 9x faster than a hand-coded loop on a Nexus One with the JIT.

謹慎使用Native方法

使用Android NDK寫Native代碼來實現App功能不一定比使用Java高效。例如,Java與Native的交互需要開銷,並且JIT無法對此進行優化。一旦爲Native資源分配內存(如在Native heap中),就意味着很難安排收集這些資源,並且需要爲不同的CPU架構來編譯不同的版本,甚至需要爲相同的架構編譯多個版本:如爲G1的ARM處理器編譯的Native代碼並不能在Nexus One上充分發揮性能,而爲Nexus One的ARM編譯的代碼則無法在G1的ARM上運行。

當我們想將已有的Native代碼庫用到Android的時候,我們才應該使用Native代碼,而並非爲了提升Java代碼模塊的速度使用Native代碼。

如果需要使用Native代碼,需要了解JNI的相關知識。

性能神話

在沒有JIT的情況下,通過明確類型的對象來調用方法比接口調用要快一些,如HashMap對象的方法調用快於Map接口,兩者的效率大概相差6%左右,如果有JIT,則兩者效率幾乎是相同的。

在沒有JIT的情況下,緩存字段的訪問比重複訪問某個字段快20%;有JIT時,字段訪問開銷跟本地訪問相同,所以除非你覺得會讓代碼更易閱讀,否則這不需要優化。

持續衡量

在進行優化之前,最好先確認你需要解決的問題,務必確保能夠精確衡量到目前的性能狀態,否則即使優化之後,你也無法衡量優化所帶來的性能提升。

你可能會使用Traceview來進行分析,但是需要意識到,使用Traceview時是禁用JIT的,因此可能會導致當你覺得性能不佳,但使用JIT後,性能又會顯著提升的情況。當你根據Traceview的建議修改之後,務必要確保在沒有Traceview的情況下,優化後的代碼比以前的速度更快。

參考文獻 :Performance Tips

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章