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 時,確實跑的快了。