淺談Android開發中記憶體洩露與優化與框架模式之MVC與MVP

desaco發表於2016-02-24

》淺談Android開發中記憶體洩露與優化

記憶體洩露是在Android開發中尤其要重視的一個問題,對開發人員開說,這是一個很容易犯也很常見的錯誤。優化記憶體洩露的問題,主要從兩方面著手,一是開發人員避免寫出有記憶體洩露的程式碼,二是通過一些諸如MAT的記憶體分析工具來找出潛在的記憶體洩露並解決它。

其實平時遇到的最多的情況,就是對Activity或Context保持一個長生命週期的引用。下面主要來分析一下造成記憶體洩露的各種原因。

一、靜態變數導致的記憶體洩露

要不怎麼說static關鍵字要慎用呢?來看看下面這段程式碼,Context物件為靜態的,那麼Activity就無法正常銷燬,會常駐記憶體。

  1. public class MemoryActivity extends Activity{  
  2.     public static Context mContext;  
  3.     @Override  
  4.     protected void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.         setContentView(R.layout.activity_main);  
  7.         mContext = this;  
  8.     }  
  9. }  

這是一個很隱晦的記憶體洩漏的情況,在開發過程中,Context能使用ApplicationContext得儘量使用ApplicationContext,因為這個Context的生存週期和你的應用的生存週期一樣長,而不是取決於activity的生存週期。除此之外,在Activity裡面建立了靜態的View,這就意味著該View持有一個對當前這個Activity的引用,那麼Activity也是無法正常銷燬的。

二、引用沒釋放導致的記憶體洩露

1、註冊服務沒取消導致的記憶體洩露

假如我們在鎖屏介面(LockScreen)中,監聽系統中的電話服務以獲取一些資訊(如訊號強度等),則可以在LockScreen中定義一個PhoneStateListener的物件,同時將它註冊到TelephonyManager服務中。對於LockScreen物件,當需要顯示鎖屏介面的時候就會建立一個LockScreen物件,而當鎖屏介面消失的時候LockScreen物件就會被釋放掉。但是如果在釋放LockScreen物件的時候沒有取消之前註冊的PhoneStateListener物件,那麼則會導致LockScreen無法被垃圾回收。而鎖屏介面又不斷的建立和銷燬,則最終會由於大量的LockScreen物件沒有辦法被回收而引起OutOfMemory。類似的,BraodcastReceiver,ContentObserver,FileObserver在Activity onDeatory或者某類宣告週期結束之後一定要unregister掉,否則這個Activity類會被system強引用,不會被記憶體回收。

2、集合中的物件沒有及時清理導致的記憶體洩露

當該集合為靜態的時候,那麼在集合裡面物件越來越多的時候,最好要及時清理不需要用到的物件。

三、單例模式導致的記憶體洩露

單例模式的特點就是它的生命週期和Application一樣,那麼如果某個Activity例項被一個單例所持有,也就是說在單例裡面引用了它,那麼就會造成Activity物件無法正常回收釋放。

四、資源物件未關閉導致的記憶體洩露

資源性物件(如Cursor,File檔案等)往往都用了一些緩衝,我們在不使用的時候,應該及時關閉它們,以便它們的緩衝及時回收記憶體。它們的緩衝不僅存在於java虛擬機器內,還存在於java虛擬機器外。如果我們僅僅是把它的引用設定為null,而不關閉它們,往往會造成記憶體洩露。例如程式中經常會進行查詢資料庫的操作,但是經常會有使用完畢Cursor後沒有關閉的情況。如果我們的查詢結果集比較小,對記憶體的消耗不容易被發現,只有在常時間大量操作的情況下才會復現記憶體問題,這樣就會給以後的測試和問題排查帶來困難和風險。類似的,Bitmap在不需要之後,應該呼叫recycle回收,再置為null。

五、屬性動畫導致的記憶體洩露

例如下面的程式碼,由於該屬性動畫為迴圈動畫,如果在Activity銷燬時,沒有取消動畫,那麼雖然我們看不見動畫在執行,實際上動畫仍然一直播放下去,這個時候Button會被動畫所持有,而Button又持有對應的Activity物件,那麼就會造成Activity無法正常釋放。

  1. public class MemoryActivity extends Activity{  
  2.     @Override  
  3.     protected void onCreate(Bundle savedInstanceState) {  
  4.         super.onCreate(savedInstanceState);  
  5.         setContentView(R.layout.activity_main);  
  6.         Button button = (Button) findViewById(R.id.btn_end);  
  7.         ObjectAnimator animator = ObjectAnimator.ofFloat(button, "", 0,180);  
  8.         animator.setDuration(2000);  
  9.         animator.setRepeatCount(-1);  
  10.         animator.start(); // 沒有呼叫cancle()  
  11.     }  
  12. }  

六、Adapter未使用快取的convertView導致的記憶體洩露

ListView提供每一個item所需要的view物件,初始時ListView會從BaseAdapter中根據當前的螢幕佈局例項化一定數量的View物件,同時ListView會將這些view物件快取起來。當向上滾動ListView時,原先位於最上面的Item的View物件會被回收,然後被用來構造新出現的最下面的Item。這個構造過程就是由getView()方法完成的,getView()的第二個形參View convertView就是被快取起來的list item的view物件(初始化時快取中沒有view物件則convertView是null)。由此可以看出,如果我們不去使用 convertView,而是每次都在getView()中重新例項化一個View物件的話,即浪費資源也浪費時間,也會使得記憶體佔用越來越大。正確的寫法如下

  1. public View getView(int position, ViewconvertView, ViewGroup parent) {  
  2.         View view = null;  
  3.         if (convertView != null) { // 不應該直接new  
  4.                 view = convertView;   
  5.                 ...   
  6.         } else {   
  7.                 view = new Xxx(...);  
  8.                 ...   
  9.         }   
  10.         return view;   
  11. }  

七、Handler內部類記憶體洩露

當使用內部類(包括匿名類)來建立Handler的時候,Handler物件會隱式地持有一個外部類物件(通常是一個Activity)的引用,而Handler通常會伴隨著一個耗時的後臺執行緒(例如從網路拉取圖片)一起出現,這個後臺執行緒在任務執行完畢(例如圖片下載完畢)之後,通過訊息機制通知Handler,然後Handler把圖片更新到介面。然而,如果使用者在網路請求過程中關閉了Activity,正常情況下,Activity不再被使用,它就有可能在GC檢查時被回收掉,但由於這時執行緒尚未執行完,而該執行緒持有Handler的引用,這個Handler又持有Activity的引用,就導致該Activity無法被回收(即記憶體洩露),直到網路請求結束(例如圖片下載完畢)。另外,如果你執行了Handler的postDelayed()方法,該方法會將你的Handler裝入一個Message,並把這條Message推到MessageQueue中,那麼在你設定的delay到達之前,會有一條MessageQueue -> Message -> Handler -> Activity的鏈,導致你的Activity被持有引用而無法被回收。可以在Activity結束後,關閉執行緒,如果你的Handler是被delay的Message持有了引用,那麼呼叫removeCallbacks方法來移除訊息佇列。

  記憶體洩露檢測工具MAT的使用請參考:http://jingyan.baidu.com/article/ae97a646b4eea8bbfc461d5a.html

  這裡需要強調一點,有一些記憶體洩露通過Mat是查不出來的,比如native的程式碼,MAT對C/C++是無能為力的。

》框架模式之MVC與MVP 

MVC (Model-View-Controller):顧名思義,M是指邏輯模型,V是指檢視模型,C則是控制器。一個邏輯模型我們可以才用多種檢視模型,例如進度條顯示,我們可以採用圓環、直線、容器式的試圖顯示,當然,一種檢視模型也可以對於多種邏輯模型。

MVC的作用:

1、將M層和V層的實現程式碼分離,從而使同一個程式可以在同一View下,有不同的表現形式;

2、C層則是確保M層和V層的同步,一旦M層有改變,那麼C層應該控制V去同步更新;

3、MVC是一個框架模式,它強制性的使應用程式的輸入、處理和輸出分開。

4、使用MVC應用程式被分成三個核心部件:模型、檢視、控制器,它們各自處理自己的任務。

MVC的好處:

1、MVC的價值,在於各模組的鬆耦合、複用,提高開發效率,在Android中,三個模組分離想對明顯,介面框架和ContentProvider模組得到充分複用;

2、從使用者的角度出發,使用者可以根據自己的需求,選擇自己合適的瀏覽資料的方式;

3、從開發者的角度,MVC把應用程式的邏輯層與介面是完全分開的,故開發者一般不需要管UI,主要把精力放在邏輯層上。

舉個例子,在Android中: 

1、檢視層(View):一般在layout下,採用XML檔案進行介面的描述,可以通過R.layout.***引入。當然,檢視的顯示絕不僅僅這些,還可以通過HTML5+CSS3+JS,利用Android自帶的WebView來顯示,另外,諸如PhoneGap的跨平臺開發框架,也是針對不同平臺的WebView做了擴充套件和封裝,使WebView這個元件變成可訪問裝置本地API,所以本質上還是用WebView;

2、控制層(Controller):控制器起到不同層面間的組織作用,用於控制應用程式的流程,處理事件並作出響應。Android的控制層的重任通常落在了眾多的Acitvity的肩上,這句話的另一層含義則是儘量不要在Acitivity中寫一些複雜邏輯的程式碼,這部分複雜的業務邏輯,交由Model層去處理,另外,耗時的操作儘量在子執行緒中執行,使得主執行緒能快速處理UI事件和Broadcast訊息,UI需要更新通過Handler、Messenger等方式通知主執行緒更新。眾所周知,Android主執行緒阻塞時間不能超過5秒,否則將會發生ANR(Application Not Responding)異常。

3、模型層(Model):對資料庫的操作、網路請求、格式轉換、檔案下載、大資料處理等的耗時操作都在Model裡面處理。

  其實,MVC並沒有嚴格的定義,但是Android中,MVC三個層分離明顯,介面框架和ContentProvider模組得到充分複用,開發者並不需要在這方面話太多的心思去思考。

  MVP(Model-View-Presenter):和MVC類似,Presenter就是Model和View互動的中間紐帶,負責處理與使用者互動的邏輯。

android-MVP模式:

View不直接與Model互動,而是通過與Presenter互動來與Model間接互動。 
Presenter與View的互動是通過介面來進行的。 
通常View與Presenter是一對一的,但複雜的View可能繫結多個Presenter來處理邏輯。

android-MVC模式:

View可以與Model直接互動。 
Controller是基於行為的,並且可以被多個View共享,並且決定顯示哪個View。

相關文章