Designing For Performance

Android 開發者指南 開發者指南(17) —— Designing For Performance 


前言 

本章內容爲開發者指南(Dev Guide)/Best Practices/Designing For Performanc,這裏 譯爲“性能優化”,版本爲 Android3.1 r1,翻譯來自:"[email protected]",歡迎大家訪問 他的博客:"http://admires.iteye.com/",再次感謝"[email protected]" !期待你一起 參與翻譯 Android 的相關資料,聯繫我 [email protected]


Designing for Performance

 譯者署名: [email protected] 

譯者鏈接:http://admires.iteye.com/ 版本:Android 3.1 r1 

原文 

http://developer.android.com/guide/practices/design/performance.html 

性能優化

 Android 應用程序運行的移動設備受限於其運算能力,存儲空間,及電池續航。由此,它必須是高效的。 電池續航可能是一個促使你優化程序的原因, 即使他看起來已經運行的足夠快了。 由於續航對用戶的重要性,當電量耗損陡增時,意味這用戶遲早會發現是由於你的程序。 

雖然這份文檔主要包含着細微的優化,但這些絕不能成爲你軟件成敗的關鍵。選擇合適的 算法和數據結構永遠是你最先應該考慮的事情,但這超出這份文檔之外。 


簡介 

寫出高效的代碼有兩條基本的原則: 不作沒有必要的工作。 儘量避免內存分配。


明智的優化 

這份文檔是關於 Android 規範的細微優化,所以先確保你已經瞭解哪些代碼需要優化,並 且知道如何去衡量你所做修改所帶來的效果(好或壞)。開發投入的時間是有限的,所以明智的 時間規劃很重要。 (更多分析和筆記參見總結。) 這份文檔同時確保你在算法和數據結構上作出最佳選擇的同時, 考慮 API 選擇所帶來的潛在 影響。使用合適的數據結構和算法比這裏的任何建議都更有價值,優先考慮 API 版本帶來的影 響有助於你找到更好的實現。(這在類庫代碼中更爲重要,相比應用代碼) 

(如果你需要這樣的建議,參見 Josh Bloch's Effective Java, item 47.) 

在優化 Android 程序時,會遇到的一個棘手問題是,保證你的程序能在不同的硬件平臺上 運行。虛擬機版本和處理器各部相同,因此運行在之上的速度也大不一樣。但這並且不是簡單的 A 比 B 快或慢,並能在設備間做出排列。特別的,模擬器上只能評測出一小部分設備上體現的 東西。 有無 JIT 的設備間也存在着巨大差異, JIT 設備上好的代碼有時候會在無 JIT 的設備上 在 表現的並不好。 如果你想知道一個程序在設備上的具體表現,就必須在上面進行測試。

 避免創建不必要的對象 

對象創建永遠不會是免費的。 每個線程的分代 GC 給零時對象分配一個地址池以降低分配開 銷,但往往內存分配比不分配需要的代價大。 

如果在用戶界面週期內分配對象,就會強制一個週期性的垃圾回收,給用戶體驗增加小小的 停頓間隙。Gingerbread 中提到的併發回收也許有用,但不必要的工作應當被避免的。 

因此,應該避免不必要的對象創建。

下面是幾個例子: 

如果有一個返回 String 的方法,並且他的返回值常常附加在一個 StringBuffer 上, 改變聲明和實現,讓函數直接在其後面附加,而非創建一個短暫存在的零時變量。

當從輸入的數據集合中讀取數據時,考慮返回原始數據的子串,而非新建一個拷貝.這 樣你雖然創建一個新的對象,但是他們共享該數據的 char 數組。(結果是即使僅僅使 用原始輸入的一部分,你也需要保證它的整體一直存在於內存中。) 

一個更徹底的方案是將多維數組切割成平行一維數組:

 Int 類型的數組常有餘 Integer 類型的。推而廣之,兩個平行的 int 數組要比一個 (int,int)型的對象數組高效。這對於其他任何基本數據類型的組合都通用。 如果需要實現一個容器來存放元組(Foo,Bar),兩個平行數組 Foo[],Bar[]會優於一 個(Foo,Bar)對象的數組。(例外情況是:當你設計 API 給其他代碼調用時,應用 好的 API 設計來換取小的速度提升。但在自己的內部代碼中,儘量嘗試高效的實現。) 通常來講,儘量避免創建短時零時對象.少的對象創建意味着低頻的垃圾回收。而這對於用 戶體驗產生直接的影響。 


性能之謎 

前一個版本的文檔給出了好多誤導人的主張,這裏做一些澄清: 在沒有 JIT 的設備上, 調用方法所傳遞的對象採用具體的類型而非接口類型會更高效 (比如, 傳遞 HashMap map 比 Map map 調用一個方法的開銷小,儘管兩個 map 都是 HashMap). 但這並不是兩倍慢的情形,事實上,他們只相差 6%,而有 JIT 時這兩種調用的效率不相上下。 在沒有 JIT 的設備上,緩存後的字段訪問比直接訪問快大概 20%。而在有 JIT 的情況下, 字段訪問的代價等同於局部訪問,因此這裏不值得優化,除非你覺得他會讓你的代碼更易讀(對 於 final ,static,及 static final 變量同樣適用) 

用靜態代替虛擬 

如果不需要訪問某對象的字段,將方法設置爲靜態,調用會加速 15%到 20%。這也是 一種好的做法,因爲你可以從方法聲明中看出調用該方法不需要更新此對象的狀態。 

避免內部的 Getters/Setters

在源生語言像 C++中,通常做法是用 Getters(i=getCount())代替直接字段訪問 (i=mCount)。這是 C++中一個好的習慣,因爲編譯器會內聯這些訪問,並且如果需要約束 或者調試這些域的訪問,你可以在任何時間添加代碼。 而在 Android 中,這不是一個好的做法。虛方法調用的代價比直接字段訪問高昂許多。通常根據面嚮對象語言的實踐,在公共接口中使用 Getters 和 Setters 是有道理的,但在一個字 段經常被訪問的類中宜採用直接訪問。 無 JIT 時,直接字段訪問大約比調用 getter 訪問快 3 倍。

有 JIT 時(直接訪問字段開銷等同於 局部變量訪問) 要快 7 倍。 Froyo 版本中確實如此 , 在 但以後版本可能會在 JIT 中改進 Getter 方法的內聯。

 對常量使用 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>方法,因爲常量通過靜態字段初始化器進入 dex 文件中。引用 intVal 的代碼,將直接調用整形值 42;而訪問 strVal,也會採用相對開銷較小的“字符串常量”(原文: “sring constant”)指令替代字段查找。(這種優化僅僅是針對基本數據類型和 String 類型常 量的,而非任意的引用類型。但儘可能的將常量聲明爲 static final 是一種好的做法。 

使用改進的 For 循環語法 

改進 for 循環(有時被稱爲“for-each”循環)能夠用於實現了 iterable 接口的集合類及數 組中。在集合類中,迭代器讓接口調用 hasNext()和 next()方法。在 ArrayList 中,手寫的計數循環迭代要快 3 倍(無論有沒有 JIT),但其他集合類中,改進的 for 循環語法和迭代器具有 相同的效率。 

這裏有一些迭代數組的實現:

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()不分上下。他採用 了 JDK1.5 中的改進 for 循環語法。 

結論:優先採用改進 for 循環,但在性能要求苛刻的 ArrayList 迭代中,考慮採用手寫計數 循環。 (參見 Effective Java item 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);
    }
}

需要注意的關鍵是:我們定義的一個私有內部類(Foo$Inner),直接訪問外部類中的一 個私有方法和私有變量。這是合法的,代碼也會打印出預期的“Value is 27”。

 但問題是, 虛擬機認爲從 Foo$Inner 中直接訪問 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 方法的地方調用這些靜態方法。 這意味着這些代碼將直接存取成員變量表現爲通過存取器方法訪問。 之前提到過存取器訪 問如何比直接訪問慢,這例子說明,某些語言約會定導致不可見的性能問題。

 如果你在高性能的 Hotspot 中使用這些代碼,可以通過聲明被內部類訪問的字段和成員爲 包訪問權限,而非私有。但這也意味着這些字段會被其他處於同一個包中的類訪問,因此在公共 API 中不宜採用。

合理利用浮點數

通常的經驗是,在 Android 設備中,浮點數會比整型慢兩倍,在缺少 FPU 和 JIT 的 G1 上 對比有 FPU 和 JIT 的 Nexus One 中確實如此(兩種設備間算術運算的絕對速度差大約是 10 倍)

 從速度方面說,在現代硬件上,float 和 double 之間沒有任何不同。更廣泛的講,double 大 2 倍。在臺式機上,由於不存在空間問題,double 的優先級高於 float。 

但即使是整型,有的芯片擁有硬件乘法,卻缺少除法。這種情況下,整型除法和求模運算 是通過軟件實現的,就像當你設計 Hash 表,或是做大量的算術那樣。

 瞭解並使用類庫 

選擇 Library 中的代碼而非自己重寫,除了通常的那些原因外,考慮到系統空閒時會用 彙編代碼調用來替代 library 方法,這可能比 JIT 中生成的等價的最好的 Java 代碼還要好。典 型的例子就是 String.indexOf,Dalvik 用內部內聯來替代。同樣的,System.arraycopy 方 法在有 JIT 的 Nexus One 上,自行編碼的循環快 9 倍。 (參見 Effective Java item 47.)

 合理利用本地方法 

本地方法並不是一定比 Java 高效。最起碼,Java 和 native 之間過渡的關聯是有消耗的, 而 JIT 並不能對此進行優化。當你分配本地資源時(本地堆上的內存,文件說明符等),往往很 難實時的回收這些資源。同時你也需要在各種結構中編譯你的代碼(而非依賴 JIT)。甚至可能 需要針對相同的架構來編譯出不同的版本:針對 ARM 處理器的 GI 編譯的本地代碼,並不能充 分利用 Nexus One 上的 ARM, 而針對 Nexus One 上 ARM 編譯的本地代碼不能在 G1 的 ARM 上運行。 

當你想部署程序到存在本地代碼庫的 Android 平臺上時,本地代碼才顯得尤爲有用,而並 非爲了 Java 應用程序的提速。 (參見 Effective Java item 54.)

結語 

最後:通常考慮的是:先確定存在問題,再進行優化。並且你知道當前系統的性能,否則 無法衡量你進行嘗試所得到的提升。 

這份文檔中的每個主張都有標準基準測試作爲支持。你可以在 code.google.com“dalvik” 項目中找到基準測試的代碼。 

這個標準基準測試是建立在 Caliper Java 標準微基準測試框架之上的。標準微基準測試很 難找到正確的路, 所以 Caliper 幫你完成了其中的困難部分工作。 並且當你會察覺到某些情況的 測試結果並想象中的那樣(虛擬機總是在優化你的代碼的)。我們強烈推薦你用 Caliper 來運行 你自己的標準微基準測試。 

同時你也會發現 Traceview 對分析很有用,但必須瞭解,他目前是不不支持 JIT 的,這可 能導致那些在 JIT 上可以勝出的代碼運行超時。特別重要的,根據 Taceview 的數據作出更改 後,請確保代碼在沒有 Traceview 時,確實跑的快了。

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