Android性能微優化

jopen 9年前發布 | 16K 次閱讀 性能 Android開發 移動開發

      這篇文檔主要包含一些微小優化,將這些微小優化整合起來就可以提高整個應用程序的性能,但是這些改變并不會導致顯著的性能提升。選擇合適的算法和數據結構 應該優先級更高,但是不在本文的討論范圍之內。你應該使用本文檔的提示作為通常的編程實踐,你可以將這些實踐融入到提高代碼效率的習慣中。
       寫有效率的代碼有兩個基本的原則:
          1.不要做不需要的工作
           2.當可以避免分配內存的話,就不要分配。
        當對Android應用進行微優化時,一個棘手的問題是應用必須在不同類型的硬件上面運行。不同版本的VM運行在不同的處理器上面,速度也不一樣。特別指 出的是,在模擬器上面的性能測試提供了很少的在其他任何設備運行時的性能信息,有JIT和沒有JIT的設備之間也有很大的差別,對支持JIT設備支持很好 的代碼,并不一定是對不支持JIT設備最好的代碼。
     為了確保應用在大量不同的設備上面表現都好,要確保你的代碼是在任何方面都是有效率的,積極優化應用的性能。

  • 避免創建不必要的對象
        對象的創建從來都不是免費的。擁有每個線程分配池的分代垃圾回收器使臨時對象分配時的消耗更少,但是分配內存從來都是比不分配消耗的更多。
        當在應用中分配更多的對象,會引起強制性的gc,造成應用卡頓,在2.3引入的并發回收對此有幫助,但不必要的工作還是應該避免。
        因此,我們應該避免創建不必要的對象實例,下面一些例子可能會有幫助:
    1.當你有個返回string的方法,并且知道這個結果會被append到一個StringBuffer里,修改方法的實現,在方法里直接進行append操作,而不是創建一個臨時的對象。

    2.當從一系列的數據中提取string時,盡量返回原數據的substring,而不是創建一個copy。你可能會創建一個新的string,這會與原 來的數據共享char[](這樣權衡是因為如果你只是用了原數據的一小部分,這樣做的話會是所有的原數據都存在內存中)

一個比較激進的做法是,將多維數組切割成一維數組:
    1.一個int型的數組比Integer對象的數組好很多,但這也可以推廣到一個事實,兩個并行的int數組比一個(int,int)的數組對象效率高很到,這對所有元數據的組合都是一樣的。
    2.如果你需要實現一個存放(Foo,Bar)元組的對象,要記住兩個并行的Foo[]和Bar[]數組比單個的(Foo,Bar)對象數組更好。(一個 例外是,當你設計一個API讓其他代碼調用,在這種情況下,我們應該做個小小的妥協來達到一個好的API設計,但如果在自己內部的代碼,我們應該嘗試越有 效率越好)
    通常來說,如果可以避免的話,就不要創建短期的臨時對象。更少的對象意味這更少頻率的gc,而gc對用戶體驗有直接的影響。

  • 選擇靜態
    如果不需要使用其他對象的屬性,就把方法設為static的,調用速度會提高15%-20%,這也是一個好的實踐,因為static表明了調用這個方法不會改變對象的狀態。
  • 對常量使用static final修飾符

    考慮下面類頂部的聲明:
    static int intVal = 42;
    static String strVal = "Hello, world!";
    編譯器會產生類的初始化方法,叫做<clinit>,它會在類初次使用的時候被執行。這個方法將42賦值給intVal,從class文件的string常量表提取一個引用到strVal,當這些值稍后被引用,它們就可以通過查找變量獲取。
    我們可以通過final關鍵字進行優化:
    static final int intVal = 42;
    static final String strVal = "Hello, world!";
    這樣的話類就不需要<clinit> 方法了,因為這個常量在dex文件中已經進行了靜態字段初始化操作。引用了intVal的代碼會直接使用整數值42,使用了strVal的會使用相對比較快的string常量指令替代字段尋找。
   這個優化只適用于元數據和String常量,而不是任意的引用類型,在任何可能的時候聲明常量為static final是一個良好的編程實踐。
  • 在類內部避免使用Getters/Setters
    在本地化語言比如c++中,使用getters(i = getCount())替代直接使用字段(i= mCount)是一個公認的實踐,這在其他面向對象的語言中比如c#和java,都是經常用到的方法,因為編譯器可以通常進行內聯調用,如果需要約束或者 調試字段,你可以隨時添加代碼。
     但是,在Android中這是一個不好的方法。虛方法的調用是耗費昂貴的,有更多的實例字段查找,在公共的接口中遵守這個實踐是合理的,但在一個類中你應該直接使用字段。
     沒有JIT,直接使用字段比調用getter速度快3倍,有JIT的話(直接使用字段與使用局部字段消耗一樣小),直接使用之短比調用getter快7倍。
     假如使用ProGuard,使用這種方法可以有兩者的優點,因為Proguard可以內聯調用。
  • 使用增強for循環
    增強for循環可以用在實現了Iterable接口的集合和數組上面,集合的迭代器實現了hasNext()和next()方法。對于 ArrayList,一個手寫的計數循環會比增強for循環快三倍,但是對與其他集合來說增強的for循環與明確的迭代器調用速度相當。
    下面有幾種迭代數組的方案:
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還沒有優化掉循環中每一次迭代獲取array長度的消耗。
    one()比zero()快一點,它把所有涉及到的值的都抽出為局部變量,避免了查詢,單單數組長度一項就提供了性能優勢。
    two()在沒有JIT的設備上面是最快的,在有JIT的設備上面和one()速度差不多,它采用了java在1.5引進的增強for循環語法。
    所以我們應該默認使用增強for循環,但是對ArrayList的迭代如果性能要求高的話采用手寫的技術循環

  • 當內部類訪問private方法或變量時,應該將private改為包訪問權限
          考慮下面的類定義:

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)直接訪問其外部類的private方法和字段。這是合法的,代碼期望打印出“Value is 27”。
   問題是虛擬機認為Foo$Inner直接訪問Foo的private成員是非法的,因為Foo和Foo$Inner是兩個不同的類,雖然Java語言允許內部類訪問外部類的私有變量。為了消除這個問題,編譯器生成了幾個合成方法:
  
   當需要訪問外部類的mValue變量或者doStuff()方法時,內部類就調用這些靜態方法。這意味這上面的代碼歸結為通過方法來訪問成員變量。上面我們討論過通過方法訪問比直接訪問變量慢,所以這是java語法導致的不可見性能損失。
   如果要使用這樣的代碼,你可以通過聲明內部類訪問的變量和方法為包訪問權限,而不是private,而這些字段也可以被包里面的其他類直接訪問到,所以在公用的API里面不能這樣做。
  • 避免使用浮點型數據
    作為一個經驗法則,在Android設備上浮點型的數據比整型慢2倍左右。
    在速度方面,float和double在越來越先進的硬件上面沒有區別,空間方面,double大兩倍。在臺式機上面,假設空間不是問題,應該選擇double而不是float。
    盡管是整數,一些處理器有硬件乘法,但缺乏硬件除法,在這種情況下,整數除法和模數操作在軟件中執行。
  • 小心使用Native方法
    使用Android Ndk的native代碼開發app并不一定會比java語言開發的app更加有效。首先,java-native之間的過度連接需要成本,JIT優化無 法跨越這些邊界。如果你分配本地資源(本地堆內存,文件描述符,或者其他),及時安排回收這些資源是更加困難的。你需要在希望運行的每一種架構上編譯代 碼,而不能依賴JIT。即使是相同的體系架構,你甚至可能需要編譯多個版本:為Arm處理器的G1編譯的本地代碼不能充分利用Nexus One的Arm處理器,為Nexus One的Arm處理器編譯的代碼不能運行在G1的Arm處理器上面。
    Native代碼是有用的,當你有已經存在的native代碼想適配Android,而不是想加速Android應用中用java語言寫的部分。
  • 性能數據
   在沒有JIT的設備上面,調用準確類型的方法比調用接口方法稍微更加有效(比如通過Hashmap調用方法,比map調用更加有效,即使它們都是HashMap類型)。它實際上沒有慢到2倍,實際的差別可能是慢6%左右,此外,JIT使兩者之間差別很小。
   在沒有JIT的設備上面,訪問緩存字段比重復訪問字段要快20%左右。在有JIT的設備上面,訪問字段的消耗和訪問局部字段差不多,所有這是不值得優化 的除非你認為它是代碼更容易閱讀(這對final,static,static final修飾的字段都是成立的)。
  • 經常測量
    當你開始優化的時候,確保你有問題需要解決,確保你能準確地測量現有的性能,否則你無法衡量你優化后的好處。
    本文中的每一個聲明都是有基準的,這些基準代碼可以在http://code.google.com/p/dalvik/source/browse/#svn/trunk/benchmarks找到。
     這些基準是用Caliper的java微基準framework編譯的,微基準是很難測量正確的,所以Caliper為你做了這些比較難的工作,甚至可以檢測到實際上沒有測試的地方。我們強烈建議你使用Capliper來允許你自己的微基準。
     你可能也發現Traceview對分析也很有用,但是意識到它目前禁止掉了JIT是非常重要的,這可能導致代碼的執行時間不正確,而JIT可以贏回這些 時間。當按照Traceview數據進行修改后,確保修改后的代碼比沒有traceview時運行更快是非常重要的。
 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!