Android代碼優化小技巧

這篇文章主要介紹一些小細節的優化技巧,當這些小技巧綜合使用起來的時候,對於整個App的性能提高仍是有做用的,只是不能較大幅度的提高性能而已。選擇合適的算法與數據結構才應該是你首要考慮的因素,在這篇文章中不會涉及這方面。你應該使用這篇文章中的小技巧做爲平時寫代碼的習慣,這樣可以提高代碼的效率。html

一般來講,高效的代碼須要知足下面兩個規則:android

  • 不要作冗餘的工做程序員

  • 若是能避免,儘可能不要分配內存算法

在優化App時最難解決的問題之一就是讓App能在各類類型的設備上運行。不一樣版本的虛擬機在不一樣的處理器上會有不一樣的運行速度。你甚至不能簡單的認爲「設備X的速度是設備Y的F倍」,而後還用這種倍數關係去推測其餘設備。特別的是,在模擬器上的運行速度和在實際設備上的速度沒有半點關係。一樣,設備有沒有JIT(即時編譯,譯者注)也對運行速度有重大影響:在有JIT狀況下的最優化代碼不必定在沒有JIT的狀況下也最優化。編程

爲了確保App在各設備上都能良好運行,就要確保你的代碼在不一樣檔次的設備上都儘量的優化。數組

避免建立沒必要要的對象

建立對象歷來不是無代價的。在線程分配池裏的逐代垃圾回收器可使臨時對象的分配變得廉價一些,可是分配內存老是比不分配更昂貴。緩存

隨着你在App中分配更多的對象,你可能須要強制GC(垃圾回收,譯者注),爲用戶體驗作一個小小的減壓。Android 2.3 中引入的併發GC會幫助你作這件事情,但畢竟沒必要要的工做應該儘可能避免數據結構

所以請儘可能避免建立沒必要要的對象,有下面一些例子來講明這個問題:架構

  • 若是你須要返回一個String對象,而且你知道它最終會須要鏈接到一個StringBuffer,請修改你的函數簽名和實現方式,避免直接進行鏈接操做,應該採用建立一個臨時對象來作這個操做.併發

  • 當從輸入的數據集中抽取出String的時候,嘗試返回原數據的substring對象,而不是建立一個重複的對象。你將會 new 一個 String 對象,可是它應該和原數據共享內部的char[](代價是若是你只是用原數據中的一小部分,你只須要保存這一小部分的對象在內存中)

一個稍微激進點的作法是把全部多維的數據分解成一維的數組:

  • 一組int數據要比一組Integer對象要好不少。能夠得知,兩組一維數組要比一個二維數組更加的有效率。一樣的,這個道理能夠推廣至其餘原始數據類型。

  • 若是你須要實現一個數組用來存放(Foo,Bar)的對象,記住使用Foo[]與Bar[]要比(Foo,Bar)好不少。(例外的是,爲了某些好的API的設計,能夠適當作一些妥協。可是在本身的代碼內部,你應該多多使用分解後的容易)。

一般來講,須要避免建立更多的臨時對象。更少的對象意味者更少的GC動做,GC會對用戶體驗有比較直接的影響。

選擇Static而不是Virtual

若是你不須要訪問一個對象的值域,請保證這個方法是static類型的,這樣方法調用將快15%-20%。這是一個好的習慣,由於你能夠從方法聲明中得知調用沒法改變這個對象的狀態。

常量聲明爲Static Final

考慮下面這種聲明的方式

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

編譯器會使用一個初始化類的函數,而後當類第一次被使用的時候執行。這個函數將42存入intVal,還從class文件的常量表中提取了strVal的引用。當以後使用intValstrVal的時候,他們會直接被查詢到。

咱們能夠用final關鍵字來優化:

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

這時不再須要上面的方法了,由於final聲明的常量進入了靜態dex文件的域初始化部分。調用intVal的代碼會直接使用42,調用strVal的代碼也會使用一個相對廉價的「字符串常量」指令,而不是查表。

注意:這個優化方法只對原始類型和String類型有效,而不是任意引用類型。不過,在必要時使用static final是個很好的習慣

避免內部的Getters/Setters

像C++等native language,一般使用getters(i = getCount())而不是直接訪問變量(i = mCount)。這是編寫C++的一種優秀習慣,並且一般也被其餘面向對象的語言所採用,例如C#與Java,由於編譯器一般會作inline訪問,並且你須要限制或者調試變量,你能夠在任什麼時候候在getter/setter裏面添加代碼。

然而,在Android上,這是一個糟糕的寫法。虛函數的調用比起直接訪問變量要耗費更多。在面向對象編程中,將getter和setting暴露給公用接口是合理的,但在類內部應該僅僅使用域直接訪問。

在沒有JIT(Just In Time Compiler)時,直接訪問變量的速度是調用getter的3倍。有JIT時,直接訪問變量的速度是經過getter訪問的7倍。

請注意,若是你使用ProGuard, 你能夠得到一樣的效果,由於ProGuard能夠爲你inline accessors.

使用加強的For循環

加強的For循環(也被稱爲 for-each 循環)能夠被用在實現了 Iterable 接口的 collections 以及數組上。使用collection的時候,Iterator (迭代器,譯者注) 會被分配,用於for-each調用hasNext()next()方法。使用ArrayList時,手寫的計數式for循環會快3倍(無論有沒有JIT),可是對於其餘collection,加強的for-each循環寫法會和迭代器寫法的效率同樣。

請比較下面三種循環的方法:

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()稍微快些。

  • two() 在沒有作JIT時是最快的,但是若是通過JIT以後,與方法one()是差很少同樣快的。它使用了加強的循環方法for-each。

因此請儘可能使用for-each的方法,可是對於ArrayList,請使用方法one()。

你還能夠參考 Josh Bloch 的 《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"。

問題是,VM由於FooFoo$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()函數時,它都會調用這些靜態方法。這意味着,上面的代碼能夠歸結爲,經過accessor函數來訪問成員變量。早些時候咱們說過,經過accessor會比直接訪問域要慢。因此,這是一個特定語言用法形成性能下降的例子。

若是你正在性能熱區(hotspot:高頻率、重複執行的代碼段)使用像這樣的代碼,你能夠把內部類須要訪問的域和方法聲明爲包級訪問,而不是私有訪問權限。不幸的是,這意味着在相同包中的其餘類也能夠直接訪問這些域,因此在公開的API中你不能這樣作。

避免使用float類型

Android系統中float類型的數據存取速度是int類型的一半,儘可能優先採用int類型。

就速度而言,現代硬件上,float 和 double 的速度是同樣的。空間而言,double 是兩倍float的大小。在桌面機上,空間不是問題的狀況下,你應該使用 double 。

一樣,對於整型,有些處理器實現了硬件幾倍的乘法,可是沒有除法。這時,整型的除法和取餘是在軟件內部實現的,這在你使用哈希表或大量輸血操做時要考慮到。

使用庫函數

除了那些常見的讓你多使用自帶庫函數的理由之外,記得系統函數有時能夠替代第三方庫,而且還有彙編級別的優化,他們一般比帶有JIT的Java編譯出來的代碼更高效。典型的例子是:Android API 中的String.indexOf(),Dalvik出於內聯性能考慮將其替換。一樣System.arraycopy()函數也被替換,這樣的性能在Nexus One測試,比手寫的for循環並使用JIT還快9倍。

參見 Josh Bloch 的 《Effective Java》這本書的第47條

謹慎使用native函數

結合Android NDK使用native代碼開發,並不老是比Java直接開發的效率更好的。Java轉native代碼是有代價的,並且JIT不能在這種狀況下作優化。若是你在native代碼中分配資源(好比native堆上的內存,文件描述符等等),這會對收集這些資源形成巨大的困難。你同時也須要爲各類架構從新編譯代碼(而不是依賴JIT)。你甚至對已一樣架構的設備都須要編譯多個版本:爲G1的ARM架構編譯的版本不能徹底使用Nexus One上ARM架構的優點,反之亦然。

Native 代碼是在你已經有本地代碼,想把它移植到Android平臺時有優點,而不是爲了優化已有的Android Java代碼使用。

若是你要使用JNI,請學習JNI Tips

參見 Josh Bloch 的 《Effective Java》這本書的第54條

關於性能的誤區

在沒有JIT的設備上,使用一種確切的數據類型確實要比抽象的數據類型速度要更有效率(例如,調用HashMap map要比調用Map map效率更高)。有誤傳效率要高一倍,實際上只是6%左右。並且,在JIT以後,他們直接並無大多差別。

在沒有JIT的設備上,讀取緩存域比直接讀取實際數據大概快20%。有JIT時,域讀取和本地讀取基本無差。因此優化並不值得除非你以爲能讓你的代碼更易讀(這對 final, static, static final 域一樣適用)。

關於測量

在優化以前,你應該決定你遇到了性能問題。你應該確保你可以準確測量出如今的性能,不然你也不會知道優化是否真的有效。

本章節中全部的技巧都須要Benchmark(基準測試)的支持。Benchmark能夠在code.google.com "dalvik" project中找到

Benchmark是基於Java版本的Calipermicrobenchmarking(基準微測,譯者注)框架開發的。Microbenchmarking很難作準確,因此Caliper幫你完成這部分工做,甚至還幫你測了你沒想到須要測量的部分(由於,VM幫你管理了代碼優化,你很難知道這部分優化有多大效果)。咱們強烈推薦使用Caliper來作你的基準微測工做。

咱們也能夠用Traceview來測量,可是測量的數據是沒有通過JIT優化的,因此實際的效果應該是要比測量的數據稍微好些。

關於如何測量與調試,還能夠參考下面兩篇文章:

相關文章
相關標籤/搜索