標籤:android 效能 最佳實務 設計 安卓
Android應用應該要很快,更精確的說應該是要有效率。那就是說行動裝置環境中有限的計算能力和資料存放區,很小的螢幕,有限的電池壽命中要更有效率。這篇部落格我就會向你展示為效能而設計的最佳實務。
1. 避免建立對象對象的建立在android中開銷要比在java中大的多。盡量去避免建立一個對象,越多的對象意味著越多的記憶體回收,越多的記憶體回收意味著使用者會覺得有點“小卡”。一般的說,儘可能避免建立短暫的臨時變數,更少的對象建立意味著更少的記憶體回收,將會提升使用者體驗。如果能建立一個“池”來管理對象的話,那是最好的了。
2. 將阻塞操作從UI線程中剝離出來使用AsyncTask, Thread, IntentService,或者簡單的後台Service去做大開銷的操作。使用Loaders去簡化管理很長時間載入資料的狀態就像一個指標。如果一個操作需要消耗時間和資源,那就放到另外的進程中非同步進行,這樣你的程式就能夠繼續響應並且使用者也可以進行操作。
3. 使用Native方法同樣一個Java迴圈,C/C++代碼可以快10到100倍。
4. 實現優於介面假設你有一個HashMap對象,你可以使用通用的Map來聲明這個HashMap:
Map myMap1 = new HashMap();HashMap myMap2 = new HashMap();
哪一種更好?傳統的觀點認為你應該使用Map,因為它允許你更改為只要實現了Map介面的底層實現。傳統觀點對於常規編程是正確的,但是對於嵌入式系統來說就不是那麼好了。調用一個介面引用的方法相對於調用一個固定實現的引用要多花費2倍的時間。如果你HashMap能處理你要做的事情,那麼使用Map來申明就沒有一點價值了。讓IDE幫你重構你的代碼,就算你對代碼還沒有頭緒,使用Map也是沒什麼價值的。(當然,公用的API可能有不同:一個好的API肯定勝過小的效能問題)
5. 靜態方法更好如果你的方法不需要訪問一個對象的變數,那麼將你的方法改為靜態。調用起來會快很多,因為它將不需要經過virtual method table。這也是一個很好的實踐,因為你可以告訴方法簽名在調用方法的時候不會更改對象的狀態。
6. 避免使用內部的Getters/Setters在像C++這種語言中,經常會見到getters(比如 i=getCount())來代替直接存取變數(i=mCount)。這是一個非常好的習慣,因為編譯器會使用內聯訪問,而且如果你相對欄位做約束或者進行調試的時候,你可以隨時的修改代碼。在Android中,這是一個不好的想法。虛擬函數的調用開銷很大,遠遠超過了變數的訪問。如果是一個類,你應該直接存取變數,如果是一個公用介面,還是盡量遵循物件導向編程的實踐使用Getters/Setters。
7. 常量聲明Final考慮下面的變數聲明:
static int intVal = 42;static String strVal = "Hello, world!";
編譯器產生一個類的構造方法,叫<clinit>,它會在類第一次使用的時候觸發。方法會將42儲存到intVal,從String表裡擷取一個引用給strVal。當這些值被引用後,他們訪問時就能夠去尋找了。
我們可以使用final關鍵字來提升效能:
static final int intVal = 42;static final String strVal = "Hello, world!";
類檔案不再需要<clinit>方法了,因為常量轉為了虛擬機器進行處理。代碼訪問intVal的時候直接就能拿到42,訪問strVal的時候開銷也會相對更小。
聲明類或者方法final並不能獲得效能上的好處,不夠能夠有其他的效果。比如,如果不想讓子類重寫getter方法,那麼就可以聲明final。
你也可以本地變數final。然而這不會有任何的效能效益。對於本地變數,使用final僅僅能讓代碼語義更加清晰(或者你可以讓匿名類訪問到這個變數)。
8. 小心使用增強for迴圈增強行的for迴圈(也可以說是for-each迴圈)可以使用在任何實現了iterable介面的集合上。對於這些對象,iterator會調用hasNext()和next()介面來建立,對於ArrayList,你最好避開這種方式,不過對於其他種類的集合,增強型for迴圈和顯式的使用迭代迴圈相差無幾。
下面代碼展示了增強型的for迴圈:
public class Foo { int mSplat; static Foo mArray[] = new Foo[27]; public static void zero() { int sum = 0; for (int i = 0; i < mArray.length; i++) { sum += mArray[i].mSplat; } } public static void one() { int sum = 0; Foo[] localArray = mArray; int len = localArray.length; for (int i = 0; i < len; i++) { sum += localArray[i].mSplat; } } public static void two() { int sum = 0; for (Foo a: mArray) { sum += a.mSplat; } }}
zero()檢索static變數兩次,而且每次迴圈都會擷取數組的長度。
one()將所有東西就放到了臨時變數中,避免了尋找。
two()使用1.5版本java的增強型for迴圈,由編譯器來產生代碼,複製數組引用和數組長度到本地變數,產生一個好的訪問數組方案,它會產生一個額外的本地載入儲存(儲存a這個對象),它會比one()要慢一點點。
總結就是:增強型for迴圈有更好的語義和代碼結構,但是要謹慎使用它,因為有可能會有額外的建立對象開銷。
9. 避免Enums枚舉非常的方便,但是不幸的是他的速度的大小都是讓人痛苦的。比如說:
public class Foo { public enum Shrubbery { GROUND, CRAWLING, HANGING }}
會編譯成900byte的.class檔案 (Foo$Shrubbery.class)。當第一次使用,類會調用初始化函數<init>來對每一個枚舉值建立對象。每一個對象都有自己的靜態變數,而且全部都儲存在一個數組中(一個叫"$VALUES"的靜態變數)。這麼多的代碼,僅僅只是為了三個整數。
下面這句代碼:
Shrubbery shrub = Shrubbery.GROUND;
一個靜態變數的尋找。如果"GROUND"是一個靜態int常量,編譯器會把它當做一個已知常量並且使用內聯。
從另一個方面說,你當然會從枚舉類型中獲得很多好用的API和編譯時間的檢查。所以,通常需要這樣來權衡:你應該在所有公用API的地方盡量使用枚舉類型,不過在效能重要的時候,盡量避免使用。
在一些環境中,通過ordinal()方法來擷取enum的數值很有用,比如:
for (int n = 0; n < list.size(); n++) { if (list.items[n].e == MyEnum.VAL_X) // do stuff 1 else if (list.items[n].e == MyEnum.VAL_Y) // do stuff 2}
然後:
int valX = MyEnum.VAL_X.ordinal();int valY = MyEnum.VAL_Y.ordinal();int count = list.size();MyItem items = list.items();for (int n = 0; n < count; n++){ int valItem = items[n].e.ordinal(); if (valItem == valX) // do stuff 1 else if (valItem == valY) // do stuff 2}
在一些案例中,這將會更快,儘管這沒有保證。
10. 對內部類使用Package存取權限考慮一下下面的類定義:
public class Foo { 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); } private class Inner { void stuff() { Foo.this.doStuff(Foo.this.mValue); } }}
關鍵的事事:我們定義了一個內部類(Foo$Inner)直接存取了外部類的private方法和private執行個體。這是合法的,而且代碼列印出了我們期望的"Value is 27"。
問題是Foo$Inner在技術上來說,是一個完全獨立的類。它來直接存取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方法。這意味著上面的代碼真正的情況是,你是通過的訪問器訪問而不是通過直接存取的代碼。之前我們就討論過了使用訪問器要比直接存取變數更慢,所以這就是一個特定的語言習慣形成的一種"隱形"的效能影響。
我們可以通過聲明package訪問空間而不是private訪問空間來避免這個問題。這將會啟動並執行更快,而且避免了產生方法產生的開銷。(不幸的是,這可能讓在同一個package下的其他包能夠直接存取這個變數,這違反了物件導向的設計思想。再次說明一下,如果你要設計一個公用的API,你就要仔細考慮一下了。)
11. 避免Float在奔騰CPU發布之前,幾乎所有的遊戲開發人員都會盡量使用整數運算。奔騰CPU將浮點數副處理器設定成了內建功能。通過交叉整數和浮點操作使遊戲比以前純整數運算的時候要快。傳統型程式現在常見的做法是隨意使用float。
不幸的是,嵌入式處理器通常沒有浮點支援,所以所有的float和double操作開銷都很大。一些基本的浮點操作可以在一毫秒的順序完成。
同樣,即使是整數,一些晶片支援乘法,但缺乏除法。在這種情況下,在軟體執行整數除法和模量操作時,想想如果你設計一個雜湊表或做大量的數學。
12. 一些簡單的效能數字為了說明我們的一些想法,我們對一些基本的行為列出了近似已耗用時間。請注意,這些值不應被視為絕對數字:他們是CPU和時鐘時間的組合,並且對於系統的改進將變化。值得注意的是這些值是相對於彼此——例如,添加一個成員變數目前需要的時間大約是添加一個本地變數的四倍。
| Action |
Time |
| Add a local variable |
1 |
| Add a member variable |
4 |
| Call String.length() |
5 |
| Call empty static native method |
5 |
| Call empty static method |
12 |
| Call empty virtual method |
12.5 |
| Call empty interface method |
15 |
| Call Iterator:next() on a HashMap |
165 |
| Call put() on a HashMap |
600 |
| Inflate 1 View from XML |
22,000 |
| Inflate 1 LinearLayout containing 1 TextView |
25,000 |
| Inflate 1 LinearLayout containing 6 View objects |
100,000 |
| Inflate 1 LinearLayout containing 6 TextView objects |
135,000 |
| Launch an empty activity |
3,000,000 |
13. 總結寫出好的、有效代碼的最好的方式,就是去理解你的代碼到底作了什麼。如果你真的想要在List上通過迭代器使用增強for迴圈來訪問;讓它成為一個深思熟慮的選擇,而不是成為副作用。俗話說有備無患!知道你引入了什麼,插入一些你最喜歡的東西混合在一起也可以,不過必須謹慎考慮你的代碼在做什麼,然後找機會去提升它的速度。
Android程式效能設計最佳實務