標籤:com 集中 情況 sha iterator objects 平台 iterable promise
主要介紹一些小細節的最佳化技巧,雖然這些小技巧不能較大幅度的提升應用效能,但是恰當的運用這些小技巧並發生累積效應的時候,對於整個App的效能提升還是有不小作用的。通常來說,選擇合適的演算法與資料結構會是你首要考慮的因素,在這篇文章中不會涉及這方面的知識點。你應該使用這篇文章中的小技巧作為平時寫代碼的習慣,這樣能夠提升代碼的效率。
通常來說,高效的代碼需要滿足下面兩個原則:
不要做冗餘的工作
盡量避免執行過多的記憶體配置操作
To ensure your app performs well across a wide variety of devices, ensure your code is efficient at all levels and agressively optimize your performance. 目標:為了確保App在各裝置上都能良好運行,就要確保你的代碼在不同檔次的裝置上都儘可能的最佳化。
以下方法可以用於最佳化代碼:
方法一: 避免建立不必要的對象
Object creation is never free. 建立對象從來不是免費的。Generational GC(with per-thread allocation pools)可以使臨時對象的分配變得廉價一些,但是執行分配記憶體總是比不執行分配操作更昂貴。隨著你在App中分配更多的對象,你可能需要強制gc,而gc操作會給使用者體驗帶來一點點卡頓。雖然從Android 2.3開始,引入了並發gc,它可以協助你顯著提升gc的效率,減輕卡頓,但畢竟不必要的記憶體配置操作還是應該盡量避免。
因此請盡量避免建立不必要的對象,有下面一些例子來說明這個問題:
If you have a method returning a string, and you know that its result will always be appended to a StringBuffer anyway, change your signature and implementation so that the function does the append directly, instead of creating a short-lived temporary object. 如果你需要返回一個String對象,並且你知道它最終會需要串連到一個StringBuffer,請修改你的函數實現方式,避免直接進行串連操作,應該採用建立一個臨時對象來做字串的拼接這個操作。
When extracting strings from a set of input data, try to return a substring of the original data, instead of creating a copy. You will create a new String object, but it will share the char[] with the data. (The trade-off being that if you‘re only using a small part of the original input, you‘ll be keeping it all around in memory anyway if you go this route.) 當從已經存在的資料集中抽取出String的時候,嘗試返回原資料的substring對象,而不是建立一個重複的對象。使用substring的方式,你將會得到一個新的String對象,但是這個string對象是和原string共用內部char[]空間的。
一個稍微激進點的做法是把所有多維的資料分解成一維的數組:
An array of ints is a much better than an array of Integer objects, but this also generalizes to the fact that two parallel arrays of ints are also a lot more efficient than an array of (int,int) objects. The same goes for any combination of primitive types. 一組int資料要比一組Integer對象要好很多。可以得知,兩組一維數組要比一個二維數組更加的有效率。同樣的,這個道理可以推廣至其他未經處理資料類型。
If you need to implement a container that stores tuples of (Foo,Bar) objects, try to remember that two parallel Foo[] and Bar[] arrays are generally much better than a single array of custom (Foo,Bar) objects. (The exception to this, of course, is when you‘re designing an API for other code to access. In those cases, it‘s usually better to make a small compromise to the speed in order to achieve a good API design. But in your own internal code, you should try and be as efficient as possible.) 如果你需要實現一個數組用來存放(Foo,Bar)的對象,記住使用Foo[]與Bar[]要比(Foo,Bar)好很多。(例外的是,為了某些好的API的設計,可以適當做一些妥協。但是在自己的代碼內部,你應該多多使用分解後的容易)
通常來說,需要避免建立更多的臨時對象。更少的對象意味者更少的gc動作,gc會對使用者體驗有比較直接的影響。
方法二:使用static而不是virtual
如果你不需要訪問一個對象的值,請保證這個方法是static類型的,這樣方法調用將快15%-20%。這是一個好的習慣,因為你可以從方法聲明中得知調用無法改變這個對象的狀態。
If you don‘t need to access an object‘s fields, make your method static. Invocations will be about 15%-20% faster. It‘s also good practice, because you can tell from the method signature that calling the method can‘t alter the object‘s state.
方法三:常量聲明為static final
考慮下述聲明:
static int intVal = 42;
static String strVal = "Hello, world!";
編譯器會使用一個初始化類的函數(名為:clinit),然後當類第一次被使用的時候執行。這個函數將42存入intVal,還從class檔案的常量表中提取了strVal的引用。當之後使用intVal或strVal的時候,他們會直接被查詢到。
可以使用final聲明來進行最佳化:
static final int intVal = 42;
static final String strVal = "Hello, world!";
這時再也不需要上面的方法了,因為final聲明的常量進入了靜態dex檔案的域初始化部分。調用intVal的代碼會直接使用42,調用strVal的代碼也會使用一個相對廉價的“字串常量”指令,而不是查表。
需要注意的是:這個最佳化方法只對原始類型和String類型有效,而不是任意參考型別。不過,在必要時使用static final是個很好的習慣。
方法四:避免內部的Getters/Setters
像C++等native language,通常使用getters(i = getCount())而不是直接存取變數(i = mCount)。這是編寫C++的一種優秀習慣,而且通常也被其他物件導向的語言所採用,例如C#與Java,因為編譯器通常會做inline訪問,而且你需要限制或者調試變數,你可以在任何時候在getter/setter裡面添加代碼。
然而,在Android上,這不是一個好的寫法。虛函數的調用比起直接存取變數要耗費更多。在物件導向編程中,將getter和setting暴露給公用介面是合理的,但在類內部應該僅僅使用域直接存取。
在沒有JIT(Just In Time Compiler)時,直接存取變數的速度是調用getter的3倍。有JIT時,直接存取變數的速度是通過getter訪問的7倍。
請注意,如果你使用ProGuard,你可以獲得同樣的效果,因為ProGuard可以為你inline accessors.
方法五:使用增強for迴圈
增強For迴圈(也被稱為 for-each 迴圈)可以被用在實現了 Iterable 介面的 collections 以及數組上。使用collection的時候,Iterator會被分配,用於for-each調用hasNext()和next()方法。使用ArrayList時,手寫的計數式for迴圈會快3倍(不管有沒有JIT),但是對於其他collection,增強for-each迴圈寫法會和迭代器寫法的效率一樣。
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()是差不多一樣快的。它使用了增強迴圈方法for-each。
所以請盡量使用for-each的方法,但是對於ArrayList,請使用方法one()。
為什麼ArrayList類中存在內部類:ArrayListIterator。該內部類聲明如下:
private class ArrayListIterator implements Iterator<E> { /** Number of elements remaining in this iteration */ private int remaining = size; /** Index of element that remove() would remove, or -1 if no such elt */ private int removalIndex = -1; /** The expected modCount value */ private int expectedModCount = modCount; public boolean hasNext() { return remaining != 0; } ......
方法六:使用包級訪問而不是私人的內部類訪問
考慮如下代碼:
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"。問題是,VM因為Foo和Foo$Inner是不同的類,會認為在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()函數時,它都會調用這些靜態方法。這意味著,上面的代碼可以歸結為,通過accessor函數來訪問成員變數。早些時候我們說過,通過accessor會比直接存取域要慢。所以,這是一個特定語言用法造成效能降低的例子。
如果你正在效能熱區(hotspot:高頻率、重複執行的程式碼片段)使用像這樣的代碼,你可以把內部類需要訪問的域和方法聲明為包級訪問,而不是私人存取權限。不幸的是,這意味著在相同包中的其他類也可以直接存取這些域,所以在公開的API中你不能這樣做。
方法七:避免使用float類型
Android系統中float類型的資料存取速度是int類型的一半,盡量優先採用int類型。就速度而言,現代硬體上,float 和 double 的速度是一樣的。空間而言,double 是兩倍float的大小。在空間不是問題的情況下,你應該使用 double 。同樣,對於整型,有些處理器實現了硬體幾倍的乘法,但是沒有除法。這時,整型的除法和取餘是在軟體內部實現的,這在你使用雜湊表或大量計算操作時要考慮到。
方法八:使用庫函數
除了那些常見的讓你多使用內建庫函數的理由以外,記得系統函數有時可以替代第三方庫,並且還有彙編層級的最佳化,他們通常比帶有JIT的Java編譯出來的代碼更高效。典型的例子是:Android API 中的 String.indexOf(),Dalvik出於內聯效能考慮將其替換。同樣 System.arraycopy()函數也被替換,這樣的效能在Nexus One測試,比手寫的for迴圈並使用JIT還快9倍。
方法九:謹慎使用Native方法
結合Android NDK使用native代碼開發,並不總是比Java直接開發的效率更好的。Java轉native代碼是有代價的,而且JIT不能在這種情況下做最佳化。如果你在native代碼中分配資源(比如native堆上的記憶體,檔案描述符等等),這會對收集這些資源造成巨大的困難。你同時也需要為各種架構重新編譯代碼(而不是依賴JIT)。你甚至對已同樣架構的裝置都需要編譯多個版本:為G1的ARM架構編譯的版本不能完全使用Nexus One上ARM架構的優勢,反之亦然。
Native 代碼是在你已經有本地代碼,想把它移植到Android平台時有優勢,而不是為了最佳化已有的Android Java代碼使用。
如果你要使用JNI,請學習JNI Tips。
方法十:效能最佳化誤區
在沒有JIT的裝置上,使用一種確切的資料類型確實要比抽象的資料類型速度要更有效率(例如,調用HashMap map要比調用Map map效率更高)。有誤傳效率要高一倍,實際上只是6%左右。而且,在JIT之後,他們直接並沒有大多差異。
在沒有JIT的裝置上,讀取緩衝域比直接讀取實際資料大概快20%。有JIT時,域讀取和本地讀取基本無差。所以最佳化並不值得除非你覺得能讓你的代碼更易讀(這對 final, static, static final 域同樣適用)。
方法十一:擷取效能最佳化的基準
在最佳化之前,你應該確定你遇到了效能問題。你應該確保你能夠準確測量出現在的效能,否則你也不會知道最佳化是否真的有效。
本章節中所有的技巧都需要Benchmark(基準測試)的支援。Benchmark可以在 code.google.com "dalvik" project中找到。
Benchmark是基於Java版本的 Caliper microbenchmarking架構開發的。Microbenchmarking很難做準確,所以Caliper幫你完成這部分工作,甚至還幫你測了你沒想到需要測量的部分(因為,VM幫你管理了代碼最佳化,你很難知道這部分最佳化有多大效果)。我們強烈推薦使用Caliper來做你的基準微測工作。
我們也可以用Traceview 來測量,但是測量的資料是沒有經過JIT最佳化的,所以實際的效果應該是要比測量的資料稍微好些。
Android應用程式效能最佳化Tips