為什么不取消注冊BroadcastReceiver會導致內存泄漏


原始問題是這樣

然后扔到了很多Android開發交流群里。
接着產生了很多的見解,我感覺比較靠譜的有以下:

網友對我問題的回答

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

2、上述情況Activity有沒有被回收?
網友1 :Receiver一直持有Activity的引用怎么被回收
網友2 activity也是GC負責回收的,如果被強引用,沒法回收

3、如果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群中有網友發出了網上的一個相對經典的問題,問題具體見下圖(白注:就是上面那張圖)。


本來是無意寫此文的,但群里多個網友熱情不好推卻,於是,撰此文予以分析。

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

理解誤區主要體現在對以下幾個概念沒有區分清:

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


下面我們來一個個具體分析下。


1,Activity的onDestory回調方法

onDestory作為Activity中生命周期中的一個常見的方法,我們先來看一下官方文檔中的描述。

從這個定義中,我們得出如下幾點細節:

  • 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()方法的返回值來判斷。

為了行文方便,且從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的內存泄露。






免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM