ANDROID內存優化(大彙總——全)

轉自:http://blog.csdn.net/a396901990/article/details/38904543


寫在最前:

本文的思路主要借鑑了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掉,因爲在源代碼中我們發現,如果等到垃圾回收器來回收時,會給用戶以錯誤提示。

所以我們使用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

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