Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android官方開發文檔Training系列課程中文版:性能優化建議

Android官方開發文檔Training系列課程中文版:性能優化建議

編輯:關於Android編程

原文地址:http://android.xsoftlab.net/training/articles/perf-tips.html

本篇文章主要介紹那些可以提升整體性能的微小優化點。它與那些能突然改觀性能效果的優化手段並不屬於同一類。選擇正確的算法與數據結構必然是我們的第一總則,但是這不是我們這篇文章要介紹的。你應該將這篇文章所提及的知識點作為編碼的日常習慣,這可以提升常規代碼的執行效率。

下面是書寫代碼的基本准則:

絕不要做你不需要的工作。 如果可以不申請內存就不要申請,要合理復用已有的對象。

另一個較復雜的問題就是被優化過的APP肯定是要運行在各種類型的硬件平台上。不同版本的虛擬機運行在不同的處理器上肯定會有不同的運行速度。需要特別說明的是,在模擬器上測試很少會得知其它設備的性能。在不同設備上還有一個很大的不同點就是有沒有JIT(JIT的意思是即時編譯器):在JIT設備上運行的最優代碼並不總在沒有JIT設備上有效。

為了確保APP可以在各類設備上運行良好,要確保代碼在各個版本的平台上都是高效的。

避免創建不必要的對象

創建對象絕不是沒有成本的。雖然分代垃圾收集器可以使臨時對象的分配成本變得很低,但是內存分配的成本總是遠高於非內存分配的成本。

隨著更多對象的生成,你可能就開始關注垃圾收集器了。雖然Android 2.3中出現的並發收集器可能會幫到你,但是不必要的工作總是應該避免的。

因此,要避免創建不需要的對象。下面的示例可能會幫到你:

如果你有個返回字符串的方法,該方法所返回的字符串總是被接在一個StringBuffer對象後面。那麼就可以更改此方法的實現方式:讓該字符串直接跟在StringBuffer的後面返回。這樣就可以避免創建那些臨時性的變量。 當從字符串中提取子串時,應該嘗試返回原始數據的子串,而不是創建一個副本。子串將會創建一個新的String對象,但是它與char[]共用的是同一數據。采用這種方式的唯一不足就是:雖然使用了其中的一部分數據,但是剩余的數據還都保留在內存中。

一條更為先進的法則就是,將多維數組轉換為平行數組使用:

int數組的效率要比Integer數組的效率高的多。 如果你需要實現一個用於存儲(Foo,Bar)對象的數組,要記得使用兩個平行的Foo[],Bar[]數組,這要比單一的(Foo,Bar)數組效率好太多。

通常來說,要盡量避免創建那些生命周期很短的臨時變量。更少的對象創建意味著更低頻率的垃圾回收,這會直接反應到用戶體驗上。

首選靜態

如果不需要訪問對象的屬性,那麼就可以將方法設置為靜態方法。這樣調用將會增加15%-20%的速度。這還是一個好的習慣,因為這樣可以告訴其它方法一個信號:它們更改不了對象的狀態。

使用常量

請先考慮以下聲明:

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>方法,因為常量的初始化工作被移入了dex文件中。代碼可以直接引用intVal為42的值,並且訪問strVal也會直接得到字符串”string constant” ,這樣可以省去了查找字符串的過程。

Note: 這樣優化手段僅僅適用於基本數據類型以及字符串常量,不要作用其它類型。

避免內部的get\set方法

像C++這種本地語言通常都會使用get方法來訪問屬性。這對C++來說是一個非常好的習慣,並且C#、Java等面向對象語言也廣泛使用這種方式,因為編譯器通常會進行內聯訪問,並且如果你需要限制訪問或者調試屬性的話,只需要添加代碼就可以。

不過,這在Android上並不是個好習慣。方法調用的開銷是非常大的。雖然為了遵循面向對象語言提供get、set方法是合理的,但是在Android中最好是可以直接訪問對象的字段。

在沒有JIT的設備中,直接訪問對象字段的速度要比通過get方法訪問的速度快3倍。在含有JIT的設備中,這個效率會達到7倍之多。

注意:如果你使用了ProGuard,那麼就有了一個兩全其美的結果,因為ProGuard會直接為你進行內聯訪問。

使用增強for循環

增強for循環可用於實現了Iterable接口的集合或數組。在集合內部,迭代器需要實現接口方法:hasNext()以及next()。

有以下幾種訪問數組的方式:

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()方法是最快的。它使用了增強for循環。

所以應當在默認情況下使用增強for循環。

Tip: 也可以查看Josh Bloch 的 Effective Java,第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);
    }
}

上面的代碼定義了一個內部類,它可以直接訪問外部類的私有成員以及私有方法。這是正確的,這段代碼將會打印出我們所期望的”Value is 27”。

這裡的問題是:VM會認為Foo$Inner直接訪問Foo對象的私有成員是非法的,因為Foo和Foo$Inner是兩個不同的類,雖然Java語言允許內部類可以直接訪問外部類的私有成員(PS:虛擬機與語言是兩種互不干擾的存在)。為了彌補這種差異,編譯器專門為此生成了一組方法:

/*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()方法時會調用上面這些靜態方法。上面的代碼歸結為你所訪問的成員屬性都是通過訪問器方法訪問的。早期我們說通過訪問器訪問要比直接訪問慢很多,所以這是一段特定語言形成的隱性性能開銷示例。

避免使用浮點型

一般來說,在Android設備上浮點型要比整型慢大概2倍的速度。

在速度方面,float與double並沒有什麼區別。在空間方面,double是float的兩倍大。所以在桌面級設備上,假設空間不是問題,那麼我們應當首選double,而不是float。

還有,在對待整型方面,某些處理器擅長乘法,不擅長除法。在這種情況下,整型的除法與取模運算都是在軟件中進行的,如果你正在設計一個哈希表或者做其它大量的數學運算的話,這些東西應該考慮到。

使用本地方法要當心

使用本地代碼開發的APP並不一定比Java語言編寫的APP高效多少。首先,它會花費在Java-本地代碼的轉換過程中,並且JIT也不能優化到這些邊界。如果你正在申請本地資源,那麼對於這些資源的收集能明顯的感覺到困難。除此之外,你還需要對每一種CPU架構進行單獨編譯。你可能甚至還需要為同一個CPU架構編譯多個不同的版本:為G1的ARM處理器編譯的代碼不能運行在Nexus One的ARM處理上,為Nexus One的ARM處理器編譯的代碼也同樣不能運行在G1的ARM處理器上。

本地代碼在這種情況下適宜采用:當你有一個已經存在的本地代碼庫,你希望將它移植到Android上時,不要為了改善Java語言所編寫的代碼速度而去使用本地代碼。

如果你需要使用本地代碼,那麼應該讀一讀JNI Tips.

Tip: 相關信息也可以查看Josh Bloch 的 Effective Java,第54條。

性能誤區

在沒有JIT的設備中,通過具體類型的變量調用方法要比抽象接口的調用要高效,這是事實。舉個例子,通過HashMap map調用方法要比Map map調用方法要高效的多,開銷也少,雖然這兩個實現都是HashMap。事實上速度並不會慢2倍這麼多;真實的不同大概有6%的減緩。進一步講,JIT會使兩者的差別進一步縮小。

在沒有JIT的設備上,通過緩存訪問屬性要比反復訪問屬性要快將近20%的速度。在JIT的設備中,屬性訪問的花銷與本地訪問的花銷基本一致,所以這不是一項有多少價值的優化手段,除非你覺得這樣做的話代碼更易讀(這對static,final,常量同樣適用)。

經常估測

在開始優化之前,要確保你有個問題需要解決:要確保你可以精准測量現有的性能,否則將不能觀察到優化所帶來的提升。

基准點由Caliper的微型基准點框架創建。基准點很難正確獲得,所以Caliper將這份很難處理的工作做了,甚至是在你沒有在測量那些你想測量的地方的時候它也在工作。我們強烈的推薦你使用Caliper來創建自己的微型基准點。

你可能還發現Traceview非常有助於提升性能,不過你應該意識到Traceview工作的時候JIT並沒有開啟。這會錯誤的認為JIT會將損失掉的時間彌補回來。這尤其重要:根據Traceview所更改的結果會使實際代碼運行的更快。

有關更多提升APP性能的工具及方法,請參見以下文檔:

Profiling with Traceview and dmtracedump Analyzing UI Performance with Systrace
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved