Android 開發者指南之性能提示

翻譯自 Android 開發者訓練課程,原文鏈接:Performance tips


這篇文檔主要涵蓋了一些微小的優化,組合它們能夠提升應用的整體性能,但是這些變化不會帶來戲劇性的效果。你應該優選選擇正確的算法和數據結構,但是它超出了本文檔要說明的範圍。在一般的開發練習中,你應該使用本文檔中的提示,這樣才能把提高代碼效率當成一種習慣。

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

  • 不要做不該做的事
  • 儘量避免分配內存

當你微優化安卓應用時,面對最棘手的問題之一就是,你的應用會運行在各種不同類型的硬件上。不同版本的虛擬機跑在不同處理器上,運行速度也不同。通常你不能簡單地說,設備 X 是比設備 Y 運行快/慢的因素,將結果從一個設備擴展到其他設備。特別是,關於在其他設備上的性能,模擬器上的測量結果不全面。有沒有 JIT 的設備也有非常大的差異:具有 JIT 的設備的最佳代碼並不總是沒有設備的最佳代碼。

爲確保你的應用在各種設備上都能正常運行,確保你的代碼在各個級別都高效,並積極優化你的性能。

避免創建不必要的對象

創建對象並不是沒有開銷的。分代垃圾收集器具有用於臨時對象的每個線程分配池,這可以使分配更便宜,但是分配內存總是比不分配代價要大。

當你在應用中創建更多的對象時,你將被迫進行垃圾收集,對於用戶體驗來說,它就像「打嗝」一樣的。在安卓 2.3 之後引入了併發垃圾收集器,但是也應該避免不必要的工作。

因此,你要避免創建不必要的對象。下面是一些例子:

  • 如果你的方法返回一個字符串,你知道它的結果總會拼接到 StringBuffer,這時你就該更改簽名和實現,這樣函數會直接追加,而不是創建存活期短的臨時對象。
  • 當從輸入數據提取字符串時,嘗試返回原始數據到子字符串,而不是創建一個拷貝。你會創建一個新的 String 對象,但是它會和原始數據共享 char[]。(需要考慮的是,如果你只使用原始輸入的一小部分,那麼無論如何,如果你用這個方法,你都會在內存中保留它。))

一個激進的想法是,把多維數組切片變成並行的一維數組。

  • int 數組比 Integer 對象數組好多了。但是概括來說,兩個並行的 int 數組同樣比二維數組 (int,int)高效。對於其他的基本數據類型的組合也是如此。
  • 如果你需要實現一個容器,用來存儲二元組 (Foo,Bar) 對象,記住兩個並行的 Foo[]Bar[] 數組通常比一個常規的 (Foo,Bar) 對象數組要好得多。(當然例外情況是,你爲其他代碼設計 API 以進行訪問。在這些情況下,爲了實現良好的 API 設計,通常最好對速度進行小的折衷。但是在你自己的內部代碼中,你應該嘗試儘可能高效。)

一般來說,儘量避免創建短期的臨時對象。更少地創建對象意味着更低頻率的垃圾回收,這對用戶體驗有直接影響。

首選靜態虛擬

如果你不需要訪問對象的字段,請將方法設爲靜態,調用速度就會提高 15%-20%。這也是很好的做法,因爲你可以從方法簽名中看出,調用方法不能改變對象的狀態。

考慮下面的在類首部的聲明。

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

編譯器生成一個類的初始化方法,叫做 <clint>,當第一次使用類的時候,該方法會被執行。這個方法把值 42 存在 intVal 變量中,從類文件字符串常量表中提取一個引用指向 strVal。當稍後引用這些值時,通過字段可以訪問它們。

我們可以使用 final 關鍵字改善這一步:

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

這樣,類就不需要 <clinit> 方法了,因爲常量進入 dex 文件中的靜態字段初始值設定項。引用 intVal 的代碼會直接使用整數值 42,訪問 strVal 會使用相對划算的「字符串常量」指令,而不是字段查找。

注意:此優化僅適用於基本類型和字符串常量,而不適用於任意引用類型。儘管如此,最好儘可能地聲明常量 static final 值。

使用增強型 for 循環

增強型 for 循環(也就是 for-each 循環)可以遍歷實現了 Iterable 接口的集合和數組。對於集合,迭代器被分配用於創建叫做 hasNext()next() 的接口。對於 ArrayList,一個手寫的計數循環比 for-each 快約 3 倍,但是對於其他集合,增強型 for 循環完全等同於顯式迭代器用法。

這裏有幾個遍歷數組的方案:

static class Foo {
    int splat;
}

Foo[] array = ...

public void zero() {
    int sum = 0;
    for (int i = 0; i < array.length; ++i) {
        sum += array[i].splat;
    }
}

public void one() {
    int sum = 0;
    Foo[] localArray = array;
    int len = localArray.length;

    for (int i = 0; i < len; ++i) {
        sum += localArray[i].splat;
    }
}

public void two() {
    int sum = 0;
    for (Foo a : array) {
        sum += a.splat;
    }
}

zero() 最慢,因爲每次通過循環迭代獲得數組長度是有成本的,JIT 還不會優化。

one() 快一些,它將所有內容都拉到局部變量中,從而避免了查找。只有數組的長度才能提供性能優勢。

two() 在沒有 JIT 的設備上是最快的,與具有 JIT 的設備的 one() 無法區分。它使用了 Java 語言 1.5 版本後引入的增強型 for 循環語法。

所以,你應該默認使用增強型 for 循環,但是考慮一個手寫的計數循環,用於性能關鍵的 ArrayList 迭代。

考慮包而不是私有內部類的私有訪問

來看下面的類的定義:

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 is27」。

問題是,虛擬機認爲從 Foo$Inner 直接訪問 Foo 的私有成員是非法的,因爲 FooFoo$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() 方法時,它會調用這些靜態方法。這意味着上面的代碼實際上歸結爲,你通過訪問器方法訪問成員字段的情況。之前我們討論到訪問器如何比直接訪問字段更慢。所以這是一個特定語言習語的例子,導致「看不見」的表演。

避免使用浮點型

根據經驗,浮點數 比Android 設備上的整數慢約 2 倍。

在速度方面,現代硬件上的 floatdouble 沒有區別。在空間方面,double 大 2 倍。與桌面計算機一樣,假設空間不是問題,您應該更喜歡 double

此外,即使對於整數,一些處理器也有硬件乘法但缺乏硬件除法。在這種情況下,整數除法和模數運算在軟件中執行 - 如果您正在設計哈希表或進行大量數學運算,則需要考慮。

瞭解並使用庫

除了喜歡庫代碼而不是自己編寫代碼,請記住系統可以自由地用手動編譯彙編程序替換對庫方法的調用,這可能比 JIT 可以生成的等效的 Java 最佳代碼更好。這裏典型的例子是 String.indexOf() 和相關的 API,Dalvik 用內聯的內在代替。類似地,System.arraycopy() 方法比帶有 JIT 的 Nexus One 上的手動編碼循環快約 9 倍。

小心使用原生方法

使用 Android NDK 的原生代碼開發應用,不一定比用 Java 語言開發的更高效。一方面,Java 和 原生之間傳遞有損耗,JIT 不會跨越這些邊界優化。如果你分配了原生資源(原生堆上的內存,文件描述符,或其他內容),安排及時收集這些資源可能要困難得多。你還需要爲要運行的每個體系結構編譯代碼(而不是依賴於具有 JIT 的體系結構)。你可能甚至需要爲相同的架構編譯多個版本:爲 G1 中的 ARM 處理器編譯的原生代碼無法充分利用 Nexus One 中的 ARM,以及爲 Nexus One 中的 ARM 編譯的代碼不會在 G1 中的 ARM 上運行。

性能神話

在沒有 JIT 的設備上,通過具有精確類型而不是接口的變量調用方法確實更有效。(因此例如,調用 HashMap 映射上的方法比使用 Map 映射更便宜,即使在這兩種情況下映射都是 HashMap。)情況並非如此慢 2 倍,實際差異更像是慢了 6%。此外,JIT 使兩者有效地難以區分。

在沒有 JIT 的設備上,緩存字段訪問比重複訪問字段快約 20%。使用 JIT,字段訪問的成本與本地訪問大致相同,因此除非您覺得它使代碼更易於閱讀,否則這不值得進行優化。(對於 final,static 和 static final 字段也是如此。)

總是測量

在開始優化之前,請確保你遇到需要解決的問題。確保你可以準確衡量現有的績效,否則你將無法衡量嘗試的替代方案的好處。

你可能還會發現 Traceview 對於分析很有用,但重要的是要知道當前會禁用 JIT,這可能會導致它錯誤地將時間錯誤歸結爲 JIT 可能能夠贏回的代碼。在 Traceview 數據建議進行更改以確保在沒有 Traceview 的情況下運行時生成的代碼實際運行得更快時,這一點尤其重要。

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