前言
本章內容為開發人員指南(Dev Guide)/Best Practices/Designing For Performanc,這裡譯為“效能最佳化”,版本為Android3.1 r1,翻譯來自:"qiongju@gmail.com",歡迎大家訪問他的部落格:"http://admires.iteye.com/",再次感謝"qiongju@gmail.com" !期待你一起參與翻譯Android的相關資料,聯絡我over140@gmail.com。
聲明
歡迎轉載,但請保留文章原始出處:)
部落格園:http://www.cnblogs.com/
Android中文翻譯組:http://goo.gl/6vJQl
Designing for Performance
譯者署名: qiongju@gmail.com
譯者連結: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)型的對象數組高效。這對於其他任何基礎資料型別 (Elementary Data Type)的組合都通用。
* 如果需要實現一個容器來存放元組(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修飾符
考慮下面類首的聲明:
編譯器會產生一個類初始化方法<clinit>,當該類初次被使用時執行,這個方法將42存入intVal中,並得到類檔案字串常量strVal的一個引用。當這些值在後面被引用時,他們通過欄位尋找進行訪問。
我們改進實現,採用 final關鍵字:
類不再需要<clinit>方法,因為常量通過靜態欄位初始化器進入dex檔案中。引用intVal的代碼,將直接調用整形值42;而訪問strVal,也會採用相對開銷較小的“字串常量”(原文:“sring constant”)指令替代欄位尋找。(這種最佳化僅僅是針對基礎資料型別 (Elementary Data Type)和String類型常量的,而非任意的參考型別。但儘可能的將常量聲明為static final是一種好的做法。
使用改進的For迴圈文法
改進for迴圈(有時被稱為“for-each”迴圈)能夠用於實現了iterable介面的集合類及數組中。在集合類中,迭代器讓介面調用hasNext()和next()方法。在ArrayList中,手寫的計數迴圈迭代要快3倍(無論有沒有JIT),但其他集合類中,改進的for迴圈文法和迭代器具有相同的效率。
這裡有一些迭代數組的實現:
zero()是當中最慢的,因為對於這個遍曆中的曆次迭代,JIT並不能最佳化擷取數組長度的開銷。
One()稍快,將所有東西都放進局部變數中,避免了尋找。但僅只有聲明數組長度對效能改善有益。
Two()是在無JIT的裝置上運行最快的,對於有JIT的裝置則和one()不分上下。他採用了JDK1.5中的改進for迴圈文法。
結論:優先採用改進for迴圈,但在效能要求苛刻的ArrayList迭代中,考慮採用手寫計數迴圈。
(參見 Effective Java item 46.)
在私人內部內中,考慮用包存取權限替代私人存取權限
考慮下面的定義:
需要注意的關鍵是:我們定義的一個私人內部類(Foo$Inner),直接存取外部類中的一個私人方法和私人變數。這是合法的,代碼也會列印出預期的“Value is 27”。
但問題是,虛擬機器認為從Foo$Inner中直接存取Foo的私人成員是非法的,因為他們是兩個不同的類,儘管Java語言允許內部類訪問外部類的私人成員,但是通過編譯器產生幾個綜合方法來橋接這些間隙的。
內部類會在外部類中任何需要訪問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時,確實跑的快了。