標籤:
Google效能點滴
http://developer.android.com/intl/zh-cn/training/articles/perf-tips.html
本文主要涉及一些小最佳化,組合使用可以提升App整體效能,但不會顯著的提升效能。提升效能首選合適的演算法和資料結構,這超出了本文的範疇。這裡的技巧應該作為你平時寫代碼的習慣,以便寫出高效的代碼。
高效的代碼有兩個基本的規則:
最佳化Android程式的時候需要面對不同的硬體、不同版本的VM,不同的處理器,不同的速度!模擬器上的測試很少涉及效能。是否擁有JIT影響也很大。
避免建立不必要的對象
對象建立不是免費的。垃圾收集也是需要成本的。
如果方法返回String,結果總會被添加到StringBuffer,應該修改函數實現簽名不建立臨時對象直接附加 。
當從一組輸入資料中抽取字串的時候,返回substring而不是建立拷貝. 這樣雖然建立了新的String對象,但是它和輸入資料共用了char[](弊端是如果僅僅使用來源資料的一小部分,會導致整個資料無法回收。)。
一些更激進的做法是把多維陣列切割成多個一維數組:int[]比Integer[]更高效。
使用Static而不是Virtual
如果不需要訪問對象的成員,把方法聲明成 static,調用速度將提高15%-20%。這是一個很好的習慣,因為從方法簽名上可以辨別出這個方法調用不會影響對象的狀態。
使用Static Final定義常量
static int intVal = 42;static String strVal = "Hello, world!";
編譯器會產生一個類初始化方法 <clinit>,類首次使用的時候調用。該方法把42存入了intVal,並且從類檔案字串常量表中把引用賦給strVal。後面使用這些值時,用欄位尋找(field lookup)的方式。
加上final :
static final int intVal = 42;static final String strVal = "Hello, world!";
這樣類不再需要一個 <clinit> 方法,因為常量會進入dex檔案的靜態欄位初始化器(static field initializers)。引用intVal會被42直接代替。
Note: 這項最佳化僅對基本類型和String類型有效,而不是任意的參考型別。但是在聲明常量的時候加上static final首碼依然是個好習慣。
避免內部Getters/Setters
C++之類的原生語言常使用getters( i = getCount() )替換成員訪問( i = mCount)是, 這樣編譯器可以內聯訪問,控制存取權限。但Android上虛方法(virtual method)的訪問比起成員尋找(field lookup)還貴。
沒有JIT的成員訪問比用getter方法訪問大約快3倍。有JIT訪問成員變數和訪問本地變數一樣廉價,成員訪問大約比getter訪問速度快7倍。
注意ProGuard可以內聯代碼。
使用增強版For迴圈
增強版For迴圈(也叫做 "for-each" 迴圈) 可以用來枚舉實現了Iterable介面的集合和數組。集合分配迭代器(iterator)對象來實現 hasNext() 和 next() 方法。 ArrayList手寫的帶計數器的迴圈要快3 倍,其它集合則效率差不多。
下面是幾種常見迭代數組的情境:
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迴圈。
使用包級訪問而不是內部類的私人訪問
對於下面的代碼
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),直接存取了外層類的私人方法和私人成員。對於虛擬機器來說 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);}
每次Foo$Inner訪問Foo的私人成員或者私人方法時,表面上是直接調用,但是實際上是通過這些靜態存取方法訪問的,方法調用會比成員的直接調用慢很多。定義成員變數的時候最好改成 public 或者 default, 但是這樣又會暴露類的成員,所以在公開的API裡面不要這麼做。
避免使用浮點數
Android 裝置上float比int會慢2倍。
在現在的硬體裝置上float和double類型速度上沒有差別,但後者佔用了2倍的空間,對於台式機空間並不是問題,但Android是個問題。
另外,對於整型數有些機器是有硬體乘法的,但是卻沒有除法。除法或者取餘運算都是軟體實現的,如果你在設計雜湊表或者做了很多數學運算的時候需要考慮這些。
使用庫函數
優先考慮庫(library)中提供的代碼,有時系統可以使用彙編代碼(hand-coded assembler)來替換庫方法,比使用JIT更高效,比如String.indexOf() 和相關的API,Dalvik會使用內聯實現。同樣的 System.arraycopy() 在帶有JIT的nexus裝置上比手寫迴圈快9倍。
謹慎使用native函數
使用基於NDK的原生代碼未必比Java更快,因為在java-native之前的轉換也是需要成本的,而且JIT也無法最佳化這種邊界情況。如果你使用了本地資源(native-resources,記憶體、檔案或其它),資源的及時回收將會變得更加困難。而且你還要把本地代碼編譯成各種架。NDK一般是用來遷移原有的代碼到Android平台的,並不是為了“加速”Java。
如果的確需要使用native代碼,看看這裡 JNI Tips。
關於效能的誤區
在沒有JIT的裝置上,通過具體的類型調用方法比使用介面來調用方法更快(比如調用 HashMap map 比 Map map 更快,雖然他們實際上都是調用 HashMap 的方法)。之前有說大約會慢1倍,但是事實上只是慢了 6% 左右,而在JIT裝置上幾乎就沒有區別了。
在沒有JIT的裝置商,訪問緩衝成員大概比重複的訪問快20%,但是對於JIT,成員訪問和本地變數幾乎沒有差別,所以這項最佳化沒有太大的意思,除非這樣讓你的代碼可讀性更好。(同樣適用於用static、final和 static final修飾的變數)
持續度量
在你開始最佳化之前,請確保你有效能問題需要解決,確保準確的衡量當前的效能,否則你無法弄清最佳化帶來了多少效能的提升。
這些標準測試是基於Java版的 Caliper 微測試(Microbenchmarks)架構。Microbenchmarks 很難做到準確,所以 Caliper 幫我們做了這些工作,甚至會檢測到一些我們認為測試了但是沒有測試的情況(因為VM對代碼做了最佳化)。強烈建議使用 Caliper 來運行自己的微測試程式。
使用 Traceview 也是很有用的,需要記住的是目前 Traceview 是不啟動JIT的,這樣它會錯估JIT帶來的時間最佳化。按照Traceview的資料做出變動之後,實際的代碼在沒有Traceview的時候可以啟動並執行更快。
安卓效能最佳化