Android最佳性能實踐(三)——高性能編碼優化

轉載請註明出處:http://blog.csdn.net/guolin_blog/article/details/42318689

在前兩篇文章當中,我們主要學習了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修飾符

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

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. static int intVal = 42;  
  2. static String strVal = "Hello, world!";  
編譯器會爲上述代碼生成一個初始化方法,稱爲<clinit>方法,該方法會在定義類第一次被使用的時候調用。然後這個方法會將42的值賦值到intVal當中,並從字符串常量表中提取一個引用賦值到strVal上。當賦值完成後,我們就可以通過字段搜尋的方式來去訪問具體的值了。

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

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. static final int intVal = 42;  
  2. static final String strVal = "Hello, world!";  
經過這樣修改之後,定義類就不再需要一個<clinit>方法了,因爲所有的常量都會在dex文件的初始化器當中進行初始化。當我們調用intVal時可以直接指向42的值,而調用strVal時會用一種相對輕量級的字符串常量方式,而不是字段搜尋的方式。

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

使用增強型for循環語法

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

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. static class Counter {  
  2.     int mCount;  
  3. }  
  4.   
  5. Counter[] mArray = ...  
  6.   
  7. public void zero() {  
  8.     int sum = 0;  
  9.     for (int i = 0; i < mArray.length; ++i) {  
  10.         sum += mArray[i].mCount;  
  11.     }  
  12. }  
  13.   
  14. public void one() {  
  15.     int sum = 0;  
  16.     Counter[] localArray = mArray;  
  17.     int len = localArray.length;  
  18.     for (int i = 0; i < len; ++i) {  
  19.         sum += localArray[i].mCount;  
  20.     }  
  21. }  
  22.   
  23. public void two() {  
  24.     int sum = 0;  
  25.     for (Counter a : mArray) {  
  26.         sum += a.mCount;  
  27.     }  
  28. }  
可以看到,上述代碼當中我們使用了三種不同的循環方式來對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方法呢?這裏我舉一個非常簡單的例子:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. public class Calculate {  
  2.       
  3.     private int one = 1;  
  4.       
  5.     private int two = 2;  
  6.   
  7.     public int getOne() {  
  8.         return one;  
  9.     }  
  10.   
  11.     public int getTwo() {  
  12.         return two;  
  13.     }  
  14.       
  15.     public int getSum() {  
  16.         return getOne() + getTwo();  
  17.     }  
  18. }  
可以看到,上面是一個Calculate類,這個類的功能非常簡單,先將one和two這兩個字段進行了封裝,然後提供了getOne()方法獲取one字段的值,提供了getTwo()方法獲取two字段的值,還提供了一個getSum()方法用於獲取總和的值。

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

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. public class Calculate {  
  2.       
  3.     private int one = 1;  
  4.       
  5.     private int two = 2;  
  6.   
  7.     ......  
  8.       
  9.     public int getSum() {  
  10.         return one + two;  
  11.     }  
  12. }  
改成這種寫法之後,我們就避免了在內部調用getters/setters方法,而對於外部而言Calculate類仍然是具有很好的封裝性的。
發佈了18 篇原創文章 · 獲贊 4 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章