Android優化(一)_Java代碼優化_從優化斐波那契數列帶來的思路

性能優化總綱:

大概會花一個月左右的時間出7-8個專題來分享一下在工作和學習中積累下來的android性能優化經驗

希望大家會持續關注。

現在是專題一:java代碼優化

但這也僅僅是為大家提供一些思路與較為全面的總結,算不上什么,希望有錯誤或問題在下面評論。

最后完結以后會將思維導圖與優化框架整理出來,請期待。

題記:

如何確保Java應用在Android設備上獲得高性能?首先要做的:知道Android是如何來執行代碼的,然后再體會一下所謂的優化技巧,以及一些提高應用響應速度和高效使用數據庫的技巧。

不過,你應該意識到,代碼優化不是應用開發者的首要任務,提供良好的用戶體驗并且專注于代碼的可維護性,才是我們的首要任務。事實上,代碼優化應該最后才做,如果你的程序自我感覺達到一個可以接受的水平,甚至不需要代碼優化。

一、我們先來看看Android是如何來執行代碼的

  • Android→Java代碼→Java字節碼→Dalvik字節碼→Dalvik虛擬機(4.4之前)

  • Android→Java代碼→Java字節碼→機器碼(5.0之后)

i: Android 4.4 中谷歌為開發者提供了兩種編譯模式,一種是默認的Dalvik模式,而另外一種則是ART模式,5.0廢棄Dalvik。

ii:本地代碼直接由CPU執行,而不必由虛擬機解釋執行;本地代碼可以為特定架構予以優化。

iii:從用戶的角度來看,如果可以在100ms或者更短的時間內計算完成,那就是瞬時計算。

JIT與Dalvik

JIT是"Just In Time Compiler"的縮寫,就是"即時編譯技術",與Dalvik虛擬機相關。


怎么理解這句話呢?這要從Android的一些特性說起。

JIT是在2.2版本提出的,目的是為了提高Android的運行速度,一直存活到4.4版本,因為在4.4之后的ROM中,就不存在Dalvik虛擬機了。

我們使用Java開發android,在編譯打包APK文件時,會經過以下流程

  • Java編譯器將應用中所有Java文件編譯為class文件
  • dx工具將應用編譯輸出的類文件轉換為Dalvik字節碼,即dex文件
  • 之后經過簽名、對齊等操作變為APK文件。

Dalvik虛擬機可以看做是一個Java VM,他負責解釋dex文件為機器碼,如果我們不做處理的話,每次執行代碼,都需要Dalvik將dex代碼翻譯為微處理器指令,然后交給系統處理,這樣效率不高。

為了解決這個問題,Google在2.2版本添加了JIT編譯器,當App運行時,每當遇到一個新類,JIT編譯器就會對這個類進行編譯,經過編譯后的代碼,會被優化成相當精簡的原生型指令碼(即native code),這樣在下次執行到相同邏輯的時候,速度就會更快。

當然使用JIT也不一定加快執行速度,如果大部分代碼的執行次數很少,那么編譯花費的時間不一定少于執行dex的時間。Google當然也知道這一點,所以JIT不對所有dex代碼進行編譯,而是只編譯執行次數較多的dex為本地機器碼。

有一點需要注意,那就是dex字節碼翻譯成本地機器碼是發生在應用程序的運行過程中的,并且應用程序每一次重新運行的時候,都要做重做這個翻譯工作,所以這個工作并不是一勞永逸,每次重新打開App,都需要JIT編譯。

另外,Dalvik虛擬機從Android一出生一直活到4.4版本,而JIT在Android剛發布的時候并不存在,在2.2之后才被添加到Dalvik中。


ART與AOT

AOT是"Ahead Of Time"的縮寫,指的就是ART(Anroid RunTime)這種運行方式。


前面介紹過,JIT是運行時編譯,這樣可以對執行次數頻繁的dex代碼進行編譯和優化,減少以后使用時的翻譯時間,雖然可以加快Dalvik運行速度,但是還是有弊病,那就是將dex翻譯為本地機器碼也要占用時間,所以Google在4.4之后推出了ART,用來替換Dalvik。

在4.4版本上,兩種運行時環境共存,可以相互切換,但是在5.0+,Dalvik虛擬機則被徹底的丟棄,全部采用ART。

ART的策略與Dalvik不同,在ART 環境中,應用在第一次安裝的時候,字節碼就會預先編譯成機器碼,使其成為真正的本地應用。之后打開App的時候,不需要額外的翻譯工作,直接使用本地機器碼運行,因此運行速度提高。

當然ART與Dalvik相比,還是有缺點的。

  • ART需要應用程序在安裝時,就把程序代碼轉換成機器語言,所以這會消耗掉更多的存儲空間,但消耗掉空間的增幅通常不會超過應用代碼包大小的20%
  • 由于有了一個轉碼的過程,所以應用安裝時間難免會延長

但是這些與更流暢的Android體驗相比而言,不值一提。

通過前面背景知識的介紹,我終于可以更簡單的介紹這四個名詞之間的關系了:

  • JIT代表運行時編譯策略,也可以理解成一種運行時編譯器,是為了加快Dalvik虛擬機解釋dex速度提出的一種技術方案,來緩存頻繁使用的本地機器碼
    .
  • ART和Dalvik都算是一種Android運行時環境,或者叫做虛擬機,用來解釋dex類型文件。但是ART是安裝時解釋,Dalvik是運行時解釋
    .
  • AOT可以理解為一種編譯策略,即運行前編譯,ART虛擬機的主要特征就是AOT

二、幾點優化技巧

優化思路一:

微小的優化:當n等于0或者1的時候直接返回n,而不是在另外一個if語句中來檢查n是否等于0或1.

public class Fibonacci{
     public static long computeRecursively(int n){
          if(n>1) return computeRecursivelv(n-2) + computeRecursivelv(n-1);
          return n;
     }
}

優化思路二:以優化斐波那契數列為例,簡單談談思想


1、首次優化是消除一個方法調用

public class Fibonacci{
     public static long computeRecursively(int n){
          if(n>1) {
               long result = 1;
                   do {
                        result += computeRecursivelyWithLoop(n-2);
                        n--;
                       }while (n>1)
                        return result;
                       }
                       return n;
                      }
            }

2、第二次優化會換成迭代實現:尤其是在沒有多少內存的時候,遞歸算法往往要消耗大量棧空間,有可能導致棧溢出,讓應用崩潰。

public class Fibonacci{
     public static long computeRecursively(int n){
          if(n>1) {
               long a= 0,b = 1;
               do {
                long tmp = b;
                b += a;
                a = amp;
    
               }while (--n>1)
                return b;
           }
   return n;
  }
}

3、 到三次稍加修改,每次迭代計算兩項,迭代總數少了一半。由于long型只有64位,在斐波拉契數列的第92項,會出現溢出,導致結果錯誤,第93項會變成負的。

    public class Fibonacci{
         public static long computeRecursively(int n){
          if(n>1) {
               long a= 0,b = 1;
           n--;
           a = n & 1;
           n /= 2;


           while (n-->0){
            a += b;
            b += a;
   
           }
           return b;
          }
          return n;
}

4、第四次用BigInteger,保證了不會溢出,但是速度再一次降了下來:1、BigInteger是不可變的 2、BigInteger使用BigInt和本地代碼實現 3、數字越大,相加運算所花的時間越大

public class Fibonacci{
 public static BigInteger computeIterativelvFasterUsingBigInteger(int n){
  if(n>1) {
   BigInteger a,b = BigInteger.ONE;
   n--;
   a = BigInteger.valueOf(n & 1);
   n /= 2;


   while (n-->0){
    a=a.add(0);
    b=b.add(a);
   
   }
   return b;
  }
  return n==0?BigInteger.ZERO : BigInteger.ONE;
}

5、第五次改進算法來減少分配數量。基于斐波那契Q-矩陣,我們會有一個算法公式來加快速度。


6、第六次使用BigInteger和基本類型Long的快速遞歸實現

            當n>92時才使用BigInteger來進行運算,這樣我們做以上運算會快20倍。

7、第七次使用BigInteger和預先計算結果遞歸快速實現......

> 好了,到了這里應該發現優化往往使源代碼更難于閱讀、理解和維護,而且,會有越來越少的人來能理解你寫的代碼的含義,代碼復雜到筆者已經不想寫了。而且,好的算法是無窮無盡的,只不過可能更復雜罷了,那我們費盡力氣計算出來的結果,不能白白浪費(代價太高了),所以我們考慮到了緩存。

result = cache.get(n);//輸入參數n作為鍵

if(result = null){

    //如果在緩存中沒有result值,就計算出來存進去    

    result = computeResult(n);

    cache.put(n,result);//n作為鍵,result作為值

}

return result;



8、考慮計算代價過高,最好把結果緩存起來,安卓定義的SparseArray類,比HashMap更高效(Integer和int區別)

public class Fibonacci{
 public static BigInteger computeRecursivelyWith Cache(int n){
  SparseArray<BigInteger> cache = new SparseArray<BigInteger>();
  return computeRecursivelyWithCache(n,cache);
    }


    private static BigInteger computeRecursivelyWithCache(int n,SparseArray<BigInteger> cache){
     if(n>92) {
   BigInteger fN = cache.get(n);
    if(fN == null){
     int m = (n/2) +(n&1);
     BigIntger fM = computeRecursiveWithCache(m,cache);
     BigIntger fM_1 = computeRecursiveWithCache(m - 1,cache);
     if((n&1)==1){
     fN = fm.pow(2).add(fM_1.pow(2));
     }else{
      fN = fM_1.shiftLeft(1).add(fM).multiply(fM);
     }
     cache.put(n,fN);
    }
    return fN;
  
   
   }
   return BigInteger.valueOf(iterativeFaster(n));
  }
  private static long iterativeFaster(int n){
   ...
  }
    } 
}

另外值得一提的是LRUCache算法,同樣對應著一個MRUCache算法

這個類是Android3.1引入的,可以在創建的時候自定義緩存的長度,另外,可以通過復寫sizeof()方法改變每個緩存條目計算大小的方式。

  • LRU(Least Recently Used)緩存縣丟棄最近最少使用的項目,不過在某些分情境中我們還可能用到MRUcache丟棄最近最多使用的項目。這兩種算法現在在這里不深入討論,等以后有機會分享數據結構在詳談。

最后我們得出一個結論,我們對一個場景進行優化,往往有很多方式,但是某一種實現一般不是最好的解決方式,最好的結果就是結合多種不同的技術,而不是只依賴于其中一個,例如更快的實現可以用預計算、緩存機制、甚至采用不同的數學公式。


三、API

一般我們在manifest中應該使用<uses-asd>元素制定以下三個重要的信息

  • 最低API等級(mainSdkVersion)
  • 期望API等級(targetSdkVersion)
  • 最高API等級(maxSdkVersion)

要注意一點 :過期的不能使用

另外注意:可以用新API來獲取最好的性能,也可以在舊平臺上正常運行

例如:

android6.0權限問題

android3.0以下兼容屬性動畫等。

sparseArray的使用:

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB){

    sparseArray.removeAt(1);//11以上

} else {

    int key = sparseArray.keyAt(1);//默認實現慢一些

    ?sparseArray.remove(key);

}

如不想用上面的方法來檢測版本號,還可以用反射來確認是否有特定方法,但是有一點,在性能至關主要的地方應盡量避免使用反射。替代的辦法是在靜態初始化代碼塊里調用Class.forName()和Class.getMethod()確認指定方法時否存在,在性能要求高的地方只調用Method.invoke()就好了。


四、數據結構

通過上面斐波那契數列實現證明,好的算法和數據結構是實現快速應用的關鍵。java.util包中已經定義好了很多我們可以隨手拿來用的工具了,比如各種集合。Android還定義了一些為了解決性能問題而生的類:

  • LruCache
  • SparseArray
  • SparseBooleanArray
  • SparseIntArray
  • Pair
數據結構還是和上面一樣等以后有機會再討論。

五、響應能力

讓用戶真正感覺到快才行,比如延遲加載技術

通常我們的做法是在組件的onCreate()方法中執行所有初始化。雖然這樣做可行,但這意味著onCreate()需要較長的時間才能結束。

這一點對應用的ACtivity尤為重要,onStart()直到onCreate()方法之后才會被調用(同樣,onResume()只有在onStart()完成之后才會被調用).

任何的延遲都會導致應用需要較長時間才嗯那個啟動,用戶最終可能會感到難以忍受。

  • 讓你的主線程只做下面這種事情:

    • 按鍵接收

    • 繪制View

    • 產生生命周期方法

來簡單說一下用戶感受:當用戶感覺到你的應用有卡頓的時候,好感度就會降低,到一定臨界點后,就再見了.

那么,什么時候才是感覺到卡頓的,一般我們人眼看到的圖像幀率為60fps的時候,會感到比較流暢,換算成時間就是0.016s/幀,如果你的應用某個點再0.016s之內沒有渲染完成,就會造成所謂的卡頓,那么從優化的角度來說,除了改變GPU,我們能做的事情,就是減少布局的嵌套與ViewStub推遲對象創建。當然你可以用視圖樹來檢測,那不是在代碼優化的范圍內了,所以知道就好。

Android使用android.view.ViewStub來推遲初始化,它可以在運行時展開資源。當View-Stub需要展現時,它被相應的資源展開替換,自己就成為得待垃圾回收的對象。

由于內存分配需要花時間,等到對象真正需要時才進行分配,也是一個很好的選擇。當某個對象并不是立即就要使用時,推遲創建對象有著很明顯的好處。下面代碼是退出初始化的示例:為了避免總是檢查對象是否為空,考慮使用工廠方法模式。

int n = 100;
if(cache == null){
 //createCache分配緩存對象,可以從許多地方調用它
 cache = createCache();
}
BigInteger fN = cache.get(n);
if(fN == null){
 fN = Fibonacci.computeRecurivelyWithCache(n);
 cache.put(n,fN);
}

六、SQLite:

大多數應用都不會是SQLite的重度使用者,因此,不用太擔心與數據庫打交道時的性能(對數據庫有大量使用請參考我的另一篇文章《三個方面解決性能問題》)。不過。在優化應用中SQLite相關的代碼時,需要了解幾個概念:

1、SQLite語句

2、事務

3、查詢

因為普通的sql語句是簡單的字符串,需要解釋或者編譯才可以執行。當你執行SQL語句時,例如:

SQLiteDatabase db = SQLiteDatabase.create(null);//數據庫在內存中
db.execSQL("CREATE TABLE cheese(name TEXT,origin TEXT)");
db.execSQL("INSERT INTO cheeese VALUES ('Roquefort','Roquefort-sur-Solulzon')");
db.close();//關閉數據庫

事實證明,執行SQLite的語句可能需要一段較長時間。除了編譯,語句本身還需要創建。現在我們只關心INSERT的性能,畢竟,表只會創建一次,但會添加,修改,刪除多次。

例如:String sql = "INSERT INTO cheese VALUES(\"" +name +"\",\"" + origin +"\")"';
  • 這樣的650條數據到內存數據庫中用時393ms,平均一條0.6ms。

那么第一個優化方案是用StringBuilder或String.format()來替代“+”

builder.appended(name).append("\",\"").addped(origin).append("\")");
String sql = String.format("INSERT INTO cheese VALUES(\"%s\",\"%s\")")",name .origin);




  • 上面這兩個方案大概能快幾十毫秒。

第二種方案:我們發現所有的語句都非常相似,所以可以使用一個語句,讓一部分在循環外只編譯一次:

public void populateWithCompileStatement(){
 SQLiteStatement stmt = db.compileStatement("INSERT INTO cheese VALUES(?,?)");
 int i = 0;
     for(String name : sCheeseNames){
          String origin = sCheeseOrigins[i++];
          stmt.clearBingings();
          stmt.bindString(1,name);//替換第一個問號name
          stmt.bingString(2,origin);//替換第二個問號為origin
          stmt.executeInsert();
     }
}

  • 因為只進行了一次語句編譯,而不是650次,并且綁定值是比編譯更輕量級的操作,所以這種方法明顯快多了、總共用時269ms。

事務:上述例子并沒有顯示創建任何事務,但會自動為每個插入操作創建一個事務,并在每次插入后立即提交。顯示創建事務有以下兩個基本特征:

原子提交

性能更好

拋開對性能的追求,第一個特性是很重要的。原子提交意味著數據庫的所有修改都完成或都不做。事務不會只提交部分修改。如上面代碼,加入事務之后

try{

db.beginTransaction();

SQLiteStatement stmt = db.compileStatement("INSERT INTO cheese VALUES(?,?)");
 int i = 0;
     for(String name : sCheeseNames){
          String origin = sCheeseOrigins[i++];
          stmt.clearBingings();
          stmt.bindString(1,name);//替換第一個問號name
          stmt.bingString(2,origin);//替換第二個問號為origin
          stmt.executeInsert();
     }

db.setTransactionSuccessful();//刪除這一調用不會提交任何改動!

} catch(e..){

//異常處理

}finally{

db.endTransaction();//必須寫在finally里

}


查詢:我們可以用限制數據庫的訪問方式來加快查詢速度,尤其是對存儲中的數據庫。數據庫查詢僅會返回一個cursor(游標)對象,然后用它來遍歷結果。

查詢的時候,盡量只讀取需要的數據。例如假設我們的表有兩列,name和origin:

db.query("cheese,null,null,null,null,null,null");(1)

db.query("cheese",new String[]{"name"},null,null,null,null,null);(2)

查詢一定量的數據兩種方法分別用時61ms,23ms

所以,可以肯定,只讀取需要的數據才是上上之選。


七、總結:

幾年前,java由于性能問題而廣受詬病,現在情況已大有改觀。

每次發布新版本Android時,Dalvik虛擬機(包括它的JIT編譯器)的性能都會有所提升。

代碼可以編譯為本地代碼,從而利用最新的CPU架構,而不必重新編譯。

雖然實現很重要,但最重要的還是慎選數據結構和算法。

好的算法可以彌補差的實現,甚至不需要優化就可以使應用流暢運行;而壞的算法無論你在實現上花費多少精力,其結果還是會很糟糕。

最后,響應順暢是成功的關鍵。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容