[Andorid應用開發]-(3)效能最佳化設計

來源:互聯網
上載者:User

 這篇文章說效能設計,我估摸著有很多童鞋都沒看到過原文,這裡推薦下,文章來自Android官方,在下載的Android Docs的Dev Guide可以看到。如果你沒讀過這篇文章,那麼我強烈建議去細讀它。


一個Android應用程式運行在有著有限的計算能力和儲存空間及受限的電池壽命的行動裝置上。有鑒於此,該應用程式應該是高效的。即便你的程式看起來運行得“足夠快”,電池壽命可能是你想要最佳化你的應用程式的一個原因。對使用者來說電池壽命重要,Android的電池耗盡意味著使用者將知道是你的應用程式讓電池耗盡的。

請注意,雖然本文主要是講解微最佳化的,但是這些最佳化幾乎不會破壞你的應用程式。你首要的任務依然是選擇合適的演算法和資料結構,但這些不在本文討論的範圍之內。

目錄 [隱藏]
1 簡介
2 明智地進行最佳化
3 避免建立不必要的對象
4 效能最佳化的神話
5 用靜態方法比虛方法好
6 不要在類內部使用getter和setter
7 使用Static Final 修飾常量
8 使用for-each文法
9 將與內部類一同使用的變數聲明在包範圍內
10 避免使用浮點數
11 瞭解和使用Android提供的類庫
12 謹慎使用JNI調用本地方法
13 結束語
 

簡介
以下是編寫高效代碼的兩條基本原則:

不要做不必要的事
不要分配不必要的記憶體
明智地進行最佳化
這份文檔是關於Android平台微最佳化的,這裡假定你已經分析了程式知道了那些代碼是需要最佳化的,並且你已經有了一個方法用于衡量你所做的任何改變產生的結果(好或壞)。你只有這麼點工程時間用來投資,重要的是你要知道你明智使用了它們。

(參見#結束語可以瞭解更多的分析應用程式和編寫高效的測試標準的技巧)

同時該文檔假定你選擇了正確的資料結構和演算法,並且考慮到了改變你的程式將帶來的效能影響。正確地選用資料結構和演算法帶來的影響與本文檔提供所有建議相比將有很大不同,而預知你的改變帶來的效能影響將有助於你切換到更好地實現方案(比起應用代碼,這對類庫代碼更重要些)。

(如果你需要這方面的建議,可以參考Josh Bloch編寫書籍 Effective Java, 第47條)

在對你的Android應用程式進行微最佳化時,你將會遇到一個棘手的問題:你要保證你的應用程式運行在多個硬體平台上。不同版本的虛擬機器運行在不同的處理器上其運行速度是不同的。你可以說“裝置X比裝置Y快/慢F倍”,但這個結論是不可以從一台裝置推廣到其他裝置的。特別的是,你在模擬器上的測試結果並不能告訴你在其他任何裝置中的效能怎麼樣。同樣,使用JIT的裝置和不使用JIT的裝置也有相當大的差異:在使用了JIT的裝置上運行得效能“最好”的代碼可能並不適用於沒有使用JIT的裝置。

如果你想知道你的應用程式在指定裝置上啟動並執行情況,你需要在該裝置上做測試。

避免建立不必要的對象
建立對象並非是免費的。雖然GC為每個線程都建立了臨時對象池,可以使建立對象的代價變得小一些,但是分配記憶體永遠都比不分配記憶體的代價大。

如果你在使用者介面迴圈中指派至記憶體,就會引發周期性的記憶體回收,使用者就會覺得介面像打嗝一樣一頓一頓的。Android2.3引入的並發收集器雖有助於記憶體回收,但不必要的工作還是應該避免。

所以,應盡量避免建立不必要的對象執行個體。下面的例子將有助你理解這條原則:

如果你有一個函數返回一個String對象,而你確切的知道這個字串會被附加到一個StringBuffer,那麼,請改變這個函數的參數和實現方式,直接把結果附加到StringBuffer中,而不要再建立一個短命的臨時對象。
當你從使用者輸入的資料中截取一段字串時,盡量使用substring函數取得未經處理資料的一個子串,而不是為子串另外建立一份拷貝。這樣你就有一個新的String對象,它與未經處理資料共用一個char數組。(後果就是如果你只是用原始輸入的一小部分,那麼使用這種方式你將一直把它保留在記憶體裡)
一個更極端的例子是,把多維陣列分成多個一維數組。

int數組比Integer數組好,這也概括了一個基本事實,兩個平行的int數組比(int,int)對象數組效能要好很多。同理,這試用於所有基本類型的組合。
如果你想用一種容器儲存(Foo,Bar)元組,嘗試使用兩個單獨的Foo[]數組和Bar[]數組,一定比(Foo,Bar)數組效率更高。(也有例外的情況,就是當你建立一個API,讓別人調用它的時候。這時候你要注重對API借口的設計而犧牲一點兒速度。當然在API的內部,你仍要儘可能的提高代碼的效率)
總體來說,就是避免建立短命的臨時對象。減少對象的建立就能減少垃圾收集,進而減少對使用者體驗的影響。

效能最佳化的神話
我們將在這裡澄清這份文檔先前版本造成的誤導性聲明。

在不使用JIT的裝置中,調用一個介面的引用會比調用實體類的引用花費更多的時間(舉個例子,調用一個HashMap類型的map比調用一個Map類型的map代價要低,即便在這兩種情況下,map都是HashMap的執行個體)。這裡並非是先前說的慢2倍,實際上是更像是慢了6%。此外,JIT有效區分了兩者。

在不使用JIT的裝置中,訪問緩衝的局部變數比重複調用該成員變數快大約20%。在使用JIT的裝置中,訪問局部變數的花費跟訪問成員變數的花費是一樣的,所以這並不是一個很好的最佳化措施,除非你覺得這樣會使你的代碼更容易閱讀(這條建議對final、static和final static 修飾的成員變數也是適用的)。

用靜態方法比虛方法好
如果你不需要訪問一個對象的成員變數,那麼請把方法聲明成static。使用靜態方法調用將快15-20%。這是一個很好的技巧,因為你可以通過方法簽名調用該函數而不會改變對象的狀態。

不要在類內部使用getter和setter
在很多本地語言如C++中,使用getter(比如:i = getCount())而不是直接存取成員變數(i = mCount)是一種很普遍的用法。在C++中這是一個非常好的習慣,因為編譯器能夠內聯訪問,如果你需要約束或調試變數,你可以在任何時候添加代碼。

在Android上,這就不是個好主意了。虛方法的開銷比直接存取成員變數大得多。設計公用的介面時,可以遵循物件導向的普遍做法來定義getters和setters,但是在一個類內部,你應該直接存取變數。

在沒有JIT的裝置上,直接存取成員變數比調用一個簡單的getter方法快大約3倍。在有JIT的裝置上,直接存取成員變數比調用一個簡單的getter方法快大約7倍。目前這是Froyo(android2.2)版本上的事實,我們將在後續版本上改善JIT內聯getter方法的效率。

使用Static Final 修飾常量
讓我們來看看這兩段在類前面的聲明:

static int intVal = 42;
static String strVal = "Hello, world!";
編譯器會產生一個叫做<clinit>的初始化類的方法,當類第一次被使用的時候這個方法會被執行。該方法會將42賦給intVal,然後把一個指向類中常量表的引用賦給strVal。當以後要用到這些值的時候,可以在成員變數表中尋找到他們。

下面我們使用“final”關鍵字做些改進:

static final int intVal = 42;
static final String strVal = "Hello, world!";
現在,類不再需要<clinit>方法,因為在成員變數初始化的時候,會將常量直接儲存到類檔案中。用到intVal的代碼被直接替換成42,而使用strVal的會指向一個相對來說代價小的字串常量,而不是使用成員變數(要注意的是這個最佳化只適用於基本類型和字串常量,而不適用參考型別。儘管如此,這依然是一種很提倡的做法,你應該儘可能地把常量聲明為static final)。

使用for-each文法
加強的for迴圈(有時也叫for-each迴圈)可以用在實現了Iterable介面的集合類型和數組上。對集合來說,一個迭代器意味著可以調用hasNext()和next()方法。在ArrayList中,手寫的計數迴圈比使用for-each迴圈快大約3倍,但是對其他集合類來說,foreach相當於使用 iterator。

下面展示了遍曆一個數組的3種可選用法:

static class Foo {
         int mSplat;
     }
     Foo[] mArray = ...
 
public void zero() {
         int sum = 0;
         for (int i = 0; i < mArray.length; ++i) {
             sum += mArray[i].mSplat;
         }
     }
 
public void one() {
         int sum = 0;
         Foo[] localArray = mArray;
         int len = localArray.length;
 
for (int i = 0; i < len; ++i) {
             sum += localArray[i].mSplat;
         }
     }
 
public void two() {
         int sum = 0;
         for (Foo a : mArray) {
             sum += a.mSplat;
         }
     }
zero()最慢,因為迴圈的每一次迭代都會擷取數組的長度一次,這一點JIT並沒有最佳化。

one()比zero()要快些,它將所有變數用局部變數儲存,避免了尋找。但僅僅是儲存數組長度會帶來效率的提升。

two()使用了在java1.5中引入的foreach文法。對於沒有JIT裝置來說,這種方法是最快的,區分於使用了JIT的one()。

綜上所述,預設可以使用加強型的for(即foreach),在影響關鍵效能的ArrayList遍曆時,考慮手寫計數迴圈。(可參考 Effective Java 第46條)

將與內部類一同使用的變數聲明在包範圍內
請看下面的類定義:

public class Foo {
     private class Inner {
         void stuff() {
             Foo.this.doStuff(Foo.this.mValue);
         }
     }
 
private int mValue;
 
public void run() {
         Inner in = new Inner();
         mValue = 27;
         in.stuff();
     }
 
private void doStuff(int value) {
         System.out.println("Value is " + value);
     }
 }
這其中的關鍵是,我們定義了一個私人內部類(Foo$Inner),它需要訪問外部類的私人成員變數和函數。這是合乎文法的,並且會列印出我們希望的結果”Value is 27″。

問題在於Foo和Foo$Inner是兩個不同的類,虛擬機器認為在Foo$Inner類中直接存取Foo類的私人成員是是非法的,即便在java語言中允許內部類訪問外部類的私人變數。為了跨越這一障礙,編譯器會產生一組中間方法:

/*package*/ static int Foo.access$100(Foo foo) {
    return foo.mValue;
}
/*package*/ static void Foo.access$200(Foo foo, int value) {
    foo.doStuff(value);
}
內部類在訪問mValue或調用外部類的doStuff方法時,都會調用這些靜態方法。也就是說,上面的代碼說明了一個問題,你是在通過介面方法訪問這些成員變數和函數而不是直接調用它們。在前面我們已經說過,使用介面方法(getter、setter)比直接存取速度要慢。所以這個例子就是在特定文法下面產生的一個“隱性的”效能障礙。

如果你的代碼中是類似情況,你可以通過將內部類訪問的變數和函式宣告由私人範圍改為包範圍來避免開銷。遺憾的是,這些域和方法可以被同一個包內的其他類直接存取,因此當你設計公用API的時候應該謹慎使用這條最佳化原則。

避免使用浮點數
從常理來講,在Android裝置上浮點數比整數慢2倍。在缺少FPU和JIT的G1和使用了FPU和JIT的Nexus,這都是事實。(當然,這兩者之間的運算速度相差約10倍)

在速度方面,double和float在現代硬體面前並沒有明顯差異。在空間方面,double佔用的空間是float的2倍。如果跟台式機一樣你的空間不是問題,你可以使用double而非float。

同樣,即便是整數,一些晶片對乘法有硬體支援而缺少對除法的支援。這種情況下,整數的除法和模數運算都是由軟體來完成的——你可以想象下在設計雜湊表或者做大量數學運算時做的那些事(就知道效率有多麼低了)。

瞭解和使用Android提供的類庫
除了有特別的原因,建議你儘可能使用android提供的類庫而不是你自己的,要記住的是Android系統會對現有的java類庫做一些最佳化和擴充,這顯然比原有JIT類庫更有效率些。一個典型的例子是Dalvik使用內聯最佳化了String.indexOf方法,同樣的還有System.arraycopy方法,後者比使用了JIT的Nexus One快大約9倍。(這條可參考 Effective Java 第47條)

謹慎使用JNI調用本地方法
本地代碼不一定比java代碼更有效率。首先從java到本地代碼的過渡是需要開銷的,而這點JIT並不能最佳化。如果在本地代碼裡你分配了本地資源(本地的記憶體,檔案,或任何其他的資源),那麼及時地搜集這些資源將變得更困難。同時,對不同的架構平台你需要編譯不同的代碼(而不是依賴JIT給你提供一套)。甚至對同一個平台,你也需要編譯多種版本的代碼。(G1的ARM處理器編譯的本地代碼跟 Nexus One上的ARM處理器編譯的本地代碼並不通用)

當你已經有一個本地程式碼程式庫並想把代碼用於android時,你可以考慮使用JNI調用本地代碼。事實上,自java1.3以後,使用本地方法來提高效能的做法已經不值得提倡了。

如果你確實需要用到本地代碼,可以參考JNI Tips。(參見 Effective Java 第54條)

結束語
最後一件事情:要不斷的測試。要帶著問題去最佳化,確保你能夠精確的度量未最佳化之前的效能,否則你將不能衡量你嘗試做出的效能最佳化。

該文檔中每一條都有相應的標準來衡量,在code.google.com “dalvik” project能找到這些標準。

這些標準都是構建在Caliper的java架構上的。儘管如此,”微標準“很難衡量,這個項目 Caliper可以協助你,我們在此強烈建議你使用該架構。

你可能還會發現可以用Traceview來剖析器,但要意識到目前它是禁用JIT的,這會導致計算JIT調用的時間是不正確的。要確保使用Traceview資料帶來的最佳化確實要比不使用Traceview好。

 


    文中提到的Android效能最佳化的原則和方法,閱讀過後可能還是有些茫然。我們平常開發也就處理些簡單的資料或者實現一個簡單的功能,沒有牽涉到演算法、內部類等。那麼如何最佳化我們的程式呢?下面我針對幾個常用的編程方法進行代碼的微最佳化,在做Android應用開發的時候我們也要謹記,手機或者MID記憶體是相當有限的,開發的應用程式應該盡量讓其高效率的執行代碼,而非像做J2EE程式似的鋪張浪費。有很多童鞋將之前開發J2EE的經驗帶到Android應用開發上來,其實這些也只是細節,多注意多思考吧。

1、關於擷取數組和列表長度的方法,請看下面的代碼
[java]
<SPAN style="FONT-SIZE: 16px">  List<String> list = new ArrayList<String>(); 
        for (int i = 0; i < 100; i++) { 
            list.add("abcdefg"); 
        }        
        long startTime1 = System.currentTimeMillis(); 
        for(int i = 0;i<list.size();i++){ 
            System.out.println(list.get(i)); 
        } 
        long endTime1 = System.currentTimeMillis(); 
        Log.e(TAG, "time:"+(endTime1-startTime1)); 
        /////////////////////////////////////////////  
        long startTime2 = System.currentTimeMillis(); 
        int size = list.size(); 
        for(int i = 0;i<size;i++){ 
            System.out.println(list.get(i)); 
        } 
        long endTime2 = System.currentTimeMillis(); 
        Log.e(TAG, "time:"+(endTime2-startTime2));</SPAN> 

 List<String> list = new ArrayList<String>();
     for (int i = 0; i < 100; i++) {
      list.add("abcdefg");
  }     
     long startTime1 = System.currentTimeMillis();
     for(int i = 0;i<list.size();i++){
      System.out.println(list.get(i));
     }
     long endTime1 = System.currentTimeMillis();
     Log.e(TAG, "time:"+(endTime1-startTime1));
     /////////////////////////////////////////////
     long startTime2 = System.currentTimeMillis();
     int size = list.size();
     for(int i = 0;i<size;i++){
      System.out.println(list.get(i));
     }
     long endTime2 = System.currentTimeMillis();
     Log.e(TAG, "time:"+(endTime2-startTime2));

    通過上面簡單的方法,結合列印資訊,對比可知:先擷取list的長度再迴圈效率高出一倍左右!那麼在你的程式中知道該怎麼做了吧?

2、關於Android靜態變數問題
    Android的靜態變數有個特點,就是在關閉應用程式時靜態變數任然存在,請看以下程式碼片段

[java]
<SPAN style="FONT-SIZE: 16px">        private static int i = 10; 
    @Override 
    public void onCreate(Bundle savedInstanceState) { 
        super.onCreate(savedInstanceState); 
        setContentView(R.layout.main); 
        Log.e(TAG, "i=" + i); 
        i = 15; 
    } 
    @Override 
    protected void onDestroy() { 
        Log.e(TAG, "onDestroy========="); 
        //android.os.Process.killProcess(android.os.Process.myPid());  
        super.onDestroy(); 
    }</SPAN> 

        private static int i = 10;
 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);
  Log.e(TAG, "i=" + i);
  i = 15;
 }
 @Override
 protected void onDestroy() {
  Log.e(TAG, "onDestroy=========");
  //android.os.Process.killProcess(android.os.Process.myPid());
  super.onDestroy();
 }將應用程式運行兩次,看到如下結果:

 

    怎麼樣,有意思吧!為了避免這種情況,可在Activity.onDestroy方法中加入這句代碼:android.os.Process.killProcess(android.os.Process.myPid());將當前應用所在的進程中給殺掉。如果不殺掉的話,那麼要謹慎使用了static,就像上面的代碼由於沒有銷毀而導致i的初始值不正確。當然還有另外一種方法達到static的效果。那就是繼承Application來達到多個Activity、Service中使用的目的。我們都知道,在Android應用程式中所有的Activity,Service,Broadcast間接繼承Context。在AndroidManifest.xml設定檔中,所有的Activity,Service等都被包含在標籤Application中。所有的Activity,Service都可調用Application中非私人的方法和變數。於是可以通過繼承Application自定一個Application的子類來達到目的。當然了,只要控制好了就沒什麼問題。

3、關於Android的布局最佳化,請看下一篇部落格吧,因為很重要,也有很多知識點~

 作者:tangcheng_ok
 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.