【官網翻譯】性能篇(十)性能提示

前言java

       本文翻譯自Android開發者官網的一篇文檔,主要用於介紹app開發中性能優化的一實踐要點。android

       中國版官網原文地址爲:https://developer.android.google.cn/training/articles/perf-tips算法

       路徑爲:Android Developers > Docs > 指南 > Best practies > Performance > Performance Tips編程

 

正文數組

       本文主要覆蓋了細微的優化,雖然他們組合起來可以提升整個應用的性能,可是這些改變會致使顯著的性能影響是不太可能的。選擇正確的算法和數據結構應該始終是您優先要考慮的,可是這在本文的範圍以外。您應該使用本文中的這些提示來做爲常規的編碼實踐,這樣爲了常規的代碼效率,您能夠將這些編碼實踐融入您的習慣中。緩存

       這裏有兩個編寫高效代碼的基本規則:性能優化

  • 不要作您不須要的工做。
  • 若是您能夠避免,就不要分配內存。

       其中一個您在細微優化Android應用時要面對的棘手的問題是,您的應用肯定在多種類型的硬件上運行。不一樣版本的虛擬機在不一樣的處理器上以不一樣的速度運行。通常來講,您甚至不能簡單地說「設備X是一個比設備Y更快/慢的因素F」,而且將您的結果從一個設備縮放當另一個設備。通常來講,模擬器上的測量機會不會告訴您任何關於設備的性能。一樣,在擁有或者沒有JIT的設備之間也存在着巨大的差別:有JIT的設備上最好的代碼,在沒有JIT的設備上並不老是最好的代碼。數據結構

       爲了確保您的應用在各類各樣的設備上都運行良好,請確保您的代碼在全部級別中都是有效率的,而且積極地優化您的性能。架構

 

避免建立沒必要要的對象併發

       對象建立歷來就不是免費的。一個帶有爲每一個線程分配臨時對象池的分代垃圾收集器可能讓分配更加便宜,可是分配內存老是比不分配內存要更加昂貴。

       當您在應用中分配更多的對象時,您將強制執行一個週期性的垃圾收集,從而在用戶體驗方面建立小的「打嗝」。在Android2.3中引入的併發垃圾收集器幫上了忙,可是應該避免沒必要要的工做。

       這樣,您應該避免建立您不須要的對象實例。以下一些實例能夠幫到您:

  • 若是您有一個返回字符串的方法,而且您知道不管如何它的結果將老是被附加到StringBuffer,那麼請改變您的簽名和實現,從而讓該函數直接附件,而不是建立一個短期存在的臨時對象。
  • 當從一個輸入數據集合中提取字符串時,嘗試返回原始數據的子字符串,而不是建立一個拷貝。您將建立一個新的字符串對象,可是它將和該數據共享char[]。(折衷的是,若是您只使用一小部分的原始輸入,若是您採用這種方式,不管如何您都將把它保存在整個內存中)

       一個更加完全的主意是將多維數組劃分爲並行的一維數組:

  • int型的數組比Integer對象數組要好得多,可是這也能夠概括爲一個事實,兩個並行的int數組也比(int,int)數組對象要高效得多。任何原始類型的組合也同樣。
  • 若是您須要實現一個存儲(Foo,Bar)對象元組的容器,請記住,通常來講兩個並行的Foo[]和Bar[]數組要比單一的自定義(Foo,Bar)對象數組要好得多。(固然,例外的是,當您正在爲其它代碼設計用於訪問的API時。在這些情形下,一般狀況下最好對速度作一個小小的折衷,從而實現好的API設計。可是在您本身的內部代碼,您應該嘗試儘量高效。)

       通常來講,若是能夠,請避免建立短時間的臨時對象。建立越少的對象意味着越低頻率的垃圾收集,這對用戶體驗會有直接的影響。

 

更喜歡靜態的而不是虛擬的

       若是您無需訪問對象的字段,讓方法成爲靜態的。這樣調用將會快15%-20%。這也是一個很好的實踐,由於從方法簽名能夠辨別出調用該方法不會改變對象的狀態。

 

爲常量使用static final

       在類的頂部考慮以下的聲明:

1 static int intVal = 42;
2 static String strVal = "Hello, world!";

       編譯器產生了一個被稱爲<clinit>的類初始化器方法,當類第一次使用的時候它會被執行。這個方法將42存入intVal,而且從類文件字符串常量表中爲strVal選取引用。當這些值稍後被引用時,它們會經過字段查找被訪問。

       咱們可使用「final」關鍵字來改善這些問題:

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接口的集合和數組。對於集合,將分配迭代器對hasNext()和next()進行接口調用。對於ArrayList,手寫的計數循環速度大約快3倍(有或者沒有JIT),可是對於其它的集合,增強的for循環語法將徹底等價於顯示的迭代器使用。

       對數組進行迭代有若干種選擇:

 1 static class Foo {
 2     int splat;
 3 }
 4 
 5 Foo[] array = ...
 6 
 7 public void zero() {
 8     int sum = 0;
 9     for (int i = 0; i < array.length; ++i) {
10         sum += array[i].splat;
11     }
12 }
13 
14 public void one() {
15     int sum = 0;
16     Foo[] localArray = array;
17     int len = localArray.length;
18 
19     for (int i = 0; i < len; ++i) {
20         sum += localArray[i].splat;
21     }
22 }
23 
24 public void two() {
25     int sum = 0;
26     for (Foo a : array) {
27         sum += a.splat;
28     }
29 }

       zero()方法是最慢的,由於JIT還不能優化循環中每一次迭代中獲取數組長度的花費。

       one()方法稍微快一些。它將一切都推入本地變量,從而避免了查找。只有數組長度提供了性能上的好處。

       two()在沒有JIT的設備上是最快的,在有JIT的設備上和one()沒有區別。它使用了增強的for循環語法,其在Java編程語言的1.5版本中引入。

       因此,您應該默認使用增強的for循環,可是爲性能要求較高的ArrayList迭代考慮手寫計數循環。

★ 提示:也能夠查閱 Josh Bloch 的 《Effective Java》,項目46。

 

考慮使用包而不是私有內部類的私有訪問

       考慮以下類定義:

 1 public class Foo {
 2     private class Inner {
 3         void stuff() {
 4             Foo.this.doStuff(Foo.this.mValue);
 5         }
 6     }
 7 
 8     private int mValue;
 9 
10     public void run() {
11         Inner in = new Inner();
12         mValue = 27;
13         in.stuff();
14     }
15 
16     private void doStuff(int value) {
17         System.out.println("Value is " + value);
18     }
19 }

       在這裏,重點的是定義一個私有的內部類(Foo$Inner),它直接訪問外部類中的一個私有方法和一個私有的實例字段。這是合法的,而且該代碼會如指望的那樣打印「Value is 27」。

       問題是虛擬機認爲從Foo$Inner中直接訪問Foo的私有成員是非法的,由於Foo和Foo$Inner是不一樣的類,雖然Java語言容許內部類訪問外部類的私有成員。爲了鏈接這個溝壑,編譯器生成了兩個合成的方法:

1 /*package*/ static int Foo.access$100(Foo foo) {
2     return foo.mValue;
3 }
4 /*package*/ static void Foo.access$200(Foo foo, int value) {
5     foo.doStuff(value);
6 }

       不管何時須要訪問外部類中的mValue字段或者調用doStuff()方法時,內部類代碼會調用這些靜態的方法。這意味着上面的代碼已經概括爲經過訪問器方法訪問成員字段的情形。更早咱們討論了訪問器是如何比直接字段訪問更慢的,因此這是一個特定語言習慣的例子,致使了「不可見的」性能打擊。

       若是您正在性能熱點中像這樣使用代碼,您能夠經過聲明被內部類訪問的字段和方法爲包訪問來避免這個開銷,而不是私有訪問。遺憾的是,這意味着字段能夠被同一個包中的其它類直接訪問,全部您不該該再公共API中使用它。

 

避免使用浮點型

       根據經驗,在Android驅動設備上,浮點型大約比整型慢兩倍。

       從速度方面看,在更現代的硬件上float和double之間沒有區別。從空間上看,double是float的兩倍大。和臺式機同樣,假設空間不是問題,您應該使用double而不是float。

       即便是整型也同樣,一些處理器有硬件乘法卻沒有硬件除法。在這些狀況下,整數相除和模運算是在軟件中執行的——若是您正在設計hash表或者處理大量數學問題,應該考慮這個問題。

 

瞭解並使用庫

       除了全部通用的更喜歡庫代碼而不是調用本身的代碼的緣由以外,請記住,系統能夠自由地使用手動編碼的彙編程序來取代庫方法調用,這可能比JIT可以爲等效於Java而生成的最好代碼更好。這裏一個典型的例子就是String.indexOf()以及相關的API,Dalvik使用內聯的內部函數來取代它們。相似地,System.arraycopy()方法的速度大約是帶有JIT的Nexus One上手動編碼循環速度的9倍。

★ 提示:也能夠查閱 Josh Bloch 的 《Effective Java》,項目47。

 

慎重使用原生方法

       使用Android NDK包含開發含有原生代碼的應用不必定比使用Java語言編程更有效。首先,有一筆花費與java到原生的轉移有關,而且JIT沒法跨越邊界進行優化。若是您正在分配原生資源(原生堆上的內存,文件描述符,或者其它),及時安排這些資源的收集多是明顯更加困難的。您也須要爲每個您但願在上面運行的架構編譯代碼(而不是依賴它擁有JIT)。您甚至可能不得不爲那些您認爲相同的架構編譯多個版本:爲G1中ARM處理器編譯的原生代碼不能充分利用Nexus One中的ARM,而且爲Nexus One中ARM編譯的代碼在G1的ARM上也不會運行。

       當您擁有已經存在的想移植到Android的原生代碼庫,而不是爲了「加速」用Java語言編寫的Android應用的部分功能時,原生代碼主體上是有用的。

       若是您確實須要使用原生代碼,您應該閱讀咱們的【JNI提示】。

★ 提示:也能夠查閱 Josh Bloch 的 《Effective Java》,項目54。

 

性能神話

       在沒有JIT的設備上,經過具備準確類型而非接口的變量調用方法稍微更有效是個事實。(例如,在HashMap映射上調用方法比在Map映射上要便宜,雖然在這兩種狀況下映射都是HashMap。)這並非變慢兩倍的情形;實際的差異更有多是慢6%。此外,JIT讓這二者有效地沒有區別。

       沒有JIT的設備,緩存字段訪問大約比重複訪問該字段快20%。有JIT的設備上,字段訪問和本地訪問花費大體相同,因此這不是有價值的優化,除非您感受它讓您的代碼更容易閱讀。(對於final,static和static final字段也是如此。)

 

老是測量

       在開始優化以前,確保您有問題須要解決。確保您能夠準確測量存在的性能,不然您將不能測量到您所嘗試的選擇所帶來的好處。

       您也可能發現【TraceView】對分析是有用的,可是意識到它讓JIT當前不可以使用是很重要的,這可能致使它錯誤地分配代碼時間,而JIT可能會贏回來。尤其重要的是,在按照TraceView數據提供的建議更改後,確保實際上生成的代碼在沒有TraceView時運行得更快。

       更多幫助分析和調試應用的信息,請查閱以下文檔:

 

結語

       本文最大限度保持原文的意思,因爲筆者水平有限,如有翻譯不許確或不穩當的地方,請指正,謝謝!

相關文章
相關標籤/搜索