編輯:關於Android編程
如果你真的願意去努力,你人生最壞的結果,也不過是大器晚成。
原文鏈接:http://developer.android.com/training/articles/perf-tips.html
這篇文檔主要包含一些微小的最佳優化,當把這些組合起來的時候,可以提高App的整體性能,但它不太可能對性能造成戲劇性的影響。選擇合適的算法和數據結構應該是你優先考慮的內容,但超出了本文的范圍。這個文檔更適合作為通用的編碼技巧,通過這些技巧使我們的代碼更高效。
編寫高效代碼有兩個基本規則:
不執行不必要的操作
不分配不必要的內存
我們面臨的最棘手的一個問題是我們的Android應用程序肯定會在多種類型的硬件上運行。不同版本的VM虛擬機運行在不同的處理器的運行速度顯然是不同的。你甚至不能簡單地說“設備X快/慢於設備Y大概F倍”,然後想當然的認為其在他設備上這個倍數也是成立的。但是,我們僅僅能從模擬器獲得很少關於設備性能的信息,它與真機還是有很大不同的。還有,有或沒有JIT(即時編譯)的設備之間存在巨大差異:在有JIT的設備上完美運行的代碼在沒有JIT的設備上並不一定能順暢運行。
我們要盡量通過優化性能來確保我們的應用在各種各樣的設備上都表現良好,確保代碼在支持的不同版本上是有效的。
對象創建永遠不會是免費的。雖然一個帶有線程池的垃圾回收器(garbage collector)可以使分配臨時對象的內存占用降低,但分配內存總是比不分配內存更昂貴的。
當我們在我們的應用分配更多的對象時,將迫使一個周期性的垃圾回收,創建“打嗝”的用戶體驗,即卡頓。在Android 2.3中引入的並發垃圾收集器(GC)可以幫助我們,但不必要的工作能避免要盡量避免。
因此,這樣我們應該避免創建我們不需要的對象,以下是一些實例:
如果你有一個方法返回一個字符串,你知道它的結果需要附加到StringBuffer上,改變你的實現方法使它可以直接添加,而不是創建一個短暫的臨時對象。 當要從一組輸入數據中提取字符串時,試著返回一個原始數據的子字符串(substring),而不是創建一個重復的對象。使用substring的方式,你將創建一個新的String對象,但它將與原數據共享內部char[]空間的。(如果你只使用原始輸入的一小部分,無論怎麼操作,你都會把它保持在內存中,這點需要權衡)一個更激進的想法是把一個多維數組分割成平行的一維數組:
int類型的數組比Integer對象數組要好,由此可以知道兩個平行的int數組比(int,int)二維數組效率更高。這同樣適用於所有的原始數據類型。 如果你需要實現一個容器存儲(Foo,Bar)對象的,試著記住兩個平行的Foo[]和Bar[]數組通常比單一陣列的自定義(Foo,Bar)對象要好。(但是有例外,比如當你設計一個API訪問其他代碼的時候。在這些情況下,通常可以犧牲小部分的性能達到良好的API設計。但在自己的內部代碼,我們應該試著盡可能的提高效率。)一般來說,避免創建臨時對象。更少的對象創建意味著更少的垃圾收集,這對用戶體驗有直接的影響。
如果你不需要訪問一個對象的字段,可以使用static修飾你的方法。調用將快15%~20%。這是一種很好的做法,因為通過static你還可以知道調用的方法不能改變對象的狀態。
像這樣在一個類的頂部聲明常量:
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 constant”指令,而不是查表。
Notes:這個優化方法只對原始類型和String類型有效,而不是任意引用類型。不過,在必要時使用static final是個很好的習慣。
像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可以為你內聯訪問.
增強的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()。
Tips:你還可以參考 Josh Bloch 的 《Effective Java》這本書的第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”。
問題是,VM(虛擬機)因為Foo和Foo
/*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中你不能這樣做。
Android系統中float類型的數據存取速度是int類型的一半,盡量優先采用int類型。
就速度而言,現代硬件上,float 和 double 的速度是一樣的。空間而言,double 是兩倍float的大小。在空間不是問題的情況下,你應該使用 double 。
同樣,對於整型,有些處理器實現了硬件幾倍的乘法,但是沒有除法。這時,整型的除法和取余是在軟件內部實現的,這在你使用哈希表或大量計算操作時要考慮到。
除了那些常見的讓你多使用自帶庫函數的理由以外,記得系統函數有時可以替代第三方庫,並且還有匯編級別的優化,他們通常比帶有JIT的Java編譯出來的代碼更高效。典型的例子是:Android API 中的 String.indexOf(),Dalvik出於內聯性能考慮將其替換。同樣 System.arraycopy()函數也被替換,這樣的性能在Nexus One測試,比手寫的for循環並使用JIT還快9倍。
Tips:參見 Josh Bloch 的 《Effective Java》這本書的第47條
結合Android NDK使用native代碼開發,並不總是比Java直接開發的效率更好的。Java轉native代碼是有代價的,而且JIT不能在這種情況下做優化。如果你在native代碼中分配資源(比如native堆上的內存,文件描述符等等),這會對收集這些資源造成巨大的困難。你同時也需要為各種架構重新編譯代碼(而不是依賴JIT)。你甚至對已同樣架構的設備都需要編譯多個版本:為G1的ARM架構編譯的版本不能完全使用Nexus One上ARM架構的優勢,反之亦然。
Native 代碼是在你已經有本地代碼,想把它移植到Android平台時有優勢,而不是為了優化已有的Android Java代碼使用。
如果你要使用JNI,請學習JNI Tips
Tips:參見 Josh Bloch 的 《Effective Java》這本書的第54條
在沒有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優化的,所以實際的效果應該是要比測量的數據稍微好些。
關於如何測量與調試,還可以參考下面兩篇文章:
http://developer.android.com/tools/debugging/debugging-tracing.html http://developer.android.com/tools/debugging/systrace.html在上篇中 主要有學習到皮膚資源內置到應用程序中 的方式實現換膚的 基本思路,本篇將繼續以上篇的思路學習 皮膚資源內置的方式實現換膚效果、但本篇側重於應用中換膚功能的代碼設
下面詳細解釋這四大方式的特點第一種:文件存儲數據核心原理: Context提供了兩個方法來打開數據文件裡的文件IO流 FileInputStream openFileIn
java 代碼如下(簡單的知識點我會以注釋的形式講解):package com.gc.gridviewdemo; /** * @author Android將軍 */
通過央視報道,曝光了2個跟智能手機有關的內容,一個是手機偷跑流量,另外一個則是預裝軟件難卸載問題。通過測試獲取到實際數據對比,最後結果顯示,有9款手機的流量