標籤:
在前兩篇文章當中,我們主要學習了Android記憶體方面的相關知識,包括如何合理地使用記憶體,以及當發生記憶體泄露時如何定位出問題的原因。那麼關於記憶體的知識就討論到這裡,今天開始我們將學習一些效能編碼最佳化的技巧。
這裡先事先提醒大家一句,本篇文章中討論的編碼最佳化技巧都是屬於一些“微最佳化”,也就是說即使我們都按照本篇文章的技巧來最佳化代碼,在效能方面也是看不出有什麼顯著的提升的。使用合適的演算法與資料結構將永遠是你最佳化程式效能的最主要手段,但本篇文章中不會討論這一塊的內容。因此,這裡我們即將學習的並不是什麼靈丹妙藥,而是大家應該把這些技巧當作一種好的編碼規範,我們在平時寫代碼時就可以潛移默化地使用這些編碼規範,不僅能夠在微觀層面提升程式一定的效能,也可以讓我們的代碼變得更加專業,下面就讓我們來一起學習一下這些技巧。
避免建立不必要的對象
建立對象從來都不應該是一件隨意的事情,因為建立一個對象就意味著記憶體回收行程需要回收一個對象,而這兩步操作都是需要消耗時間的。雖說建立一個對象的代價確實非常小,並且Android 2.3版本當中又增加了並發記憶體回收行程機制(詳見 Android最佳效能實踐(二)——分析記憶體的使用方式),這讓GC操作時的停頓時間也變得難以察覺,但是這些理由都不足以讓我們可以肆意地建立對象,需要建立的對象我們自然要建立,但是不必要的對象我們就應該盡量避免建立。
下面來看一些我們可以避免建立對象的情境:
如果我們有一個需要拼接的字串,那麼可以優先考慮使用StringBuffer或者StringBuilder來進行拼接,而不是加號串連符,因為使用加號串連符會建立多餘的對象,拼接的字串越長,加號串連符的效能越低。
在沒有特殊原因的情況下,盡量使用基本資料類來代替封裝資料類型,int比Integer要更加高效,其它資料類型也是一樣。
當一個方法的傳回值是String的時候,通常可以去判斷一下這個String的作用是什麼,如果我們明確地知道調用方會將這個返回的String再進行拼接操作的話,可以考慮返回一個StringBuffer對象來代替,因為這樣可以將一個對象的引用進行返回,而返回String的話就是建立了一個短生命週期的臨時對象。
正如前面所說,基礎資料型別 (Elementary Data Type)要優於對象資料類型,類似地,基礎資料型別 (Elementary Data Type)的數組也要優於對象資料類型的數組。另外,兩個平行的數組要比一個封裝好的對象數組更加高效,舉個例子,Foo[]和Bar[]這樣的兩個數組,使用起來要比Custom(Foo,Bar)[]這樣的一個數組高效得多。
當然上面所說的只是一些代表性的例子,我們所要遵守的一個基本原則就是儘可能地少建立臨時對象,越少的對象意味著越少的GC操作,同時也就意味著越好的程式效能和使用者體驗。
靜態優於抽象
如果你並不需要訪問一個對象中的某些欄位,只是想調用它的某個方法來去完成一項通用的功能,那麼可以將這個方法設定成靜態方法,這會讓調用的速度提升15%-20%,同時也不用為了調用這個方法而去專門建立對象了,這樣還滿足了上面的一條原則。另外這也是一種好的編程習慣,因為我們可以放心地調用靜態方法,而不用擔心調用這個方法後是否會改變對象的狀態(靜態方法內無法訪問非靜態欄位)。
對常量使用static final修飾符
我們先來看一下在一個類的最頂部定義如下代碼:
| 12 |
static int intVal = 42;static String strVal = "Hello, world!"; |
編譯器會為上述代碼產生一個初始化方法,稱為<clinit>方法,該方法會在定義類第一次被使用的時候調用。然後這個方法會將42的值賦值到intVal當中,並從字串常量表中提取一個引用賦值到strVal上。當賦值完成後,我們就可以通過欄位搜尋的方式來去訪問具體的值了。
但是我們還可以通過final關鍵字來對上述代碼進行最佳化:
| 12 |
static final int intVal = 42;static final String strVal = "Hello, world!"; |
經過這樣修改之後,定義類就不再需要一個<clinit>方法了,因為所有的常量都會在dex檔案的初始化器當中進行初始化。當我們調用intVal時可以直接指向42的值,而調用strVal時會用一種相對輕量級的字串常量方式,而不是欄位搜尋的方式。
另外需要大家注意的是,這種最佳化方式只對基礎資料型別 (Elementary Data Type)以及String類型的常量有效,對於其它資料類型的常量是無效的。不過,對於任何常量都是用static final的關鍵字來進行聲明仍然是一種非常好的習慣。
使用增強型for迴圈文法
增強型for迴圈(也被稱為for-each迴圈)可以用於去遍曆實現Iterable介面的集合以及數組,這是jdk 1.5中新增的一種迴圈模式。當然除了這種新增的迴圈模式之外,我們仍然還可以使用原有的普通迴圈模式,只不過它們之間是有效率區別的,我們來看下面一段代碼:
| 12345678910111213141516171819202122232425262728 |
static class Counter { int mCount;} Counter[] mArray = ... public void zero() { int sum = 0; for (int i = 0; i < mArray.length; ++i) { sum += mArray[i].mCount; }} public void one() { int sum = 0; Counter[] localArray = mArray; int len = localArray.length; for (int i = 0; i < len; ++i) { sum += localArray[i].mCount; }} public void two() { int sum = 0; for (Counter a : mArray) { sum += a.mCount; }} |
可以看到,上述代碼當中我們使用了三種不同的迴圈方式來對mArray中的所有元素進行求和。其中zero()方法是最慢的一種,因為它是把mArray.length寫在迴圈當中的,也就是說每迴圈一次都需要重新計算一次mArray的長度。而one()方法則相對快得多,因為它使用了一個局部變數len來記錄數組的長度,這樣就省去了每次迴圈時欄位搜尋的時間。two()方法在沒有JIT(Just In Time Compiler)的裝置上是運行最快的,而在有JIT的裝置上運行效率和one()方法不相上下,唯一需要注意的是這種寫法需要JDK 1.5之後才支援。
但是這裡要跟大家提一個特殊情況,對於ArrayList這種集合,自己手寫的迴圈要比增強型for迴圈更快,而其他的集合就沒有這種情況。因此,對於我們來說,預設情況下可以都使用增強型for迴圈,而遍曆ArrayList時就還是使用傳統的迴圈方式吧。
多使用系統封裝好的API
Java語言當中其實給我們提供了非常豐富的API介面,我們在編寫程式時如果可以使用系統提供的API就應該盡量使用,系統提供的API完成不了我們需要的功能時才應該自己去寫,因為使用系統的API在很多時候比我們自己寫的代碼要快得多,它們的很多功能都是通過底層的彙編模式執行的。
比如說String類當中提供的好多API都是擁有極高的效率的,像indexOf()方法和一些其它相關的API,雖說我們通過自己編寫演算法也能夠完成同樣的功能,但是效率方面會和這些方法差的比較遠。這裡舉個例子,如果我們要實現一個數組拷貝的功能,使用迴圈的方式來對數組中的每一個元素一一進行賦值當然是可行的,但是如果我們直接使用系統中提供的System.arraycopy()方法將會讓執行效率快9倍以上。
避免在內部調用Getters/Setters方法
我們平時寫代碼時都被告知,一定要使用物件導向的思維去寫代碼,而物件導向的三大特性我們都知道,封裝、多態和繼承。其中封裝的基本思想就是不要把類內部的欄位暴漏給外部,而是提供特定的方法來允許外部操作相應類的內部欄位,從而在Java語言當中就出現了Getters/Setters這種封裝技巧。
然而在Android上這個技巧就不再是那麼的受推崇了,因為欄位搜尋要比方法調用效率高得多,我們直接存取某個欄位可能要比通過getters方法來去訪問這個欄位快3到7倍。不過我們肯定不能僅僅因為效率的原因就將封裝這個技巧給拋棄了,編寫代碼還是要按照物件導向思維的,但是我們可以在能最佳化的地方進行最佳化,比如說避免在內部調用getters/setters方法。
那什麼叫做在內部調用getters/setters方法呢?這裡我舉一個非常簡單的例子:
| 123456789101112131415161718 |
public class Calculate { private int one = 1; private int two = 2; public int getOne() { return one; } public int getTwo() { return two; } public int getSum() { return getOne() + getTwo(); }} |
可以看到,上面是一個Calculate類,這個類的功能非常簡單,先將one和two這兩個欄位進行了封裝,然後提供了getOne()方法擷取one欄位的值,提供了getTwo()方法擷取two欄位的值,還提供了一個getSum()方法用於擷取總和的值。
這裡我們注意到,getSum()方法當中的演算法就是將one和two的值相加進行返回,但是它擷取one和two的值的方式也是通過getters方法進行擷取的,其實這是一種完全沒有必要的方式,因為getSum()方法本身就是Calculate類內部的方法,它是可以直接存取到Calculate類中的封裝欄位的,因此這種寫法在Android上是不推崇的,我們可以進行如下修改:
| 123456789101112 |
public class Calculate { private int one = 1; private int two = 2; ...... public int getSum() { return one + two; }} |
改成這種寫法之後,我們就避免了在內部調用getters/setters方法,而對於外部而言Calculate類仍然是具有很好的封裝性的。
http://www.androidchina.net/1961.html#rd?sukey=fc78a68049a14bb25feaa125f45f038b5eee1640d4fb19c886789710713c8e21474b702046947fc4bc660b2c685cb70a
Android最佳效能實踐(三)——高效能編碼最佳化