爲何不取消註冊BroadcastReceiver會致使內存泄漏

原始問題是這樣

而後扔到了不少Android開發交流羣裏。
接着產生了不少的看法,我感受比較靠譜的有如下:

網友對我問題的回答

一、onDestroy被回調代不表明Activity被回收了?
官方是這麼說的
Perform any final cleanup 【before】 an activity is destroyed.
衆多網友: 不表明!
網友1:表明【 將】被系統回收,具體何時回收看系統
網友2:app退出時,並不清理其所佔用的內存,你調gc只是建議,幹不幹還得看gc本身(意思是:onDestroy調用時和app退出時同樣)
網友3 GC統一回收,要看GC判斷你這個對象是否是不可達了
網友4 那只是個AMS流程回調

二、上述狀況Activity有沒有被回收?
網友1 :Receiver一直持有Activity的引用怎麼被回收
網友2 activity也是GC負責回收的,若是被強引用,無法回收

三、若是Activity被銷燬了,Receiver是否還有引用?
網友1:Receiver明顯不止被Activity持有,Receiver會註冊到系統管理的的ams中
網友2:若是receiver被static修飾,即便activity被銷燬,receiver也不會被回收,指向這個receiver的指針變成了野指針,無法主動銷燬,從而形成內存泄露
網友3:你在Activity中註冊了廣播,若是不取消註冊,這個廣播會一直存在在系統中,這個廣播會一直持有ACtivity的引用,確定會內存泄漏。就跟非靜態內部類同樣
網友4:activity回收後receiver還在運行

測試代碼

測試不取消註冊廣播致使內存泄漏的問題
/**
 * 測試不取消註冊廣播致使內存泄漏的問題
 */
public class MemoryLeaksActivity extends Activity {
	MyBRReceiver myReceiver;
	TextView textView;
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		textView = new TextView(this);
		textView.setText("音量變化\n有線耳機插入或拔下\n");
		setContentView(textView);
		
		myReceiver = new MyBRReceiver();
		IntentFilter intentFilter = new IntentFilter();
		intentFilter.addAction("android.media.VOLUME_CHANGED_ACTION");//音量變化
		intentFilter.addAction(Intent.ACTION_HEADSET_PLUG);//有線耳機插入或拔下
		registerReceiver(myReceiver, intentFilter);
	}
	
	private class MyBRReceiver extends BroadcastReceiver {
		@Override
		public void onReceive(Context context, Intent intent) {
            Log.i("bqt", "【context】" + context.getClass().getSimpleName());//MemoryLeaksActivity
			String action = intent.getAction();
			switch (action) {
				case Intent.ACTION_HEADSET_PLUG:
					int state = intent.getIntExtra("state", 0);
					if (state == 1) textView.append("\n耳機模式");
					else if (state == 0) textView.append("\n外放模式");
					break;
				case "android.media.VOLUME_CHANGED_ACTION":
					AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
					int musicVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
					int ringVolume = audioManager.getStreamVolume(AudioManager.STREAM_RING);
					textView.append("\n當前音量:musicVolume=" + musicVolume + "  ringVolume=" + ringVolume);
					break;
			}
		}
	}
	
	@Override
	protected void onDestroy() {
		super.onDestroy();
		Log.i("bqt", "【onDestroy被回調了,並不代表Activity被回收了,Receiver更是沒有被回收】");
	}
}
2017-8-24

我的總結

若是咱們在Activity中使用了registerReceiver()方法註冊了一個BroadcastReceiver,若是沒在Activity的生命週期內調用unregisterReceiver()方法取消註冊此 BroadcastReceiver ,因爲 BroadcastReceiver不止被 Activity引用,還可能會被AMS等系統服務、管理器等之類的引用,致使 BroadcastReceiver沒法被回收,而BroadcastReceiver中又持有着Activity的引用(即:onReceive方法中的參數Context),會致使Activity也沒法被回收(雖然Activity回調了onDestroy方法,但並不意味着Activity被回收了),從而致使嚴重的內存泄漏

一網友專門寫了一篇博客解釋這個問題


今天,在Q羣中有網友發出了網上的一個相對經典的問題,問題具體見下圖(白注:就是上面那張圖)。html


原本是無心寫此文的,但羣裏多個網友熱情很差推卻,因而,撰此文予以分析。android

從這個問題的陳述中,咱們發現,提問者明顯對Android中的幾個基本概念在理解上是存在誤區的(或直接稱之爲理解錯誤)。且這種誤區,我發現是較爲普遍的存在於很多Android開發心中的。api

理解誤區主要體如今對如下幾個概念沒有區分清:app

  • 1,Activity的onDestory回調方法;
  • 2,Activity的銷燬;
  • 3,Activity的內存回收;
  • 4,內存泄露;
  • 5,Activity中動態註冊的BroadcastReceiver與Activity的引用持有關係。


下面咱們來一個個具體分析下。eclipse


1,Activity的onDestory回調方法

onDestory做爲Activity中生命週期中的一個常見的方法,咱們先來看一下官方文檔中的描述。異步

從這個定義中,咱們得出以下幾點細節:ide

  • a,onDestory回調方法是Activity被銷燬前的最後一個Activity中回調方法;
  • b,onDestory回調方法的觸發時機是Activity被外部主動調用了finish()方法,或因系統內存空間不足而致使的臨行性的銷燬該Activity實例,以便得到內存空間。


在實際操做中,onDestory回調方法的觸發時機(或稱之爲Activity銷燬的觸發時機)主要表如今以下四種狀況:測試

  • a,人爲的主動的調用了finish()方法,以指望去銷燬當前的Activity;
  • b,人爲的主動操做致使的系統去銷燬當前Activity,如常見的按下手機上的返回鍵;
  • c,系統因內存不足致使的臨行性的銷燬該Activity實例,如從A Activity跳轉到B Activity後,系統內存不足的狀況下可能會銷燬掉A Activity;
  • d,打開手機上的「開發者選項」中的「不保留活動」選項,其中,這個更多的是爲了模擬出C場景,效果同C。

另外,上述的b中的按下手機上的返回鍵,系統源碼中也是調用了finish()方法。優化

區分上述的ab與cd兩種方式能夠經過isFinishing()方法的返回值來判斷。this

爲了行文方便,且從ab與cd的人爲主觀性角度出發,本文將ab情形稱之爲「主動銷燬」,cd情形稱之爲「被動銷燬」。

 

2,Activity的銷燬

相較於onDestory做爲的Activity生命週期中的回調方法,「銷燬」一詞在Activity中更多的表示的是Activity所處聲明週期中的一種「狀態」。

處於此種狀態的Activity實例,對於User Interface層來講是再也不可見的(不管是當前界面仍是按返回鍵等各類狀況)。

實踐中,處於「銷燬狀態」的Activity與上述的Activity銷燬的觸發時機具備一致的邏輯關係,這種邏輯關係具體體現爲:

  • a,對於主動銷燬,除卻Activity實例再也不可見外,當前Activity實例也直接被Activity棧中移除,直接表位爲對用戶操做導航路徑的影響;
  • b,對於被動銷燬,當前Activity實例依然再也不可見,但與主動銷燬不一樣的是,Activity實例的對應關係在Activity棧中依然存在,此時,對用戶操做導航路徑並沒有影響。B Activity中,A Activity雖然被動銷燬,但未改變棧結構,按下返回鍵依然看到A,不過此時的A與以前的A並不是一個Activity實例。


須要注意的是,處於「銷燬狀態」的Activity,嚴格意義上與當前Activity的真實內存佔用是否釋放沒有直接的對應關係。也就是說,Activity的銷燬,並不意味着Activity的內存就已經被回收

 

3,Activity的內存回收

Android是基於Java基礎之上,雖然在內存回收機制等方面作了必定的處理與優化(主要是基於Dalvik/ART),可是基本的GC原理上並沒有差異。主要表如今:

  • a,對於一個堆內存中的對象空間,一旦還有其餘的強引用可達,該內存空間就處於不可回收狀態
  • b,GC的觸發時機依然具備不可肯定性,取決於系統依據當前的運行狀態與其系統自己的GC機制斷定進行。

也就是說,即便堆內存中對象已經處於可回收狀態,但只要GC未被觸發,內存依然被佔用。

在此,須要區分下GC的不可回收狀態與可回收狀態的區別,嚴格意義上來講,其並不是對立面,由於針對可回收狀態,還有可能對應的軟引用與弱引用須要加以考慮。

 

4,內存泄露

Android中,內存泄露做爲一個基本的概念,經常被說起且實踐中也需儘可能掌握。網上關於內存泄露的文章林林總總。

終究內存泄露的本質,是指當前對象在實際運行中超出了其自己意義上生命週期範圍的,從而致使本該處於內存可回收狀態的但實際上卻一直處於不可回收狀態的內存佔用非正常現象。

內存泄露在出現,經常見於以下兩種狀況(爲行文方便,下述將發生了內存泄露的對象稱之爲M):

  • a,因異步回調中持有M,異步回調自己的生命時長長於M自己而致使的M發生內存泄露(如最多見的是Activity中的Handler以及異步線程致使Activity自己發生內存泄露);
  • b,因靜態屬性所指向的對象中持有了M而致使的M一直被強引用可達,使得M發生內存泄露(如最多見的單例對象中強引用了外部的非靜態對象)。

內存泄露過多會致使應用內存的不斷上升,達到必定程度會直接致使內存溢出(OOM)。具體解決內存泄露時,主要都是針對上述AB兩種狀況分析排查便可。

 

5,Activity中動態註冊的BroadcastReceiver與Activity的引用持有關係

對於Android中的廣播機制,能夠先參考文章:Android總結篇系列:Android廣播機制》

Activity中動態註冊的廣播接收器,通常性寫法都是Activity中持有建立的廣播接收器的對象引用,並指明廣播接收器對應的接收廣播類型(IntentFilter)。

Activity中調用registerReceiver(mBroadcastReceiver, intentFilter)方法進行廣播接收器的註冊。此時,經過Binder機制向AMS(Activity Manager Service)進行註冊

AMS會對應的記錄Activity上下文、廣播接收器以及對應的IntentFilter等內容,並造成相似於消息的發佈-訂閱存儲模式與結構。

當對應的廣播發出時,在定義的廣播接收器的onReceive(context, intent)方法回調中,對於Activity中動態註冊的廣播接收器,onReceive方法回調中的context指的是Activity Context

也就是說,Activity與mBroadcastReceiver此時其實是經過AMS相互持有強引用的。所以,對於Activity中動態註冊的廣播接收器,必定要在對應的聲明週期回調方法中去unregisterReceiver,以斬斷此關聯。

不然,就會出現當前Activity的內存泄露。



相關文章
相關標籤/搜索