ANDROID記憶體優化(大彙總——下)

yangxi_001發表於2015-03-16

轉載請註明本文出自大苞米的部落格(http://blog.csdn.net/a396901990),謝謝支援!


寫在最前:

本文的思路主要借鑑了2014年AnDevCon開發者大會的一個演講PPT,加上把網上搜集的各種記憶體零散知識點進行彙總、挑選、簡化後整理而成。

所以我將本文定義為一個工具類的文章,如果你在ANDROID開發中遇到關於記憶體問題,或者馬上要參加面試,或者就是單純的學習或複習一下記憶體相關知識,都歡迎閱讀。(本文最後我會盡量列出所參考的文章)。


OOM:


記憶體洩露可以引發很多的問題:

1.程式卡頓,響應速度慢(記憶體佔用高時JVM虛擬機器會頻繁觸發GC)

2.莫名消失(當你的程式所佔記憶體越大,它在後臺的時候就越可能被幹掉。反之記憶體佔用越小,在後臺存在的時間就越長)

3.直接崩潰(OutOfMemoryError)


ANDROID記憶體面臨的問題:

1.有限的堆記憶體,原始只有16M

2.記憶體大小消耗等根據裝置,作業系統等級,螢幕尺寸的不同而不同

3.程式不能直接控制

4.支援後臺多工處理(multitasking)

5.執行在虛擬機器之上


5R:

本文主要通過如下的5R方法來對ANDROID記憶體進行優化:


1.Reckon(計算)

首先需要知道你的app所消耗記憶體的情況,知己知彼才能百戰不殆

2.Reduce(減少)

消耗更少的資源

3.Reuse(重用)

當第一次使用完以後,儘量給其他的使用

5.Recycle(回收)

回收資源

4.Review(檢查)

回顧檢查你的程式,看看設計或程式碼有什麼不合理的地方。





記憶體簡介Reckon(計算):


關於記憶體簡介,和Reckon的內容請看:ANDROID記憶體優化(大彙總——上)



Reduce(減少) ,Reuse(重用):


關於Reduce,和Reuse的內容請看:ANDROID記憶體優化(大彙總——中)



Recycle(回收):


Recycle(回收),回收可以說是在記憶體使用中最重要的部分。因為記憶體空間有限,無論你如何優化,如何節省記憶體總有用完的時候。而回收的意義就在於去清理和釋放那些已經閒置,廢棄不再使用的記憶體資源和記憶體空間。

因為在Java中有垃圾回收(GC)機制,所以我們平時都不會太關注它,下面就來簡單的介紹一下回收機制:



垃圾回收(GC):


Java垃圾回收器:

在C,C++或其他程式設計語言中,資源或記憶體都必須由程式設計師自行宣告產生和回收,否則其中的資源將消耗,造成資源的浪費甚至崩潰。但手工回收記憶體往往是一項複雜而艱鉅的工作。

於是,Java技術提供了一個系統級的執行緒,即垃圾收集器執行緒(Garbage Collection Thread),來跟蹤每一塊分配出去的記憶體空間,當Java 虛擬機器(Java Virtual Machine)處於空閒迴圈時,垃圾收集器執行緒會自動檢查每一快分配出去的記憶體空間,然後自動回收每一快可以回收的無用的記憶體塊。 


作用:

1.清除不用的物件來釋放記憶體:

採用一種動態儲存管理技術,它自動地釋放不再被程式引用的物件,按照特定的垃圾收集演算法來實現資源自動回收的功能。當一個物件不再被引用的時候,記憶體回收它佔領的空間,以便空間被後來的新物件使用。 

2.消除堆記憶體空間的碎片:

由於建立物件和垃圾收集器釋放丟棄物件所佔的記憶體空間,記憶體會出現碎片。碎片是分配給物件的記憶體塊之間的空閒記憶體洞。碎片整理將所佔用的堆記憶體移到堆的一端,JVM將整理出的記憶體分配給新的物件。 


垃圾回收器優點:

1.減輕程式設計的負擔,提高效率:

使程式設計師從手工回收記憶體空間的繁重工作中解脫了出來,因為在沒有垃圾收集機制的時候,可能要花許多時間來解決一個難懂的儲存器問題。在用Java語言程式設計的時候,靠垃圾收集機制可大大縮短時間。

2.它保護程式的完整性:

因此垃圾收集是Java語言安全性策略的一個重要部份。 


垃圾回收器缺點:

1.佔用資源時間:

Java虛擬機器必須追蹤執行程式中有用的物件, 而且最終釋放沒用的物件。這一個過程需要花費處理器的時間。

2.不可預知:

垃圾收集器執行緒雖然是作為低優先順序的執行緒執行,但在系統可用記憶體量過低的時候,它可能會突發地執行來挽救記憶體資源。當然其執行與否也是不可預知的。 

3.不確定性:

不能保證一個無用的物件一定會被垃圾收集器收集,也不能保證垃圾收集器在一段Java語言程式碼中一定會執行。

同樣也沒有辦法預知在一組均符合垃圾收集器收集標準的物件中,哪一個會被首先收集。 

4.不可操作

垃圾收集器不可以被強制執行,但程式設計師可以通過呼叫System. gc方法來建議執行垃圾收集器。

垃圾回收演算法:
1.引用計數(Reference Counting) 
比較古老的回收演算法。原理是此物件有一個引用,即增加一個計數,刪除一個引用則減少一個計數。垃圾回收時,只用收集計數為0的物件。此演算法最致命的是無法處理迴圈引用的問題。 
2.標記-清除(Mark-Sweep) 
此演算法執行分兩階段。第一階段從引用根節點開始標記所有被引用的物件,第二階段遍歷整個堆,把未標記的物件清除。此演算法需要暫停整個應用,同時,會產生記憶體碎片。
3.複製(Copying) 
此演算法把記憶體空間劃為兩個相等的區域,每次只使用其中一個區域。垃圾回收時,遍歷當前使用區域,把正在使用中的物件複製到另外一個區域中。次演算法每次只處理正在使用中的物件,因此複製成本比較小,同時複製過去以後還能進行相應的記憶體整理,不過出現“碎片”問題。當然,此演算法的缺點也是很明顯的,就是需要兩倍記憶體空間。
4.標記-整理(Mark-Compact) 
此演算法結合了 “標記-清除”和“複製”兩個演算法的優點。也是分兩階段,第一階段從根節點開始標記所有被引用物件,第二階段遍歷整個堆,把清除未標記物件並且把存活物件 “壓縮”到堆的其中一塊,按順序排放。此演算法避免了“標記-清除”的碎片問題,同時也避免了“複製”演算法的空間問題。 
5.增量收集(Incremental Collecting) 
實施垃圾回收演算法,即:在應用進行的同時進行垃圾回收。不知道什麼原因JDK5.0中的收集器沒有使用這種演算法的。 
6.分代(Generational Collecting) 
基於對物件生命週期分析後得出的垃圾回收演算法。把物件分為年青代、年老代、持久代,對不同生命週期的物件使用不同的演算法(上述方式中的一個)進行回收。現在的垃圾回收器(從J2SE1.2開始)都是使用此演算法的。 


finalize():

每一個物件都有一個finalize方法,這個方法是從Object類繼承來的。 

當垃圾回收確定不存在對該物件的更多引用時,由物件的垃圾回收器呼叫此方法


Java 技術允許使用finalize方法在垃圾收集器將物件從記憶體中清除出去之前做必要的清理工作。一旦垃圾回收器準備好釋放物件佔用的空間,將首先呼叫其finalize()方法,並且在下一次垃圾回收動作發生時,才會真正回收物件佔用的記憶體。
簡單的說finalize方法是在垃圾收集器刪除物件之前對這個物件呼叫的


System.gc():

我們可以呼叫System.gc方法,建議虛擬機器進行垃圾回收工作(注意,是建議,但虛擬機器會不會這樣幹,我們也無法預知!)


下面來看一個例子來了解finalize()System.gc()的使用:

  1. public class TestGC {  
  2.     public TestGC() {}  
  3.       
  4.     //當垃圾回收器確定不存在對該物件的更多引用時,由物件的垃圾回收器呼叫此方法。  
  5.     protected void finalize() {  
  6.         System.out.println("我已經被垃圾回收器回收了...");  
  7.     }  
  8.       
  9.     public static void main(String [] args) {  
  10.         TestGC gc = new TestGC();  
  11.         gc = null;    
  12.         // 建議虛擬機器進行垃圾回收工作  
  13.         System.gc();  
  14.     }  
  15. }  
如上面的例子所示,大家可以猜猜重寫的finalize方法會不會執行?

答案是:不一定


因為無論是設定gc的引用為null還是呼叫System.gc()方法都只是"建議"垃圾回收器進行垃圾回收,但是最終所有權還在垃圾回收器手中,它會不會進行回收我們無法預知!

垃圾回收面試題:
最後通過網上找到的3道面試題來結束垃圾回收的內容。

 

面試題一: 

  1. 1.fobj = new Object ( ) ;   
  2. 2.fobj. Method ( ) ;   
  3. 3.fobj = new Object ( ) ;   
  4. 4.fobj. Method ( ) ;   

問:這段程式碼中,第幾行的fobj 符合垃圾收集器的收集標準? 
答:第3行。因為第3行的fobj被賦了新值,產生了一個新的物件,即換了一塊新的記憶體空間,也相當於為第1行中的fobj賦了null值。這種型別的題是最簡單的。 

面試題二: 
  1. 1.Object sobj = new Object ( ) ;   
  2. 2.Object sobj = null ;   
  3. 3.Object sobj = new Object ( ) ;   
  4. 4.sobj = new Object ( ) ;   
問:這段程式碼中,第幾行的記憶體空間符合垃圾收集器的收集標準? 
答:第2行和第4行。因為第2行為sobj賦值為null,所以在此第1行的sobj符合垃圾收集器的收集標準。而第4行相當於為sobj賦值為null,所以在此第3行的sobj也符合垃圾收集器的收集標準。 

如果有一個物件的控制程式碼a,且你把a作為某個構造器的引數,即 new Constructor ( a )的時候,即使你給a賦值為null,a也不符合垃圾收集器的收集標準。直到由上面構造器構造的新物件被賦空值時,a才可以被垃圾收集器收集。 

面試題三: 
  1. 1.Object aobj = new Object ( ) ;   
  2. 2.Object bobj = new Object ( ) ;   
  3. 3.Object cobj = new Object ( ) ;   
  4. 4.aobj = bobj;   
  5. 5.aobj = cobj;   
  6. 6.cobj = null;   
  7. 7.aobj = null;   
問:這段程式碼中,第幾行的記憶體空間符合垃圾收集器的收集標準? 
答:第4,7行。注意這類題型是認證考試中可能遇到的最難題型了。 
行1-3:分別建立了Object類的三個物件:aobj,bobj,cobj
行4:此時物件aobj的控制程式碼指向bobj,原來aojb指向的物件已經沒有任何引用或變數指向,這時,就符合回收標準。
行5:此時物件aobj的控制程式碼指向cobj,所以該行的執行不能使aobj符合垃圾收集器的收集標準。 
行6:此時仍沒有任何一個物件符合垃圾收集器的收集標準。 
行7:物件cobj符合了垃圾收集器的收集標準,因為cobj的控制程式碼指向單一的地址空間。在第6行的時候,cobj已經被賦值為null,但由cobj同時還指向了aobj(第5行),所以此時cobj並不符合垃圾收集器的收集標準。而在第7行,aobj所指向的地址空間也被賦予了空值null,這就說明了,由cobj所指向的地址空間已經被完全地賦予了空值。所以此時cobj最終符合了垃圾收集器的收集標準。 但對於aobj和bobj,仍然無法判斷其是否符合收集標準。 

總之,在Java語言中,判斷一塊記憶體空間是否符合垃圾收集器收集的標準只有兩個: 
1.給物件賦予了空值null,以下再沒有呼叫過。 
2.給物件賦予了新值,既重新分配了記憶體空間。 

最後再次提醒一下,一塊記憶體空間符合了垃圾收集器的收集標準,並不意味著這塊記憶體空間就一定會被垃圾收集器收集。



資源的回收:

剛才講了一堆理論的東西,下面來點實際能用上的,資源的回收:


Thread(執行緒)回收:

執行緒中涉及的任何東西GC都不能回收(Anything reachable by a thread cannot be GC'd ),所以執行緒很容易造成記憶體洩露。

如下面程式碼所示:

  1. Thread t = new Thread() {  
  2.     public void run() {  
  3.         while (true) {  
  4.             try {  
  5.                 Thread.sleep(1000);  
  6.                 System.out.println("thread is running...");  
  7.             } catch (InterruptedException e) {  
  8.               
  9.             }  
  10.         }  
  11.     }  
  12. };  
  13. t.start();  
  14. t = null;  
  15. System.gc();  
如上線上程t中每間隔一秒輸出一段話,然後將執行緒設定為null並且呼叫System.gc方法。

最後的結果是執行緒並不會被回收,它會一直的執行下去。


因為執行中的執行緒是稱之為垃圾回收根(GC Roots)物件的一種,不會被垃圾回收。當垃圾回收器判斷一個物件是否可達,總是使用垃圾回收根物件作為參考點。


Cursor(遊標)回收:

Cursor是Android查詢資料後得到的一個管理資料集合的類,在使用結束以後。應該保證Cursor佔用的記憶體被及時的釋放掉,而不是等待GC來處理。並且Android明顯是傾向於程式設計者手動的將Cursor close掉,因為在原始碼中我們發現,如果等到垃圾回收器來回收時,會給使用者以錯誤提示。<wbr><wbr>

所以我們使用Cursor的方式一般如下:

  1. Cursor cursor = null;  
  2. try {  
  3.     cursor = mContext.getContentResolver().query(uri,nullnull,null,null);  
  4.     if(cursor != null) {  
  5.         cursor.moveToFirst();  
  6.         //do something  
  7.     }  
  8. catch (Exception e) {  
  9.     e.printStackTrace();  
  10. finally {  
  11.     if (cursor != null) {  
  12.         cursor.close();  
  13.     }  
  14. }  
有一種情況下,我們不能直接將Cursor關閉掉,這就是在CursorAdapter中應用的情況,但是注意,CursorAdapter在Acivity結束時並沒有自動的將Cursor關閉掉,因此,你需要在onDestroy函式中,手動關閉。
  1. @Override    
  2. protected void onDestroy() {          
  3.     if (mAdapter != null && mAdapter.getCurosr() != null) {    
  4.         mAdapter.getCursor().close();    
  5.     }    
  6.     super.onDestroy();     
  7. }    

Receiver(接收器)回收

呼叫registerReceiver()後未呼叫unregisterReceiver(). 
當我們Activity中使用了registerReceiver()方法註冊了BroadcastReceiver,一定要在Activity的生命週期內呼叫unregisterReceiver()方法取消註冊 
也就是說registerReceiver()和unregisterReceiver()方法一定要成對出現,通常我們可以重寫Activity的onDestory()方法: 
  1. @Override    
  2. protected void onDestroy() {    
  3.       this.unregisterReceiver(receiver);    
  4.       super.onDestroy();    
  5. }    

Stream/File(流/檔案)回收:

主要針對各種流,檔案資源等等如:

InputStream/OutputStream,SQLiteOpenHelper,SQLiteDatabase,Cursor,檔案,I/O,Bitmap圖片等操作等都應該記得顯示關閉。
和之前介紹的Cursor道理類似,就不多說了。





Review:


Review(回顧,檢查),大家都知道Code Review的重要性。而這裡我說的Review和Code Review差不多,主要目的就是檢查程式碼中存在的不合理和可以改進的地方,當然這個Review需要大家自己來做啦。



Code Review(程式碼檢查):

Code Review主要檢查程式碼中存在的一些不合理或可以改進優化的地方,大家可以參考之前寫的Reduce,Reuse和Recycle都是側重講解這方面的。




UI Review(檢視檢查):

Android對於檢視中控制元件的佈局渲染等會消耗很多的資源和記憶體,所以這部分也是我們需要注意的。


減少檢視層級:
減少檢視層級可以有效的減少記憶體消耗,因為檢視是一個樹形結構,每次重新整理和渲染都會遍歷一次。

hierarchyviewer:
想要減少檢視層級首先就需要知道檢視層級,所以下面介紹一個SDK中自帶的一個非常好用的工具hierarchyviewer。
你可以在下面的地址找到它:your sdk path\sdk\tools


如上圖大家可以看到,hierarchyviewer可以非常清楚的看到當前檢視的層級結構,並且可以檢視檢視的執行效率(檢視上的小圓點,綠色表示流暢,黃色和紅色次之),所以我們可以很方便的檢視哪些view可能會影響我們的效能從而去進一步優化它。


hierarchyviewer還提供另外一種列表式的檢視方式,可以檢視詳細的螢幕畫面,具體到畫素級別的問題都可以通過它發現。


ViewStub標籤

此標籤可以使UI在特殊情況下,直觀效果類似於設定View的不可見性,但是其更大的意義在於被這個標籤所包裹的Views在預設狀態下不會佔用任何記憶體空間。


include標籤

可以通過這個標籤直接載入外部的xml到當前結構中,是複用UI資源的常用標籤。


merge標籤

它在優化UI結構時起到很重要的作用。目的是通過刪減多餘或者額外的層級,從而優化整個Android Layout的結構。


注意:靈活運用以上3個標籤可以有效減少檢視層級,具體使用大家可以上網搜搜)


佈局用Java程式碼比寫在XML中快

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



重用系統資源:

1. 利用系統定義的id

比如我們有一個定義ListView的xml檔案,一般的,我們會寫類似下面的程式碼片段。

  1. <ListView  
  2.     android:id="@+id/mylist"  
  3.     android:layout_width="fill_parent"  
  4.     android:layout_height="fill_parent"/>  

這裡我們定義了一個ListView,定義它的id是"@+id/mylist"。實際上,如果沒有特別的需求,就可以利用系統定義的id,類似下面的樣子。

  1. <ListView  
  2.     android:id="@android:id/list"  
  3.     android:layout_width="fill_parent"  
  4.     android:layout_height="fill_parent"/>  
在xml檔案中引用系統的id,只需要加上“@android:”字首即可。如果是在Java程式碼中使用系統資源,和使用自己的資源基本上是一樣的。不同的是,需要使用android.R類來使用系統的資源,而不是使用應用程式指定的R類。這裡如果要獲取ListView可以使用android.R.id.list來獲取。

2. 利用系統的圖片資源

這樣做的好處,一個是美工不需要重複的做一份已有的圖片了,可以節約不少工時;另一個是能保證我們的應用程式的風格與系統一致。

3. 利用系統的字串資源

如果使用系統的字串,預設就已經支援多語言環境了。如上述程式碼,直接使用了@android:string/yes和@android:string/no,在簡體中文環境下會顯示“確定”和“取消”,在英文環境下會顯示“OK”和“Cancel”。

4. 利用系統的Style

 假設佈局檔案中有一個TextView,用來顯示視窗的標題,使用中等大小字型。可以使用下面的程式碼片段來定義TextView的Style。

  1. <TextView  
  2.         android:id="@+id/title"  
  3.         android:layout_width="wrap_content"  
  4.         android:layout_height="wrap_content"  
  5.         android:textAppearance="?android:attr/textAppearanceMedium" />  
其中android:textAppearance="?android:attr/textAppearanceMedium"就是使用系統的style。需要注意的是,使用系統的style,需要在想要使用的資源前面加“?android:”作為字首,而不是“@android:”。

5. 利用系統的顏色定義

除了上述的各種系統資源以外,還可以使用系統定義好的顏色。在專案中最常用的,就是透明色的使用。

  1. android:background ="@android:color/transparent"  


除了上面介紹的以外還有很多其他Android系統本身自帶的資源,它們在應用中都可以直接使用。具體的,可以進入android-sdk的相應資料夾中去檢視。例如:可以進入$android-sdk$\platforms\android-8\data\res,裡面的系統資源就一覽無餘了。

開發者需要花一些時間去熟悉這些資源,特別是圖片資源和各種Style資源,這樣在開發過程中,能重用的儘量重用,而且有時候使用系統提供的效果可能會更好。



其他小tips:

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

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

3.多用wrap_content, match_parent

4.永遠不要使用AbsoluteLayout

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

6.將Acitivity中的Window的背景圖設定為空。getWindow().setBackgroundDrawable(null);android的預設背景是不是為空。

7.View中設定快取屬性.setDrawingCache為true。



Desgin Review(設計檢查):

Desgin Review主要側重檢查一下程式的設計是否合理,包括框架的設計,介面的設計,邏輯的設計(其實這些東西開發之前就應該想好了)


框架設計:

是否定義了自己的Activity和fragment等常用控制元件的基類去避免進行重複的工作

是否有完善的異常處理機制,即使真的出現OOM也不會直接崩潰導致直接退出程式


介面設計:

1.在檢視中載入你所需要的,而不是你所擁有。因為使用者不可能同時看到所有東西。最典型的例子就是ListView中的滑動載入。

2.如果資料特別大,此時應該暗示使用者去點選載入,而不是直接載入。

3.合理運用分屏,轉屏等,它是個雙刃劍,因為它即可以使程式更加美觀功能更加完善,但也相應增加了資源開銷。


邏輯設計:

避免子類直接去控制父類中內容,可以使用監聽等方式去解決


關於這三點由於我工作經驗比較少,加上一時半會也想不出來多少,如果大家有建議希望可以留言,之後我給加進去。



寫在最後:


到此ANDROID記憶體優化上、中、下三篇全部寫完了。

記憶體簡介,Recoken(計算)請看ANDROID記憶體優化(大彙總——上)

Reduce(減少),Reuse(重用) 請看:ANDROID記憶體優化(大彙總——中)

Recycle(回收), Review(檢查) 請看:ANDROID記憶體優化(大彙總——全)


最初寫這篇文章的原因是因為我拿到一個國外大牛演講的PPT,我看過之後感覺寫的非常好,於是想按照ppt的思路將其總結一下。結果到寫的時候發現困難重重,因為記憶體本來就是很理論的東西,很多都是靠經驗的。而我的經驗幾乎可以忽略,寫的東西完全是網上各路文章的大彙總(所以大家千萬不要叫我大神,我只是大神的搬運工。。。)


雖然如此我覺得我總結和蒐集的還算比較全面的,當然也有很多遺落也可能有很多錯誤,這個就希望大家一起幫著完善一下。


最後我把這個PPT的原件附上,裡面很多高階的東西我沒看懂(比如那個5R中其實是沒有Review的,原文是Reorder,由於這部分我看不懂而且找不到很好的資料只能自己換了一個Review),各路大神有興趣可以看看,如果可以的話寫出來分享一下。


Putting Your App on a Memory Diet, Parts I and II_Murphy


最後小嘮叨一下,我最近參加了devstore網站的一個小比賽,所以blog先停更一個月,十一之後接著寫。

在這段時間裡我正好也可以休息一下想想以後寫點什麼東西。像記憶體這種偏理論的東西我還是不要碰了,以後可能會多翻譯一些國外大神的文章和自己做的一些小Demo吧。


不知不覺Blog也寫了快半年了,越來越覺得Blog這種分享精神的重要性,因為只有分享才能收穫的更多!

最後要謝謝那些關注,點贊和評論的網友們,這些真的是我能堅持下來的一個巨大動力!



參考文章:

Android記憶體優化http://blog.csdn.net/imain/article/details/8560986
Android 記憶體優化http://blog.csdn.net/awangyunke/article/details/20380719
關於android效能,記憶體優化http://www.cnblogs.com/zyw-205520/archive/2013/02/17/2914190.htm
Java垃圾回收原理http://www.360doc.com/content/11/0911/15/18042_147476260.shtml
JVM垃圾回收(GC)原理http://www.360doc.com/content/11/0911/16/18042_147492404.shtml

相關文章