1. 程式人生 > >【官網翻譯】效能篇(十)效能提示

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

前言

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

       中國版官網原文地址為: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時執行得更快。

       更多幫助分析和除錯應用的資訊,請查閱如下文件:

  • 【使用TraceView和dmtracedump進行分析】
  • 【使用Systrace分析UI效能】

 

結語

       本文最大限度保持原文的意思,由於筆者水平有限,若有翻譯不準確或不妥當的地方,請指正,謝