Android最佳效能實踐(3):高效能編碼優化

發表於2015-08-17

在前兩篇文章當中,我們主要學習了Android記憶體方面的相關知識,包括如何合理地使用記憶體,以及當發生記憶體洩露時如何定位出問題的原因。那麼關於記憶體的知識就討論到這裡,今天開始我們將學習一些效能編碼優化的技巧。

這裡先事先提醒大家一句,本篇文章中討論的編碼優化技巧都是屬於一些“微優化”,也就是說即使我們都按照本篇文章的技巧來優化程式碼,在效能方面也是看不出有什麼顯著的提升的。使用合適的演算法與資料結構將永遠是你優化程式效能的最主要手段,但本篇文章中不會討論這一塊的內容。因此,這裡我們即將學習的並不是什麼靈丹妙藥,而是大家應該把這些技巧當作一種好的編碼規範,我們在平時寫程式碼時就可以潛移默化地使用這些編碼規範,不僅能夠在微觀層面提升程式一定的效能,也可以讓我們的程式碼變得更加專業,下面就讓我們來一起學習一下這些技巧。

避免建立不必要的物件

建立物件從來都不應該是一件隨意的事情,因為建立一個物件就意味著垃圾回收器需要回收一個物件,而這兩步操作都是需要消耗時間的。雖說建立一個物件的代價確實非常小,並且Android 2.3版本當中又增加了併發垃圾回收器機制(詳見 Android最佳效能實踐(二)——分析記憶體的使用情況),這讓GC操作時的停頓時間也變得難以察覺,但是這些理由都不足以讓我們可以肆意地建立物件,需要建立的物件我們自然要建立,但是不必要的物件我們就應該儘量避免建立。

下面來看一些我們可以避免建立物件的場景:

  • 如果我們有一個需要拼接的字串,那麼可以優先考慮使用StringBuffer或者StringBuilder來進行拼接,而不是加號連線符,因為使用加號連線符會建立多餘的物件,拼接的字串越長,加號連線符的效能越低。
  • 在沒有特殊原因的情況下,儘量使用基本資料類來代替封裝資料型別,int比Integer要更加高效,其它資料型別也是一樣。
  • 當一個方法的返回值是String的時候,通常可以去判斷一下這個String的作用是什麼,如果我們明確地知道呼叫方會將這個返回的String再進行拼接操作的話,可以考慮返回一個StringBuffer物件來代替,因為這樣可以將一個物件的引用進行返回,而返回String的話就是建立了一個短生命週期的臨時物件。
  • 正如前面所說,基本資料型別要優於物件資料型別,類似地,基本資料型別的陣列也要優於物件資料型別的陣列。另外,兩個平行的陣列要比一個封裝好的物件陣列更加高效,舉個例子,Foo[]和Bar[]這樣的兩個陣列,使用起來要比Custom(Foo,Bar)[]這樣的一個陣列高效得多。

當然上面所說的只是一些代表性的例子,我們所要遵守的一個基本原則就是儘可能地少建立臨時物件,越少的物件意味著越少的GC操作,同時也就意味著越好的程式效能和使用者體驗。

靜態優於抽象

如果你並不需要訪問一個物件中的某些欄位,只是想呼叫它的某個方法來去完成一項通用的功能,那麼可以將這個方法設定成靜態方法,這會讓呼叫的速度提升15%-20%,同時也不用為了呼叫這個方法而去專門建立物件了,這樣還滿足了上面的一條原則。另外這也是一種好的程式設計習慣,因為我們可以放心地呼叫靜態方法,而不用擔心呼叫這個方法後是否會改變物件的狀態(靜態方法內無法訪問非靜態欄位)。

對常量使用static final修飾符

我們先來看一下在一個類的最頂部定義如下程式碼:

編譯器會為上述程式碼生成一個初始化方法,稱為<clinit>方法,該方法會在定義類第一次被使用的時候呼叫。然後這個方法會將42的值賦值到intVal當中,並從字串常量表中提取一個引用賦值到strVal上。當賦值完成後,我們就可以通過欄位搜尋的方式來去訪問具體的值了。

但是我們還可以通過final關鍵字來對上述程式碼進行優化:

經過這樣修改之後,定義類就不再需要一個<clinit>方法了,因為所有的常量都會在dex檔案的初始化器當中進行初始化。當我們呼叫intVal時可以直接指向42的值,而呼叫strVal時會用一種相對輕量級的字串常量方式,而不是欄位搜尋的方式。

另外需要大家注意的是,這種優化方式只對基本資料型別以及String型別的常量有效,對於其它資料型別的常量是無效的。不過,對於任何常量都是用static final的關鍵字來進行宣告仍然是一種非常好的習慣。

使用增強型for迴圈語法

增強型for迴圈(也被稱為for-each迴圈)可以用於去遍歷實現Iterable介面的集合以及陣列,這是jdk 1.5中新增的一種迴圈模式。當然除了這種新增的迴圈模式之外,我們仍然還可以使用原有的普通迴圈模式,只不過它們之間是有效率區別的,我們來看下面一段程式碼:

可以看到,上述程式碼當中我們使用了三種不同的迴圈方式來對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方法呢?這裡我舉一個非常簡單的例子:

可以看到,上面是一個Calculate類,這個類的功能非常簡單,先將one和two這兩個欄位進行了封裝,然後提供了getOne()方法獲取one欄位的值,提供了getTwo()方法獲取two欄位的值,還提供了一個getSum()方法用於獲取總和的值。

這裡我們注意到,getSum()方法當中的演算法就是將one和two的值相加進行返回,但是它獲取one和two的值的方式也是通過getters方法進行獲取的,其實這是一種完全沒有必要的方式,因為getSum()方法本身就是Calculate類內部的方法,它是可以直接訪問到Calculate類中的封裝欄位的,因此這種寫法在Android上是不推崇的,我們可以進行如下修改:

改成這種寫法之後,我們就避免了在內部呼叫getters/setters方法,而對於外部而言Calculate類仍然是具有很好的封裝性的。

當然,本篇文章中推薦的這些技巧呢也並不全面,只是從Android官方文件抽取了幾個感覺比較實用的分享給大家,更多技巧大家也可以到Android官網上去閱讀。另外在高效能編碼方面《Efficient Java》這本書當中也提供了非常多的技巧,有興趣的朋友也可以去閱讀一下這本書。那麼本篇文章就到這裡,下篇文章當中將會介紹Android佈局優化的技巧,感興趣的朋友請繼續閱讀 Android最佳效能實踐(四)——佈局優化技巧 。

相關文章