Android開發之性能優化(2)

1.      內存優化

Android系統對每個軟件所能使用的RAM空間進行了限制(如:Nexus one 對每個軟件的內存限制是24M),同時Java語言本身比較消耗內存,dalvik虛擬機也要佔用一定的內存空間,所以合理使用內存,彰顯出一個程序員的素質和技能。


1)       瞭解JIT

即時編譯(Just-in-time Compilation,JIT),又稱動態轉譯(Dynamic Translation),是一種通過在運行時將字節碼翻譯爲機器碼,從而改善字節碼編譯語言性能的技術。即時編譯前期的兩個運行時理論是字節碼編譯和動態編譯。Android原來Dalvik虛擬機是作爲一種解釋器實現,新版(Android2.2+)將換成JIT編譯器實現。性能測試顯示,在多項測試中新版本比舊版本提升了大約6倍。

詳細請參考http://hi.baidu.com/cool_parkour/blog/item/2802b01586e22cd8a6ef3f6b.html

 

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%。這也是一種好的做法,因爲你可以從方法聲明中看出調用該方法不需要更新此對象的狀態。

 

4)       避免內部Getters/Setters

在源生語言像C++中,通常做法是用Getters(i=getCount())代替直接字段訪問(i=mCount)。這是C++中一個好的習慣,因爲編譯器會內聯這些訪問,並且如果需要約束或者調試這些域的訪問,你可以在任何時間添加代碼。

而在Android中,這不是一個好的做法。虛方法調用的代價比直接字段訪問高昂許多。通常根據面嚮對象語言的實踐,在公共接口中使用Getters和Setters是有道理的,但在一個字段經常被訪問的類中宜採用直接訪問。

無JIT時,直接字段訪問大約比調用getter訪問快3倍。有JIT時(直接訪問字段開銷等同於局部變量訪問),要快7倍。

 

5)       將成員緩存到本地

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

[java] view plaincopy
  1. for(int i =0; i <this.mCount; i++)  {  
  2. dumpItem(this.mItems);  
  3. }  


最好改成這樣:

[java] view plaincopy
  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 plaincopy
  1. for(int i =0; i < this.getCount(); i++) {  
  2. dumpItems(this.getItem(i));  
  3. }  



同樣如果你要多次訪問一個變量,也最好先爲它建立一個本地變量,例如:

[java] view plaincopy
  1. protected void drawHorizontalScrollBar(Canvas canvas, int width, int height) {  
  2. if(isHorizontalScrollBarEnabled()) {  
  3. intsize = mScrollBar.getSize(false);  
  4. if(size <=0) {  
  5.        size = mScrollBarSize;  
  6. }  
  7. mScrollBar.setBounds(0, height - size, width, height);  
  8. mScrollBar.setParams(computeHorizontalScrollRange(), computeHorizontalScrollOffset(), computeHorizontalScrollExtent(),false);  
  9. mScrollBar.draw(canvas);  
  10. }  
  11. }  

這裏有4次訪問成員變量mScrollBar,如果將它緩存到本地,4次成員變量訪問就會變成4次效率更高的棧變量訪問。

另外就是方法的參數與本地變量的效率相同。

 

1)       對常量使用static final修飾符

讓我們來看看這兩段在類前面的聲明:

[java] view plaincopy
  1. static int intVal = 42;  
  2. static String strVal = "Hello, world!";  

必以其會生成一個叫做clinit的初始化類的方法,當類第一次被使用的時候這個方法會被執行。方法會將42賦給intVal,然後把一個指向類中常量表的引用賦給strVal。當以後要用到這些值的時候,會在成員變量表中查找到他們。 下面我們做些改進,使用“final”關鍵字:

[java] view plaincopy
  1. static final int intVal = 42;  
  2. static final String strVal = "Hello, world!";  

現在,類不再需要clinit方法,因爲在成員變量初始化的時候,會將常量直接保存到類文件中。用到intVal的代碼被直接替換成42,而使用strVal的會指向一個字符串常量,而不是使用成員變量。

將一個方法或類聲明爲final不會帶來性能的提升,但是會幫助編譯器優化代碼。舉例說,如果編譯器知道一個getter方法不會被重載,那麼編譯器會對其採用內聯調用。

你也可以將本地變量聲明爲final,同樣,這也不會帶來性能的提升。使用“final”只能使本地變量看起來更清晰些(但是也有些時候這是必須的,比如在使用匿名內部類的時候)。

 

2)       使用改進的For循環語法

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

[java] view plaincopy
  1. static class Foo {  
  2.         int mSplat;  
  3.     }  
  4.     Foo[] mArray = ...  
  5.    
  6.     public void zero() {  
  7.         int sum = 0;  
  8.         for (int i = 0; i < mArray.length; ++i) {  
  9.             sum += mArray[i].mSplat;  
  10.         }  
  11.     }  
  12.    
  13.     public void one() {  
  14.         int sum = 0;  
  15.         Foo[] localArray = mArray;  
  16.         int len = localArray.length;  
  17.    
  18.         for (int i = 0; i < len; ++i) {  
  19.             sum += localArray[i].mSplat;  
  20.         }  
  21.     }  
  22.    
  23.     public void two() {  
  24.         int sum = 0;  
  25.         for (Foo a : mArray) {  
  26.             sum += a.mSplat;  
  27.         }  
  28. }  
  29. }  

在zero()中,每次循環都會訪問兩次靜態成員變量,取得一次數組的長度。

在one()中,將所有成員變量存儲到本地變量。

two()使用了在java1.5中引入的foreach語法。編譯器會將對數組的引用和數組的長度保存到本地變量中,這對訪問數組元素非常好。但是編譯器還會在每次循環中產生一個額外的對本地變量的存儲操作(對變量a的存取)這樣會比one()多出4個字節,速度要稍微慢一些。

 

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回收,例如圖片處理時,


[java] view plaincopy
  1. if(bitmap.isRecycled()==false) { //如果沒有回收  
  2.      bitmap.recycle();  
  3. }  

 

9)       瞭解Java四種引用方式

JDK 1.2版本開始,把對象的引用分爲4種級別,從而使程序能更加靈活地控制對象的生命週期。這4種級別由高到低依次爲:強引用、軟引用、弱引用和虛引用。

                     i.    強引用(StrongReference)

強引用是使用最普遍的引用。如果一個對象具有強引用,那垃圾回收器絕不會回收它。當內存空間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足的問題。


                     ii.    軟引用(SoftReference)

如果一個對象只具有軟引用,則內存空間足夠,垃圾回收器就不會回收它;如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現內存敏感的高速緩存。


                   iii.    弱引用(WeakReference)

在垃圾回收器線程掃描它所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過,由於垃圾回收器是一個優先級很低的線程,因此不一定會很快發現那些只具有弱引用的對象。


                    iv.    虛引用(PhantomReference)

顧名思義,就是形同虛設。與其他幾種引用都不同,虛引用並不會決定對象的生命週期。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。

瞭解並熟練掌握這4中引用方式,選擇合適的對象應用方式,對內存的回收是很有幫助的。

詳細請參考http://blog.csdn.net/feng88724/article/details/6590064

             

10)     使用實體類比接口好

假設你有一個HashMap對象,你可以將它聲明爲HashMap或者Map:

[java] view plaincopy
  1. Map map1 = new HashMap();  
  2. HashMap map2 = new HashMap();  

哪個更好呢?

按照傳統的觀點Map會更好些,因爲這樣你可以改變他的具體實現類,只要這個類繼承自Map接口。傳統的觀點對於傳統的程序是正確的,但是它並不適合嵌入式系統。調用一個接口的引用會比調用實體類的引用多花費一倍的時間。如果HashMap完全適合你的程序,那麼使用Map就沒有什麼價值。如果有些地方你不能確定,先避免使用 Map,剩下的交給IDE提供的重構功能好了。(當然公共API是一個例外:一個好的API常常會犧牲一些性能)

 

11)     避免使用枚舉

枚舉變量非常方便,但不幸的是它會犧牲執行的速度和並大幅增加文件體積。例如:

[java] view plaincopy
  1. public class Foo {  
  2.        public enum Shrubbery { GROUND, CRAWLING, HANGING }  
  3. }  

會產生一個900字節的.class文件(Foo$Shubbery.class)。在它被首次調用時,這個類會調用初始化方法來準備每個枚舉變量。每個枚舉項都會被聲明成一個靜態變量,並被賦值。然後將這些靜態變量放在一個名爲”$VALUES”的靜態數組變量中。而這麼一大堆代碼,僅僅是爲了使用三個整數。

這樣:Shrubbery shrub =Shrubbery.GROUND;會引起一個對靜態變量的引用,如果這個靜態變量是final int,那麼編譯器會直接內聯這個常數。

一方面說,使用枚舉變量可以讓你的API更出色,並能提供編譯時的檢查。所以在通常的時候你毫無疑問應該爲公共API選擇枚舉變量。但是當性能方面有所限制的時候,你就應該避免這種做法了。

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

[java] view plaincopy
  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 plaincopy
  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 plaincopy
  1. public class Foo {  
  2.            public class Inner {  
  3.                 public void stuff() {  
  4.                        Foo.this.doStuff(Foo.this.mValue);  
  5.                 }  
  6.            }  
  7.            private int mValue;  
  8.            public void run() {  
  9.                 Inner in = new Inner();  
  10.                 mValue = 27;  
  11.                 in.stuff();  
  12.            }  
  13.            private void doStuff(int value) {  
  14.                  System.out.println("value:"+value);  
  15.            }  
  16. }  

需要注意的關鍵是:我們定義的一個私有內部類(Foo$Inner),直接訪問外部類中的一個私有方法和私有變量。這是合法的,代碼也會打印出預期的“Value is 27”。

但問題是,虛擬機認爲從Foo$Inner中直接訪問Foo的私有成員是非法的,因爲他們是兩個不同的類,儘管Java語言允許內部類訪問外部類的私有成員,但是通過編譯器生成幾個綜合方法來橋接這些間隙的。

[java] view plaincopy
  1. /*package*/static int Foo.access$100(Foo foo) {  
  2. return foo.mValue;  
  3. }  
  4. /*package*/static void Foo.access%200(Foo foo,int value) {  
  5.        foo.duStuff(value);  
  6. }  

內部類會在外部類中任何需要訪問mValue字段或調用doStuff方法的地方調用這些靜態方法。這意味着這些代碼將直接存取成員變量表現爲通過存取器方法訪問。之前提到過存取器訪問如何比直接訪問慢,這例子說明,某些語言約會定導致不可見的性能問題。

如果你在高性能的Hotspot中使用這些代碼,可以通過聲明被內部類訪問的字段和成員爲包訪問權限,而非私有。但這也意味着這些字段會被其他處於同一個包中的類訪問,因此在公共API中不宜採用。

 

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

請看下面的類定義:

[java] view plaincopy
  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的私有成員是非法的。要跨越這個鴻溝,編譯器需要生成一組方法:

[java] view plaincopy
  1. /*package*/ static int Foo.access$100(Foo foo) {  
  2.     return foo.mValue;  
  3. }  
  4. /*package*/ static void Foo.access$200(Foo foo, int value) {  
  5.     foo.doStuff(value);  
  6. }  


內部類在每次訪問mValueg和gdoStuffg方法時,都會調用這些靜態方法。就是說,上面的代碼說明了一個問題,你是在通過接口方法訪問這些成員變量和函數而不是直接調用它們。在前面我們已經說過,使用接口方法(getter、setter)比直接訪問速度要慢。所以這個例子就是在特定語法下面產生的一個“隱性的”性能障礙。

通過將內部類訪問的變量和函數聲明由私有範圍改爲包範圍,我們可以避免這個問題。這樣做可以讓代碼運行更快,並且避免產生額外的靜態方法。(遺憾的是,這些域和方法可以被同一個包內的其他類直接訪問,這與經典的OO原則相違背。因此當你設計公共API的時候應該謹慎使用這條優化原則)。

 

14)     緩存

適量使用緩存,不要過量使用,因爲內存有限,能保存路徑地址的就不要存放圖片數據,不經常使用的儘量不要緩存,不用時就清空。

 

15)     關閉資源對象

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

             

2.      視圖優化

1)       View優化

                       i.    減少不必要的View以及View的嵌套層次。

比如實現一個listview中常用的layout,可以使用RelativeLayout減少嵌套,要知道每個View的對象會耗費1~2k內存,嵌套層次過多會引起頻繁的gc,造成ANR。

                     ii.    通過HierarchyViewer查看佈局結構

利用HierarchyViewer來查看View的結構:~/tools/hierarchyviewer,能很清楚地看到RelativeLayout下面的扁平結構,這樣能加快dom的渲染速度。

詳細請參考http://developer.android.com/guide/developing/tools/hierarchy-viewer.html

                   iii.    通過Layoutopt優化佈局

通過Android sdk中tools目錄下的layoutopt 命令查看你的佈局是否需要優化。詳細請參考http://apps.hi.baidu.com/share/detail/34247942

             

2)       多線程解決複雜計算

佔用CPU較多的數據操作儘可能放在一個單獨的線程中進行,通過handler等方式把執行的結果交於UI線程顯示。特別是針對的網絡訪問,數據庫查詢,和複雜的算法。目前Android提供了AsyncTask,Hanlder、Message和Thread的組合。

對於多線程的處理,如果併發的線程很多,同時有頻繁的創建和釋放,可以通過concurrent類的線程池解決線程創建的效率瓶頸。

另外值得注意的是,應用開發中自定義View的時候,交互部分,千萬不要寫成線程不斷刷新界面顯示,而是根據TouchListener事件主動觸發界面的更新。

 

3)       佈局用Java完成比XML快

一般情況下對於Android程序佈局往往使用XML文件來編寫,這樣可以提高開發效率,但是考慮到代碼的安全性以及執行效率,可以通過Java代碼執行創建,雖然Android編譯過的XML是二進制的,但是加載XML解析器的效率對於資源佔用還是比較大的,Java處理效率比XML快得多,但是對於一個複雜界面的編寫,可能需要一些套嵌考慮,如果你思維靈活的話,使用Java代碼來佈局你的Android應用程序是一個更好的方法。

 

4)       對大型圖片進行縮放

圖片讀取是OOM(Out of Memory)的常客,當在Android手機上直接讀取4M的圖片時,死神一般都會降臨,所以導致往往自己手機拍攝的照片都不能直接讀取。對大型圖片進行縮放有,處理圖片時我們經常會用到BitmapFactory類,android系統中讀取位圖Bitmap時分給虛擬機中圖片的堆棧大小隻有8M。用BitmapFactory解碼一張圖片時,有時也會遇到該錯誤。這往往是由於圖片過大造成的。這時我們需要分配更少的內存空間來存儲。BitmapFactory.Options.inSampleSize設置恰當的inSampleSize可以使BitmapFactory分配更少的空間以消除該錯誤。Android提供了一種動態計算的,如下:

 

讀取圖片之前先查看其大小:

[java] view plaincopy
  1. BitmapFactory.Options opts = new BitmapFactory.Options();  
  2. opts.inJustDecodeBounds = true;  
  3. Bitmap bitmap = BitmapFactory.decodeFile(imageFile, opts);  

使用得到的圖片原始寬高計算適合自己的smaplesize


[java] view plaincopy
  1. BitmapFactory.Options opts = new BitmapFactory.Options();  
  2.  opts.inSampleSize = 4 ;// 4就代表容量變爲以前容量的1/4  
  3.  Bitmap bitmap = BitmapFactory.decodeFile(imageFile, opts);  

                    

對於過時的Bitmap對象一定要及時recycle,並且把此對象賦值爲null。


[java] view plaincopy
  1. bitmap.recycle();  
  2. bitmap = null;  

             

5)       合理使用ViewStub

ViewStub 是一個隱藏的,不佔用內存空間的視圖對象,它可以在運行時延遲加載佈局資源文件。當ViewStub可見,或者調用 inflate()函數時,纔會加載這個佈局資源文件。 該ViewStub在加載視圖時在父容器中替換它本身。因此,ViewStub會一直存在於視圖中,直到調用setVisibility(int) 或者inflate()爲止。ViewStub的佈局參數會隨着加載的視圖數一同被添加到ViewStub父容器。同樣,你也可以通過使用inflatedId屬性來定義或重命名要加載的視圖對象的Id值。所以我們可以使用ViewStub延遲加載某些比較複雜的layout,動態加載View,採用ViewStub 避免一些不經常的視圖長期握住引用。

詳細請參考http://developer.android.com/reference/android/view/ViewStub.html

 

6)       針對ListView的性能優化

                    i.    複用convertView。


                    ii.    在getItemView中,判斷convertView是否爲空,如果不爲空,可複用。如果couvertview中的view需要添加listerner,代碼一定要在if(convertView==null){}之外。


                   iii.    異步加載圖片,item中如果包含有web image,那麼最好異步加載。


                    iv.    快速滑動時不顯示圖片

當快速滑動列表時(SCROLL_STATE_FLING),item中的圖片或獲取需要消耗資源的view,可以不顯示出來;而處於其他兩種狀態(SCROLL_STATE_IDLE 和SCROLL_STATE_TOUCH_SCROLL),則將那些view顯示出來。


                      v.    item儘可能的減少使用的控件和佈局的層次;背景色與cacheColorHint設置相同顏色;ListView中item的佈局至關重要,必須儘可能的減少使用的控件,佈局。RelativeLayout是絕對的利器,通過它可以減少佈局的層次。同時要儘可能的複用控件,這樣可以減少ListView的內存使用,減少滑動時gc次數。ListView的背景色與cacheColorHint設置相同顏色,可以提高滑動時的渲染性能。


                      vi.    getView優化

ListView中getView是性能是關鍵,這裏要儘可能的優化。getView方法中要重用view;getView方法中不能做複雜的邏輯計算,特別是數據庫和網絡訪問操作,否則會嚴重影響滑動時的性能。優化如下所示:

[java] view plaincopy
  1. @Override  
  2. public View getView(int position, View convertView, ViewGroup parent) {  
  3.        Log.d("MyAdapter""Position:" + position + "---" + String.valueOf(System.currentTimeMillis()));  
  4.        final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);  
  5.        View v = inflater.inflate(R.layout.list_item_icon_text, null);  
  6.        ((ImageView) v.findViewById(R.id.icon)).setImageResource(R.drawable.icon);  
  7.        ((TextView) v.findViewById(R.id.text)).setText(mData[position]);  
  8.        return v;  
  9. }  


建議改爲:

[java] view plaincopy
  1. @Override  
  2. public View getView(int position, View convertView, ViewGroup parent) {  
  3.        Log.d("Adapter""Position:" + position + " : " + String.valueOf(System.currentTimeMillis()));  
  4.        ViewHolder holder;  
  5.        if (convertView == null) {  
  6.               final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);  
  7.               convertView = inflater.inflate(R.layout.list_item_icon_text, null);  
  8.               holder = new ViewHolder();  
  9.              holder.icon = (ImageView) convertView.findViewById(R.id.icon);  
  10.              holder.text = (TextView) convertView.findViewById(R.id.text);  
  11.              convertView.setTag(holder);  
  12.        } else {  
  13.              holder = (ViewHolder) convertView.getTag();  
  14.        }  
  15.               holder.icon.setImageResource(R.drawable.icon);  
  16.               holder.text.setText(mData[position]);  
  17.               return convertView;  
  18.        }  
  19.    
  20.        static class ViewHolder {  
  21.                ImageView icon;  
  22.                TextView text;  
  23.        }  
  24. }  


以上是Google IO大會上給出的優化建議,經過嘗試ListView確實流暢了許多。使用1000條記錄,經測試第一種方式耗時:25211ms,第二種方式耗時:16528ms。

 

7)       其他

                   i.    分辨率適配

-ldpi,-mdpi, -hdpi配置不同精度資源,系統會根據設備自適應,包括drawable, layout,style等不同資源。


                   ii.    儘量使用dp(density independent pixel)開發,不用px(pixel)。


                   iii.    多用wrap_content, fill_parent


                   iv.    拋棄AbsoluteLayout


                   v.    使用9patch(通過~/tools/draw9patch.bat啓動應用程序),png格式


                   vi.    採用<merge> 優化佈局層數;採用<include >來共享佈局。


                   vii.    將Acitivity中的Window的背景圖設置爲空。getWindow().setBackgroundDrawable(null);android的默認背景是不是爲空。


                   viii.    View中設置緩存屬性.setDrawingCache爲true。

 

3.      網絡優化

1)       避免頻繁網絡請求

訪問server端時,建立連接本身比傳輸需要跟多的時間,如非必要,不要將一交互可以做的事情分成多次交互(這需要與Server端協調好)。有效管理Service 後臺服務就相當於一個持續運行的Acitivity,如果開發的程序後臺都會一個service不停的去服務器上更新數據,在不更新數據的時候就讓它sleep,這種方式是非常耗電的,通常情況下,可以使用AlarmManager來定時啓動服務。如下所示,第30分鐘執行一次。

[java] view plaincopy
  1. AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);  
  2. Intent intent = new Intent(context, MyService.class);  
  3. PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0);  
  4. long interval = DateUtils.MINUTE_IN_MILLIS * 30;  
  5. long firstWake = System.currentTimeMillis() + interval;  
  6. am.setRepeating(AlarmManager.RTC,firstWake,  interval,  pendingIntent);  

2)       數據壓縮

傳輸數據經過壓縮 目前大部門網站都支持GZIP壓縮,所以在進行大數據量下載時,儘量使用GZIP方式下載,可以減少網絡流量,一般是壓縮前數據大小的30%左右。


[java] view plaincopy
  1. HttpGet request = new HttpGet("http://example.com/gzipcontent");  
  2. HttpResponse resp = new DefaultHttpClient().execute(request);  
  3. HttpEntity entity = response.getEntity();  
  4. InputStream compressed = entity.getContent();  
  5. InputStream rawData = new GZIPInputStream(compressed);  

 

3)       使用線程池

線程池,分爲核心線程池和普通線程池,下載圖片等耗時任務放置在普通線程池,避免耗時任務阻塞線程池後,導致所有異步任務都必須等待。

 

4)       選擇合適的數據格式傳輸形式


   


其中Tree Parse 是DOM解析 Event/Stream是SAX方式解析。

很明顯,使用流的方式解析效率要高一些,因爲DOM解析是在對整個文檔讀取完後,再根據節點層次等再組織起來。而流的方式是邊讀取數據邊解析,數據讀取完後,解析也就完畢了。在數據格式方面,JSON和Protobuf效率明顯比XML好很多,XML和JSON大家都很熟悉。

從上面的圖中可以得出結論就是儘量使用SAX等邊讀取邊解析的方式來解析數據,針對移動設備,最好能使用JSON之類的輕量級數據格式爲佳。

 

1)       其他

設置連接超時時間和響應超時時間。Http請求按照業務需求,分爲是否可以緩存和不可緩存,那麼在無網絡的環境中,仍然通過緩存的HttpResponse瀏覽部分數據,實現離線閱讀。

 

2.      數據庫相關

1)       相對於封裝過的ContentProvider而言,使用原始SQL語句執行效率高,比如使用方法rawQuery、execSQL的執行效率比較高。

2)       對於需要一次性修改多個數據時,可以考慮使用SQLite的事務方式批量處理。

3)       批量插入多行數據使用InsertHelper或者bulkInsert方法

4)       有些能用文件操作的,儘量採用文件操作,文件操作的速度比數據庫的操作要快10倍左右。

 

3.      性能測試

對於Android平臺上軟件的性能測試可以通過以下幾種方法來分析效率瓶頸,目前Google在Android軟件開發過程中已經引入了多種測試工具包,比如Unit測試工程,調試類,還有模擬器的Dev Tools都可以直接反應執行性能。

1)       在模擬器上的Dev Tools可以激活屏幕顯示當前的FPS,CPU使用率,可以幫助我們測試一些3D圖形界面的性能。

2)       一般涉及到網絡應用的程序,在效率上和網速有很多關係,這裏需要多次的調試才能實際瞭解。

3)       對於邏輯算法的效率執行,我們使用Android上最普遍的,計算執行時間來查看:


[java] view plaincopy
  1. long start = System.currentTimeMillis();  
  2. // do something  
  3. long duration = System.currentTimeMillis() - start;  

最終duration保存着實際處理該方法需要的毫秒數。

4)       gc效率跟蹤,如果你執行的應用比較簡單,可以在DDMS中查看下Logcat的VM釋放內存情況,大概模擬下那些地方可以緩存數據或改進算法的。

5)       線程的使用和同步,Android平臺上給我們提供了豐富的多任務同步方法,但在深層上並沒有過多的比如自旋鎖等高級應用,不過對於Service和 appWidget而言,他們實際的產品中都應該以多線程的方式處理,以釋放CPU時間,對於線程和堆內存的查看這些都可以在DDMS中看到。

6)       利用traceview和monkey等工具測試應用。

7)       利用layoutopt和ninepatch等工具優化視圖。

 

4.      結束語

本文給出了一些Android 移動開發中常見的優化方法,多數情況下利用這些優化方法優化後的代碼,執行效率有明顯的提高,內存溢出情況也有所改善。在實際應用中對程序的優化一定要權衡是否是必須的,因爲優化可能會帶來諸如增加BUG,降低代碼的可讀性,降低代碼的移植性等不良效果。

希望不要盲目優化,請先確定存在問題,再進行優化。並且你知道當前系統的性能,否則無法衡量你進行嘗試所得到的性能提升。

希望本文能給大家切實帶來幫助。歡迎就上述問題進一步交流。如有發現錯誤或不足,請斧正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章