(轉)Android高性能編程(1)--基礎篇

關於專題  
   本專題將深刻研究Android的高性能編程方面,其中涉及到的內容會有Android內存優化,算法優化,Android的界面優化,Android指令級優化,以及Android應用內存佔用分析,還有一些其餘有關高性能編程的知識.
    隨着技術的發展,智能手機硬件配置愈來愈高,但是它和如今的 PC 相比,其運算能力,續航能力,存儲空間等都仍是受到很大的限制,同時用戶對手機的體驗要求遠遠高於 PC 的桌面應用程序。以上理由,足以須要開發人員更加專心去實現和優化你的代碼了。選擇合適的算法和數據結構永遠是開發人員最早應該考慮的事情。同時,咱們應該時刻牢記,寫出高效代碼的兩條基本的原則:(1)不要作沒必要要的事;(2)不要分配沒必要要的內存。
      高效的代碼由兩點的來決定:1.高效的數據結構 ;2高效的執行算法。因此咱們在作應用性能優化的時候,要從各個方面考慮現有的數據結構是否適合當前的功能或者產品,還有就是現有的執行算法是否高效。
     
     今天第一篇文章,主要給你們分享一些高性能編程的基礎知識,其中結合了一些網友和官網的總結分析。
     先推薦一本大神的書:Pro Android Apps Performance Optimization 
    
  內存優化
      
      Android系統對每一個軟件所能使用的RAM空間進行了限制(如:Nexus one 對每一個軟件的內存限制是24M),同時 Java 語言自己比較消耗內存,dalvik 虛擬機也要佔用必定的內存空間,因此合理使用內存,彰顯出一個程序員的素質和技能。
     (1)瞭解 JIT
  即時編譯(Just-in-time Compilation,JIT),又稱動態轉譯(Dynamic Translation),是一種經過在運行時將字節碼翻譯爲機器碼,從而改善字節碼編譯語言性能的技術。即時編譯前期的兩個運行時理論是字節碼編譯和動態編譯。Android 原來 Dalvik 虛擬機是做爲一種解釋器實現,新版(Android 2.2+)將換成 JIT 編譯器實現。性能測試顯示,在多項測試中新版本比舊版本提高了大約 6 倍。
       (2)避免建立沒必要要的對象
  就像世界上沒有免費的午飯,世界上也沒有免費的對象。雖然 gc 爲每一個線程都創建了臨時對象池,可使建立對象的代價變得小一些,可是分配內存永遠都比不分配內存的代價大。若是你在用戶界面循環中分配對象內存,就會引起週期性的垃圾回收,用戶就會以爲界面像打嗝同樣一頓一頓的。因此,除非必要,應儘可能避免盡力對象的實例。下面的例子將幫助你理解這條原則:當你從用戶輸入的數據中截取一段字符串時,儘可能使用 substring 函數取得原始數據的一個子串,而不是爲子串另外創建一份拷貝。這樣你就有一個新的 String 對象,它與原始數據共享一個 char 數組。 若是你有一個函數返回一個 String 對象,而你確切的知道這個字符串會被附加到一個 StringBuffer,那麼,請改變這個函數的參數和實現方式, 直接把結果附加到 StringBuffer 中,而不要再創建一個短命的臨時對象。
  一個更極端的例子是,把多維數組分紅多個一維數組:
  int 數組比 Integer 數組好,這也歸納了一個基本事實,兩個平行的 int 數組比 (int,int) 對象數組性能要好不少。同理,這適用於全部基本類型的組合。若是你想用一種容器存儲 (Foo,Bar) 元組,嘗試使用兩個單獨的 Foo[] 數組和 Bar[] 數組,必定比 (Foo,Bar) 數組效率更高。(也有例外的狀況,就是當你創建一個 API,讓別人調用它的時候。這時候你要注重對 API 接口的設計而犧牲一點兒速度。固然在 API 的內部,你仍要儘量的提升代碼的效率)
  整體來講,就是避免建立短命的臨時對象。減小對象的建立就能減小垃圾收集,進而減小對用戶體驗的影響。
      (3)靜態方法代替虛擬方法
  若是不須要訪問某對象的字段,將方法設置爲靜態,調用會加速 15% 到 20%。這也是一種好的作法,由於你能夠從方法聲明中看出調用該方法不須要更新此對象的狀態。從Smali指令級別來看,調用實例方法的指令時invoke-virtual 而調用靜態方法的指令是invoke-static.兩個指令的區別在於invoke-virtual須要多一個本地寄存器(用於存當前對象this)。後期會對smali指令集優化作詳細的介紹
     (4)避免內部 Getters/Setters
  在源生語言像 C++ 中,一般作法是用 Getters(i=getCount()) 代替直接字段訪問 (i=mCount)。這是 C++ 中一個好的習慣,由於編譯器會內聯這些訪問,而且若是須要約束或者調試這些域的訪問,你能夠在任什麼時候間添加代碼。而在 Android 中,這不是一個好的作法。虛方法調用的代價比直接字段訪問高昂許多。一般根據面嚮對象語言的實踐,在公共接口中使用 Getters 和 Setters 是有道理的,但在一個字段常常被訪問的類中宜採用直接訪問。無 JIT 時,直接字段訪問大約比調用 getter 訪問快 3 倍。有 JIT 時(直接訪問字段開銷等同於局部變量訪問),要快7倍。從Smali指令級別上來看,調用虛函數的指令時invoke-virtual,而直接訪問類變量的指令時iget,從官網的介紹來看,就說明iget指令的執行效果要比invoke-virtual高不少.
     (5)在屢次調用全局變量的函數裏,將全局變量賦值給本地變量html

  訪問成員變量比訪問本地變量慢得多,下面一段代碼:java

 

[java]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  1. for(int i =0; i <this.mCount; i++)  {  
  2.    dumpItem(this.mItems);  
  3. }  

        最好改爲這樣:android

[java]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  1. int count = this.mCount;  
  2. Item[] items = this.mItems;  
  3. for(int i =0; i < count; i++)  {  
  4.    dumpItems(items);  
  5. }  

  另外一個類似的原則是:永遠不要在 for 的第二個條件中調用任何方法。以下面方法所示,在每次循環的時候都會調用 getCount() 方法,這樣作比你在一個 int 先把結果保存起來開銷大不少。程序員

 

[java]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  1. for(int i =0; i < this.getCount(); i++) {  
  2.   dumpItems(this.getItem(i));  
  3.     }  

 

 

  一樣若是你要屢次訪問一個變量,也最好先爲它創建一個本地變量,例如:正則表達式

 
[java]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  1. public void useGlobalVariable() {  
  2.        mViewGroup.addStatesFromChildren();  
  3.     mViewGroup.bringToFront();  
  4.        mViewGroup.animate();  
  5.        mViewGroup.buildLayer();  
  6.    }  

  這裏有 4 次訪問成員變量 mViewGroup,若是將它緩存到本地,4 次成員變量訪問就會變成 4 次效率更高的本地變量訪問。
  另外就是方法的參數與本地變量的效率相同。(傳入參數和本地變量執行效果是相同的),在smli指令上來看,本地寄存器爲v0,v1,v2... ,參數寄存器爲p0,p1,p2
      若是不想再函數內頻繁申明本地變量,那最好將全局變量做爲參數傳入函數。和聲明本地變量效率是同樣的,甚至是更高。由於少了將全局變量賦給本地變量的指令執行。具體效率對比,後期慢慢來說。
性能優化
(1)對常量使用 static final 修飾符
  讓咱們來看看這兩段在類前面的聲明:算法

[java]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  1.            
  2. static int intVal = 42;  
  3. static String strVal = "Hello, world!";  
  4. static String strVal = "Hello, world!";  



必以其會生成一個叫作 clinit 的初始化類的方法,當類第一次被使用的時候這個方法會被執行。方法會將 42 賦給 intVal,而後把一個指向類中常量表的引用賦給 strVal。當之後要用到這些值的時候,會在成員變量表中查找到他們。 下面咱們作些改進,使用「final」:
[java]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  1. static final int intVal = 42;  
  2. static final String strVal = "Hello, world!";  

如今,類再也不須要 clinit 方法,由於在成員變量初始化的時候,會將常量直接保存到類文件中。用到 intVal 的代碼被直接替換成 42,而使用 strVal 的會指向一個字符串常量,而不是使用成員變量。
  將一個方法或類聲明爲 final 不會帶來性能的提高,可是會幫助編譯器優化代碼。舉例說,若是編譯器知道一個 getter 方法不會被重載,那麼編譯器會對其採用內聯調用。
  你也能夠將本地變量聲明爲 final,一樣,這也不會帶來性能的提高。使用「final」只能使本地變量看起來更清晰些(可是也有些時候這是必須的,好比在使用匿名內部類的時候)。看看編譯後的smali文件
        .field static intVal:I = 0x0
        .field static strVal:Ljava/lang/String;
       以上兩個指令是未聲明final的。咱們能夠發現,在編譯以後,兩個變量都是初始化值,並無賦給在Java文件中聲明的值,這就說明這些值只有在類文件被使用的時候,執行clinit的時候,纔會進行賦值
        .field static final sintVal:I = 0x1a6
        .field static final sstrVal:Ljava/lang/String; = "Hello, world!"
     而這兩個指令是聲明瞭final的指令,咱們能夠發現,編譯完成以後,這兩個變量已經具備了聲明的值,這就說明不須要Java文件執行clinit,而這兩個變量已經有了值,咱們稱之爲常量。
  
  (2)使用改進的 For 循環語法編程

  改進 for 循環(有時被稱爲 for-each 循環)可以用於實現了 iterable 接口的集合類及數組中。在集合類中,迭代器讓接口調用 hasNext() 和 next() 方法。在 ArrayList 中,手寫的計數循環迭代要快 3 倍(不管有沒有JIT),但其餘集合類中,改進的 for 循環語法和迭代器具備相同的效率。下面展現集中訪問數組的方法:數組

 

[java]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  1. static class Foo {  
  2.        int mSplat;  
  3.    }  
  4.    Foo[] mArray = ...  
  5.    public void zero() {  
  6.        int sum = 0;  
  7.        for (int i = 0; i < mArray.length; ++i) {  
  8.            sum += mArray[i].mSplat;  
  9.        }  
  10.    }  
  11.    public void one() {  
  12.        int sum = 0;  
  13.        Foo[] localArray = mArray;  
  14.        int len = localArray.length;  
  15.    
  16.        for (int i = 0; i < len; ++i) {  
  17.            sum += localArray[i].mSplat;  
  18.        }  
  19.    }  
  20.    public void two() {  
  21.        int sum = 0;  
  22.        for (Foo a : mArray) {  
  23.            sum += a.mSplat;  
  24.        }  
  25.    }  
  26. }  



 

  在 zero() 中,每次循環都會訪問兩次靜態成員變量,取得一次數組的長度。
  在 one() 中,將全部成員變量存儲到本地變量。
  two() 使用了在 Java1.5 中引入的 foreach 語法。編譯器會將對數組的引用和數組的長度保存到本地變量中,這對訪問數組元素很是好。 可是編譯器還會在每次循環中產生一個額外的對本地變量的存儲操做(對變量 a 的存取)這樣會比 one() 多出 4 個字節,速度要稍微慢一些。
       使用foreach方法循環效率是很高,可是在併發的環境下,頗有可能引發concurrentModifyException。

(3)避免使用浮點數
  一般的經驗是,在 Android 設備中,浮點數會比整型慢兩倍,在缺乏 FPU 和 JIT 的 G1 上對比有 FPU 和 JIT 的 Nexus One 中確實如此(兩種設備間算術運算的絕對速度差大約是 10 倍)從速度方面說,在現代硬件上,float 和 double 之間沒有任何不一樣。更普遍的 講,double 大 2 倍。在臺式機上,因爲不存在空間問題,double 的優先級高於 float。但即便是整型,有的芯片擁有硬件乘法,卻缺乏除法。這種狀況下,整型除法和求模運算是經過軟件實現的,就像當你設計 Hash 表,或是作大量的算術那樣,例如 a/2 能夠換成 a*0.5。

 (4)瞭解並使用類庫
  選擇 Library 中的代碼而非本身重寫,除了一般的那些緣由外,考慮到系統空閒時會用匯編代碼調用來替代 library 方法,這可能比 JIT 中生成的等價的最好的 Java 代碼還要好。
           i.    當你在處理字串的時候,不要吝惜使用 String.indexOf(),String.lastIndexOf() 等特殊實現的方法。這些方法都是使用 C/C++ 實現的,比起 Java 循環快 10 到 100 倍。
           ii.   System.arraycopy 方法在有 JIT 的 Nexus One 上,自行編碼的循環快 9 倍。
           iii.  android.text.format 包下的 Formatter 類,提供了 IP 地址轉換、文件大小轉換等方法;DateFormat 類,提供了各類時間轉換,都是很是高效的方法。
     詳細請參考 http://developer.android.com/reference/android/text/format/package-summary.html
           iv.  TextUtils 類
     對於字符串處理 Android 爲咱們提供了一個簡單實用的 TextUtils 類,若是處理比較簡單的內容不用去思考正則表達式不妨試試這個在 android.text.TextUtils 的類,詳細請參考http://developer.android.com/reference/android/text/TextUtils.html
            v.  高性能MemoryFile類。
  不少人抱怨 Android 處理底層 I/O 性能不是很理想,若是不想使用 NDK 則能夠經過 MemoryFile 類實現高性能的文件讀寫操做。MemoryFile 適用於哪些地方呢?對於 I/O 須要頻繁操做的,主要是和外部存儲相關的 I/O 操做,MemoryFile 經過將 NAND 或 SD 卡上的文件,分段映射到內存中進行修改處理,這樣就用高速的 RAM 代替了 ROM 或 SD 卡,性能天然提升很多,對於 Android 手機而言同時還減小了電量消耗。該類實現的功能不是不少,直接從 Object 上繼承,經過 JNI 的方式直接在 C 底層執行。
詳細請參考 http://developer.android.com/reference/android/os/MemoryFile.html
在此,只簡單列舉幾個經常使用的類和方法,更多的是要靠平時的積累和發現。多閱讀 Google 給的幫助文檔時頗有益的。
(5)合理利用本地方法
  本地方法並非必定比 Java 高效。最起碼,Java 和 native 之間過渡的關聯是有消耗的,而 JIT 並不能對此進行優化。當你分配本地資源時(本地堆上的內存,文件說明符等),每每很難實時的回收這些資源。同時你也須要在各類結構中編譯你的代碼(而非依賴 JIT)。甚至可能須要針對相同的架構 來編譯出不一樣的版本:針對 ARM 處理器的 GI 編譯的本地代碼,並不能充分利用 Nexus One 上的 ARM,而針對 Nexus One 上 ARM 編譯的本地代碼不能在 G1 的 ARM 上運行。當你想部署程序到存在本地代碼庫的 Android 平臺上時,本地代碼才顯得尤其有用,而並不是爲了 Java 應用程序的提速。
(6)複雜算法儘可能用 C 完成
  複雜算法儘可能用 C 或者 C++ 完成,而後用 JNI 調用。可是若是是算法比較單間,沒必要這麼麻煩,畢竟 JNI 調用也會花必定的時間。請權衡。
(7)減小沒必要要的全局變量
  儘可能避免 static 成員變量引用資源耗費過多的實例,好比 Context ,避免內存泄露(後面針對內存泄露會有詳細介紹)。Android 提供了很健全的消息傳遞機制 (Intent) 和任務模型 (Handler),能夠經過傳遞或事件的方式,防止一些沒必要要的全局變量。
(8)不要過多期望 GC
  Java 的 gc 使用的是一個有向圖,判斷一個對象是否有效看的是其餘的對象能到達這個對象的頂點,有向圖的相對於鏈表、二叉樹來講開銷是可想而知。因此不要過多期望 gc。將不用的對象能夠把它指向 NULL,並注意代碼質量。同時,顯示讓系統 gc 回收,例如圖片處理時,緩存

if(bitmap.isRecycled()==false) {
   bitmap.recycle();
}

(9)瞭解 Java 四種引用方式
  JDK 1.2 版本開始,把對象的引用分爲 4 種級別,從而使程序能更加靈活地控制對象的生命週期。這 4 種級別由高到低依次爲:強引用、軟引用、弱引用和虛引用。
  i.    強引用(StrongReference)
  強引用是使用最廣泛的引用。若是一個對象具備強引用,那垃圾回收器毫不會回收它。當內存空間不足,Java 虛擬機寧願拋出 OutOfMemoryError 錯誤,使程序異常終止,也不會靠隨意回收具備強引用的對象來解決內存不足的問題。
  ii.    軟引用(SoftReference)
  若是一個對象只具備軟引用,則內存空間足夠,垃圾回收器就不會回收它;若是內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就能夠被程序使用。軟引用可用來實現內存敏感的高速緩存。
  iii.    弱引用(WeakReference)
  在垃圾回收器線程掃描它所管轄的內存區域的過程當中,一旦發現了只具備弱引用的對象,無論當前內存空間足夠與否,都會回收它的內存。不過,因爲垃圾回收器是一個優先級很低的線程,所以不必定會很快發現那些只具備弱引用的對象。
  iv.    虛引用(PhantomReference)
  顧名思義,就是形同虛設。與其餘幾種引用都不一樣,虛引用並不會決定對象的生命週期。若是一個對象僅持有虛引用,那麼它就和沒有任何引用同樣,在任什麼時候候均可能被垃圾回收器回收。瞭解並熟練掌握這 4 中引用方式,選擇合適的對象應用方式,對內存的回收是頗有幫助的。
 
(10)使用實體類比接口好
假設你有一個 HashMap 對象,你能夠將它聲明爲 HashMap 或者 Map:
Map map1 = new HashMap();
HashMap map2 = new HashMap();
哪一個更好呢?
按照傳統的觀點 Map 會更好些,由於這樣你能夠改變他的具體實現類,只要這個類繼承自 Map 接口。傳統的觀點對於傳統的程序是正確的,可是它並不適合嵌入式系統,由於這涉及到一個上下轉型的問題。調用一個接口的引用會比調用實體類的引用多花費一倍的時間。若是 HashMap 徹底適合你的程序,那麼使用 Map 就沒有什麼價值。若是有些地方你不能肯定,先避免使用 Map,剩下的交給 IDE 提供的重構功能好了。(固然公共 API 是一個例外:一個好的 API 經常會犧牲一些性能)
(11)避免使用枚舉
枚舉變量很是方便,但不幸的是它會犧牲執行的速度和並大幅增長文件體積。例如:
public class Foo {
   public enum Shrubbery { GROUND, CRAWLING, HANGING }
}

會產生一個900字節的.class文件(Foo$Shubbery.class)。在它被首次調用時,這個類會調用初始化方法來準備每一個枚舉變 量。每一個 枚舉項都會被聲明成一個靜態變量,並被賦值。而後將這些靜態變量放在一個名爲」$VALUES」的靜態數組變量中。而這麼一大堆代碼,僅僅是爲了使用三個 整數。
這樣:Shrubbery shrub =Shrubbery.GROUND;會引發一個對靜態變量的引用,若是這個靜態變量是 final int,那麼編譯器會直接內聯這個常數。
一方面說,使用枚舉變量可讓你的 API 更出色,並能提供編譯時的檢查。因此在一般的時候你毫無疑問應該爲公共 API 選擇枚舉變量。可是當性能方面有所限制的時候,你就應該避免這種作法了。性能優化

有些狀況下,使用 ordinal() 方法獲取枚舉變量的整數值會更好一些,舉例來講:

 

[java]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  1. for(int n =0; n < list.size(); n++) {  
  2.    if(list.items[n].e == MyEnum.VAL_X) {  
  3.        // do something  
  4.    } else if(list.items[n].e == MyEnum.VAL_Y) {  
  5.        // do something  
  6.    }  
  7. }  

替換爲:

 

[java]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  1. int valX = MyEnum.VAL_X.ordinal();  
  2. int valY = MyEnum.VAL_Y.ordinal();  
  3. int count = list.size();  
  4. MyItem items = list.items();  
  5. for(int n =0; n < count; n++) {  
  6.    intvalItem = items[n].e.ordinal();  
  7.    if(valItem == valX) {  
  8.        // do something  
  9.    } else if(valItem == valY) {  
  10.        // do something  
  11.    }  
  12. }  

會使性能獲得一些改善,但這並非最終的解決之道。


(12)將與內部類一同使用的變量聲明在包範圍內

請看下面的類定義:

 

[java]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  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.    public void run() {    
  10.    Inner in = new Inner();    
  11.        mValue = 27;    
  12.        in.stuff();    
  13.    }    
  14.         
  15.    private void doStuff(int value) {    
  16.        System.out.println("Value is " + value);    
  17.    }    
  18. }  



 

這其中的關鍵是,咱們定義了一個內部類(Foo$Inner),它須要訪問外部類的私有域變量和函數。這是合法的,而且會打印出咱們但願的結果 Value is 27。問題是在技術上來說(在幕後)Foo$Inner 是一個徹底獨立的類,它要直接訪問 Foo 的私有成員是非法的。要跨越這個鴻溝,編譯器須要生成一組方法:

 

/*package*/ static int Foo.access$100(Foo foo) {  
/*package*/ static void Foo.access$200(Foo foo, int value) {  
}
內部類在每次訪問 mValue 和 doStuff 方法時,都會調用這些靜態方法。就是 說,上面的代碼說明了一個問題,你是在經過接口方法訪問這些成員變量和函數而不是直接調用它們。在前面咱們已經說過,使用接口方法(getter、 setter)比直接訪問速度要慢。因此這個例子就是在特定語法下面產生的一個「隱性的」性能障礙。
經過將內部類訪問的變量和函數聲明由私有範圍改成包範圍,咱們能夠避免這個問題。這樣作能夠 讓代碼運行更快,而且避免產生額外的靜態方法。(遺憾的是,這些域和方法能夠被同一個包內的其餘類直接訪問,這與經典的 OO 原則相違背。所以當你設計公共  API 的時候應該謹慎使用這條優化原則)。

(13)緩存
適量使用緩存,不要過量使用,由於內存有限,能保存路徑地址的就不要存放圖片數據,不常用的儘可能不要緩存,不用時就清空。在一些比較耗時的算法,且執行會有若干次的地方,加入LruCache,後期會詳細講解緩存的使用和注意事項

 

(14)關閉資源對象

對 SQLiteOpenHelper,SQLiteDatabase,Cursor,文件,I/O 操做等都應該記得顯示關閉。

  好了,以上的一些內容都是在編寫Android應用的時候,最基本須要注意的優化方面的知識。以後的文章,將會深刻講解在Android應用開發中的各個方面的性能優化問題。

 

 

摘自:http://blog.csdn.net/litton_van/article/details/21702299

相關文章
相關標籤/搜索