Android效能最佳化之Performance Tips

來源:互聯網
上載者:User

標籤:

如果你真的願意去努力,你人生最壞的結果,也不過是大器晚成。

原文連結:http://developer.android.com/training/articles/perf-tips.html#UseFinal

概述

這篇文檔主要包含一些微小的最佳最佳化,當把這些組合起來的時候,可以提高App的整體效能,但它不太可能對效能造成戲劇性的影響。選擇合適的演算法和資料結構應該是你優先考慮的內容,但超出了本文的範圍。這個文檔更適合作為通用的編碼技巧,通過這些技巧使我們的代碼更高效。

編寫高效代碼有兩個基本規則:

  • 不執行不必要的操作

  • 不分配不必要的記憶體

我們面臨的最棘手的一個問題是我們的Android應用程式肯定會在多種類型的硬體上運行。不同版本的VM虛擬機器運行在不同的處理器的運行速度顯然是不同的。你甚至不能簡單地說“裝置X快/慢於裝置Y大概F倍”,然後想當然的認為其在他裝置上這個倍數也是成立的。但是,我們僅僅能從模擬器獲得很少關於裝置效能的資訊,它與真機還是有很大不同的。還有,有或沒有JIT(即時編譯)的裝置之間存在巨大差異:在有JIT的裝置上完美啟動並執行代碼在沒有JIT的裝置上並不一定能順暢運行。

我們要盡量通過最佳化效能來確保我們的應用在各種各樣的裝置上都表現良好,確保代碼在支援的不同版本上是有效。

避免建立不必要的對象

對象建立永遠不會是免費的。雖然一個帶有線程池的記憶體回收行程(garbage collector)可以使分配臨時對象的記憶體佔用降低,但分配記憶體總是比不分配記憶體更昂貴的。

當我們在我們的應用程式指派更多的對象時,將迫使一個周期性的記憶體回收,建立“打嗝”的使用者體驗,即卡頓。在Android 2.3中引入的並發垃圾收集器(GC)可以協助我們,但不必要的工作能避免要盡量避免。

因此,這樣我們應該避免建立我們不需要的對象,以下是一些執行個體:

  • 如果你有一個方法返回一個字串,你知道它的結果需要附加到StringBuffer上,改變你的實現方法使它可以直接添加,而不是建立一個短暫的臨時對象。
  • 當要從一組輸入資料中提取字串時,試著返回一個未經處理資料的子字串(substring),而不是建立一個重複的對象。使用substring的方式,你將建立一個新的String對象,但它將與原資料共用內部char[]空間的。(如果你只使用原始輸入的一小部分,無論怎麼操作,你都會把它保持在記憶體中,這點需要權衡)

一個更激進的想法是把一個多維陣列分割成平行的一維數組:

  • int類型的數組比Integer對象數組要好,由此可以知道兩個平行的int數組比(int,int)二維數組效率更高。這同樣適用於所有的未經處理資料類型。
  • 如果你需要實現一個容器儲存(Foo,Bar)對象的,試著記住兩個平行的Foo[]和Bar[]數組通常比單一陣列的自訂(Foo,Bar)對象要好。(但是有例外,比如當你設計一個API訪問其他代碼的時候。在這些情況下,通常可以犧牲小部分的效能達到良好的API設計。但在自己的內部代碼,我們應該試著儘可能的提高效率。)

一般來說,避免建立臨時對象。更少的對象建立意味著更少的垃圾收集,這對使用者體驗有直接的影響。

選擇Static而不是Virtual

如果你不需要訪問一個對象的欄位,可以使用static修飾你的方法。調用將快15%~20%。這是一種很好的做法,因為通過static你還可以知道調用的方法不能改變對象的狀態。

使用static final修飾常量

像這樣在一個類的頂部聲明常量:

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

編譯器產生一個類初始化方法,叫做< clinit >,當這個類第一次被使用時執行。這個函數將42存入intVal,還從class檔案的常量表中提取了strVal的引用。當之後使用intVal或strVal的時候,他們會直接被查詢到。

我們可以用final關鍵字來最佳化:

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

這時再也不需要上面的方法了,因為final聲明的常量進入了靜態dex檔案的域初始化部分。調用intVal的代碼會直接使用42,調用strVal的代碼也會使用一個相對廉價的“string constant”指令,而不是查表。

Notes:這個最佳化方法只對原始類型和String類型有效,而不是任意參考型別。不過,在必要時使用static final是個很好的習慣。

避免內部的Getters/Setters

像C++等native language,通常使用getters(i = getCount())而不是直接存取變數(i = mCount)。這是編寫C++的一種優秀習慣,而且通常也被其他物件導向的語言所採用,例如C#與Java,因為編譯器通常會做inline訪問,而且你需要限制或者調試變數,你可以在任何時候在getter/setter裡面添加代碼。

然而,在Android上,這不是一個好的寫法。虛函數的調用比起直接存取變數要耗費更多。在物件導向編程中,將getter和setting暴露給公用介面是合理的,但在類內部應該僅僅使用域直接存取。

在沒有JIT(Just In Time Compiler)時,直接存取變數的速度是調用getter的3倍。有JIT時,直接存取變數的速度是通過getter訪問的7倍。

請注意,如果你使用ProGuard,你可以獲得同樣的效果,因為ProGuard可以為你內聯訪問.

使用for-each迴圈

增強For迴圈(也被稱為 for-each 迴圈)可以被用在實現了 Iterable 介面的 collections 以及數組上。使用collection的時候,Iterator會被分配,用於for-each調用hasNext()和next()方法。使用ArrayList時,手寫的計數式for迴圈會快3倍(不管有沒有JIT),但是對於其他collection,增強for-each迴圈寫法會和迭代器寫法的效率一樣。

請比較下面三種迴圈的方法:

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()。

Tips:你還可以參考 Josh Bloch 的 《Effective Java》這本書的第46條

使用package代替private以便私人內部類高效訪問外部類成員

參考下面一段代碼

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和FooInner是不同的類,會認為在FooInner中直接存取Foo類的私人成員是不合法的。即使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()函數時,它都會調用這些靜態方法。這意味著,上面的代碼可以歸結為,通過accessor函數來訪問成員變數。早些時候我們說過,通過accessor會比直接存取域要慢。所以,這是一個特定語言用法造成效能降低的例子。

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

避免使用浮點類型

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

就速度而言,現代硬體上,float 和 double 的速度是一樣的。空間而言,double 是兩倍float的大小。在空間不是問題的情況下,你應該使用 double 。

同樣,對於整型,有些處理器實現了硬體幾倍的乘法,但是沒有除法。這時,整型的除法和取餘是在軟體內部實現的,這在你使用雜湊表或大量計算操作時要考慮到。

瞭解並使用庫函數

除了那些常見的讓你多使用內建庫函數的理由以外,記得系統函數有時可以替代第三方庫,並且還有彙編層級的最佳化,他們通常比帶有JIT的Java編譯出來的代碼更高效。典型的例子是:Android API 中的 String.indexOf(),Dalvik出於內聯效能考慮將其替換。同樣 System.arraycopy()函數也被替換,這樣的效能在Nexus One測試,比手寫的for迴圈並使用JIT還快9倍。

Tips:參見 Josh Bloch 的 《Effective Java》這本書的第47條

小心使用底層方法

結合Android NDK使用native代碼開發,並不總是比Java直接開發的效率更好的。Java轉native代碼是有代價的,而且JIT不能在這種情況下做最佳化。如果你在native代碼中分配資源(比如native堆上的記憶體,檔案描述符等等),這會對收集這些資源造成巨大的困難。你同時也需要為各種架構重新編譯代碼(而不是依賴JIT)。你甚至對已同樣架構的裝置都需要編譯多個版本:為G1的ARM架構編譯的版本不能完全使用Nexus One上ARM架構的優勢,反之亦然。

Native 代碼是在你已經有本地代碼,想把它移植到Android平台時有優勢,而不是為了最佳化已有的Android Java代碼使用。

如果你要使用JNI,請學習JNI Tips

Tips:參見 Josh Bloch 的 《Effective Java》這本書的第54條

效能謬見

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

在沒有JIT的裝置上,讀取緩衝域比直接讀取實際資料大概快20%。有JIT時,域讀取和本地讀取基本無差。所以最佳化並不值得除非你覺得能讓你的代碼更易讀(這對 final, static, static final 域同樣適用)。

關於測量

在最佳化之前,你應該確定你遇到了效能問題。你應該確保你能夠準確測量出現在的效能,否則你也不會知道最佳化是否真的有效。

本章節中所有的技巧都需要Benchmark(基準測試)的支援。Benchmark可以在 code.google.com “dalvik” project 中找到。

Benchmark是基於Java版本的 Caliper microbenchmarking架構開發的。Microbenchmarking很難做準確,所以Caliper幫你完成這部分工作,甚至還幫你測了你沒想到需要測量的部分(因為,VM幫你管理了代碼最佳化,你很難知道這部分最佳化有多大效果)。我們強烈推薦使用Caliper來做你的基準微測工作。

我們也可以用Traceview 來測量,但是測量的資料是沒有經過JIT最佳化的,所以實際的效果應該是要比測量的資料稍微好些。

關於如何測量與調試,還可以參考下面兩篇文章:

  • http://developer.android.com/tools/debugging/debugging-tracing.html
  • http://developer.android.com/tools/debugging/systrace.html

Android效能最佳化之Performance Tips

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.