Android官方开发文档Training系列课程中文版:性能优化建议
原文地址:http://android.xsoftlab.net/training/articles/perf-tips.html
本篇文章主要介紹那些可以提升整體性能的微小優(yōu)化點。它與那些能突然改觀性能效果的優(yōu)化手段并不屬于同一類。選擇正確的算法與數(shù)據(jù)結(jié)構(gòu)必然是我們的第一總則,但是這不是我們這篇文章要介紹的。你應(yīng)該將這篇文章所提及的知識點作為編碼的日常習慣,這可以提升常規(guī)代碼的執(zhí)行效率。
下面是書寫代碼的基本準則:
- 絕不要做你不需要的工作。
- 如果可以不申請內(nèi)存就不要申請,要合理復用已有的對象。
另一個較復雜的問題就是被優(yōu)化過的APP肯定是要運行在各種類型的硬件平臺上。不同版本的虛擬機運行在不同的處理器上肯定會有不同的運行速度。需要特別說明的是,在模擬器上測試很少會得知其它設(shè)備的性能。在不同設(shè)備上還有一個很大的不同點就是有沒有JIT(JIT的意思是即時編譯器):在JIT設(shè)備上運行的最優(yōu)代碼并不總在沒有JIT設(shè)備上有效。
為了確保APP可以在各類設(shè)備上運行良好,要確保代碼在各個版本的平臺上都是高效的。
避免創(chuàng)建不必要的對象
創(chuàng)建對象絕不是沒有成本的。雖然分代垃圾收集器可以使臨時對象的分配成本變得很低,但是內(nèi)存分配的成本總是遠高于非內(nèi)存分配的成本。
隨著更多對象的生成,你可能就開始關(guān)注垃圾收集器了。雖然Android 2.3中出現(xiàn)的并發(fā)收集器可能會幫到你,但是不必要的工作總是應(yīng)該避免的。
因此,要避免創(chuàng)建不需要的對象。下面的示例可能會幫到你:
- 如果你有個返回字符串的方法,該方法所返回的字符串總是被接在一個StringBuffer對象后面。那么就可以更改此方法的實現(xiàn)方式:讓該字符串直接跟在StringBuffer的后面返回。這樣就可以避免創(chuàng)建那些臨時性的變量。
- 當從字符串中提取子串時,應(yīng)該嘗試返回原始數(shù)據(jù)的子串,而不是創(chuàng)建一個副本。子串將會創(chuàng)建一個新的String對象,但是它與char[]共用的是同一數(shù)據(jù)。采用這種方式的唯一不足就是:雖然使用了其中的一部分數(shù)據(jù),但是剩余的數(shù)據(jù)還都保留在內(nèi)存中。
一條更為先進的法則就是,將多維數(shù)組轉(zhuǎn)換為平行數(shù)組使用:
- int數(shù)組的效率要比Integer數(shù)組的效率高的多。
- 如果你需要實現(xiàn)一個用于存儲(Foo,Bar)對象的數(shù)組,要記得使用兩個平行的Foo[],Bar[]數(shù)組,這要比單一的(Foo,Bar)數(shù)組效率好太多。
通常來說,要盡量避免創(chuàng)建那些生命周期很短的臨時變量。更少的對象創(chuàng)建意味著更低頻率的垃圾回收,這會直接反應(yīng)到用戶體驗上。
首選靜態(tài)
如果不需要訪問對象的屬性,那么就可以將方法設(shè)置為靜態(tài)方法。這樣調(diào)用將會增加15%-20%的速度。這還是一個好的習慣,因為這樣可以告訴其它方法一個信號:它們更改不了對象的狀態(tài)。
使用常量
請先考慮以下聲明:
static int intVal = 42; static String strVal = "Hello, world!";編譯器會產(chǎn)生出一個類的實例化方法,名為< clinit>,它會在類首次被用到的時候執(zhí)行。該方法會將值42存到intVal中,并將字符串常量表中的引用賦給strVal。當這些值被引用之后,其它屬性才可以訪問它們。
我們可以使用”final”關(guān)鍵字來改進一下:
static final int intVal = 42; static final String strVal = "Hello, world!";這樣的話,類就不需要再調(diào)用< clinit>方法,因為常量的初始化工作被移入了dex文件中。代碼可以直接引用intVal為42的值,并且訪問strVal也會直接得到字符串”string constant” ,這樣可以省去了查找字符串的過程。
Note: 這樣優(yōu)化手段僅僅適用于基本數(shù)據(jù)類型以及字符串常量,不要作用其它類型。
避免內(nèi)部的get\set方法
像C++這種本地語言通常都會使用get方法來訪問屬性。這對C++來說是一個非常好的習慣,并且C#、Java等面向?qū)ο笳Z言也廣泛使用這種方式,因為編譯器通常會進行內(nèi)聯(lián)訪問,并且如果你需要限制訪問或者調(diào)試屬性的話,只需要添加代碼就可以。
不過,這在Android上并不是個好習慣。方法調(diào)用的開銷是非常大的。雖然為了遵循面向?qū)ο笳Z言提供get、set方法是合理的,但是在Android中最好是可以直接訪問對象的字段。
在沒有JIT的設(shè)備中,直接訪問對象字段的速度要比通過get方法訪問的速度快3倍。在含有JIT的設(shè)備中,這個效率會達到7倍之多。
注意:如果你使用了ProGuard,那么就有了一個兩全其美的結(jié)果,因為ProGuard會直接為你進行內(nèi)聯(lián)訪問。
使用增強for循環(huán)
增強for循環(huán)可用于實現(xiàn)了Iterable接口的集合或數(shù)組。在集合內(nèi)部,迭代器需要實現(xiàn)接口方法:hasNext()以及next()。
有以下幾種訪問數(shù)組的方式:
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不能夠?qū)γ看卧L問數(shù)組長度的開銷進行優(yōu)化。
one()方法是稍快點的。它將一切元素放入了本地變量,這樣避免了每一次的查詢。只有數(shù)組的長度提供了明顯的性能提升。
two()方法是最快的。它使用了增強for循環(huán)。
所以應(yīng)當在默認情況下使用增強for循環(huán)。
Tip: 也可以查看Josh Bloch 的 Effective Java,第46條。
考慮使用包內(nèi)訪問
請先思考以下類定義:
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);} }上面的代碼定義了一個內(nèi)部類,它可以直接訪問外部類的私有成員以及私有方法。這是正確的,這段代碼將會打印出我們所期望的”Value is 27”。
這里的問題是:VM會認為Foo$Inner直接訪問Foo對象的私有成員是非法的,因為Foo和Foo$Inner是兩個不同的類,雖然Java語言允許內(nèi)部類可以直接訪問外部類的私有成員(PS:虛擬機與語言是兩種互不干擾的存在)。為了彌補這種差異,編譯器專門為此生成了一組方法:
/*package*/ static int Foo.access$100(Foo foo) {return foo.mValue; } /*package*/ static void Foo.access$200(Foo foo, int value) {foo.doStuff(value); }當內(nèi)部類代碼需要訪問屬性mValue或者調(diào)用doStuff()方法時會調(diào)用上面這些靜態(tài)方法。上面的代碼歸結(jié)為你所訪問的成員屬性都是通過訪問器方法訪問的。早期我們說通過訪問器訪問要比直接訪問慢很多,所以這是一段特定語言形成的隱性性能開銷示例。
避免使用浮點型
一般來說,在Android設(shè)備上浮點型要比整型慢大概2倍的速度。
在速度方面,float與double并沒有什么區(qū)別。在空間方面,double是float的兩倍大。所以在桌面級設(shè)備上,假設(shè)空間不是問題,那么我們應(yīng)當首選double,而不是float。
還有,在對待整型方面,某些處理器擅長乘法,不擅長除法。在這種情況下,整型的除法與取模運算都是在軟件中進行的,如果你正在設(shè)計一個哈希表或者做其它大量的數(shù)學運算的話,這些東西應(yīng)該考慮到。
使用本地方法要當心
使用本地代碼開發(fā)的APP并不一定比Java語言編寫的APP高效多少。首先,它會花費在Java-本地代碼的轉(zhuǎn)換過程中,并且JIT也不能優(yōu)化到這些邊界。如果你正在申請本地資源,那么對于這些資源的收集能明顯的感覺到困難。除此之外,你還需要對每一種CPU架構(gòu)進行單獨編譯。你可能甚至還需要為同一個CPU架構(gòu)編譯多個不同的版本:為G1的ARM處理器編譯的代碼不能運行在Nexus One的ARM處理上,為Nexus One的ARM處理器編譯的代碼也同樣不能運行在G1的ARM處理器上。
本地代碼在這種情況下適宜采用:當你有一個已經(jīng)存在的本地代碼庫,你希望將它移植到Android上時,不要為了改善Java語言所編寫的代碼速度而去使用本地代碼。
如果你需要使用本地代碼,那么應(yīng)該讀一讀JNI Tips.
Tip: 相關(guān)信息也可以查看Josh Bloch 的 Effective Java,第54條。
性能誤區(qū)
在沒有JIT的設(shè)備中,通過具體類型的變量調(diào)用方法要比抽象接口的調(diào)用要高效,這是事實。舉個例子,通過HashMap map調(diào)用方法要比Map map調(diào)用方法要高效的多,開銷也少,雖然這兩個實現(xiàn)都是HashMap。事實上速度并不會慢2倍這么多;真實的不同大概有6%的減緩。進一步講,JIT會使兩者的差別進一步縮小。
在沒有JIT的設(shè)備上,通過緩存訪問屬性要比反復訪問屬性要快將近20%的速度。在JIT的設(shè)備中,屬性訪問的花銷與本地訪問的花銷基本一致,所以這不是一項有多少價值的優(yōu)化手段,除非你覺得這樣做的話代碼更易讀(這對static,final,常量同樣適用)。
經(jīng)常估測
在開始優(yōu)化之前,要確保你有個問題需要解決:要確保你可以精準測量現(xiàn)有的性能,否則將不能觀察到優(yōu)化所帶來的提升。
基準點由Caliper的微型基準點框架創(chuàng)建。基準點很難正確獲得,所以Caliper將這份很難處理的工作做了,甚至是在你沒有在測量那些你想測量的地方的時候它也在工作。我們強烈的推薦你使用Caliper來創(chuàng)建自己的微型基準點。
你可能還發(fā)現(xiàn)Traceview非常有助于提升性能,不過你應(yīng)該意識到Traceview工作的時候JIT并沒有開啟。這會錯誤的認為JIT會將損失掉的時間彌補回來。這尤其重要:根據(jù)Traceview所更改的結(jié)果會使實際代碼運行的更快。
有關(guān)更多提升APP性能的工具及方法,請參見以下文檔:
- Profiling with Traceview and dmtracedump
- Analyzing UI Performance with Systrace
總結(jié)
以上是生活随笔為你收集整理的Android官方开发文档Training系列课程中文版:性能优化建议的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android官方开发文档Trainin
- 下一篇: 带你根据源码了解View的事件触发流程,