Android官方開發文檔Training系列課程中文版:效能最佳化建議

來源:互聯網
上載者:User

標籤:

原文地址: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: 這樣最佳化手段僅僅適用於基礎資料型別 (Elementary Data Type)以及字串常量,不要作用其它類型。

避免內部的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

Android官方開發文檔Training系列課程中文版:效能最佳化建議

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.