原始问题是这样

然后扔到了很多Android开发交流群里。
接着产生了很多的见解,我感觉比较靠谱的有以下:

网友对我问题的回答

1、onDestroy被回调代不代表Activity被回收了?
官方是这么说的
Perform any final cleanup 【before】 an activity is destroyed.
x
 
1
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更是没有被回收】");
}
}
x
 
1
/**
2
 * 测试不取消注册广播导致内存泄漏的问题
3
 */
4
public class MemoryLeaksActivity extends Activity {
5
    MyBRReceiver myReceiver;
6
    TextView textView;
7
    
8
    @Override
9
    protected void onCreate(Bundle savedInstanceState) {
10
        super.onCreate(savedInstanceState);
11
        textView = new TextView(this);
12
        textView.setText("音量变化\n有线耳机插入或拔下\n");
13
        setContentView(textView);
14
        
15
        myReceiver = new MyBRReceiver();
16
        IntentFilter intentFilter = new IntentFilter();
17
        intentFilter.addAction("android.media.VOLUME_CHANGED_ACTION");//音量变化
18
        intentFilter.addAction(Intent.ACTION_HEADSET_PLUG);//有线耳机插入或拔下
19
        registerReceiver(myReceiver, intentFilter);
20
    }
21
    
22
    private class MyBRReceiver extends BroadcastReceiver {
23
        @Override
24
        public void onReceive(Context context, Intent intent) {
25
            Log.i("bqt", "【context】" + context.getClass().getSimpleName());//MemoryLeaksActivity
26
            String action = intent.getAction();
27
            switch (action) {
28
                case Intent.ACTION_HEADSET_PLUG:
29
                    int state = intent.getIntExtra("state", 0);
30
                    if (state == 1) textView.append("\n耳机模式");
31
                    else if (state == 0) textView.append("\n外放模式");
32
                    break;
33
                case "android.media.VOLUME_CHANGED_ACTION":
34
                    AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
35
                    int musicVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
36
                    int ringVolume = audioManager.getStreamVolume(AudioManager.STREAM_RING);
37
                    textView.append("\n当前音量:musicVolume=" + musicVolume + "  ringVolume=" + ringVolume);
38
                    break;
39
            }
40
        }
41
    }
42
    
43
    @Override
44
    protected void onDestroy() {
45
        super.onDestroy();
46
        Log.i("bqt", "【onDestroy被回调了,并不表明Activity被回收了,Receiver更是没有被回收】");
47
    }
48
}
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的内存泄露。

为什么不取消注册BroadcastReceiver会导致内存泄漏的更多相关文章

  1. objective-c strong导致内存泄漏简单案例

    例如: @interface Test:NSObject{ id __strong obj_; } -(void) setObject:(id __strong)obj; @end @implemen ...

  2. MSDN官方XmlSerializer类导致内存泄漏和性能低

    MSDN官方XmlSerializer类使用说明链接: http://msdn.microsoft.com/zh-CN/library/system.xml.serialization.xmlseri ...

  3. Android 非静态内部类导致内存泄漏原因深入剖析

    背景 上周发现蘑菇街IM-Android代码里面.一些地方代码编写不当.存在内存泄漏的问题.在和疯紫交流的过程中.发现加深了一些理解,所以决定写一下分析思路,相互学习. 内存泄漏 一个不会被使用的对象 ...

  4. python中循环引用导致内存泄漏小案例

    首先定义一个Person类和一个Dog类,然后分别实例化对象p和d,给p对象添加一个pet属性 给d对象添加一个master属性此时Person和Dog的应用计数都为2,当del p 和del d后P ...

  5. 面试官:小伙子,你给我说一下Java中什么情况会导致内存泄漏呢?

    概念 内存泄露:指程序中动态分配内存给一些临时对象,但对象不会被GC回收,它始终占用内存,被分配的对象可达但已无用.即无用对象持续占有内存或无用对象的内存得不到及时释放,从而造成的内存空间浪费. 可达 ...

  6. Android一般什么情况下会导致内存泄漏

    资料参考:https://blog.csdn.net/u011479990/article/details/78480091 内存泄漏的原因在于生命周期长的对象持有了生命周期短的对象的引用 内存泄漏形 ...

  7. 重写hashCode方法,导致内存泄漏

    package com.nchu.learn.base.reflect; import org.junit.Test; import java.util.Collection; import java ...

  8. 引用计数gc机制使用不当导致内存泄漏

    上一篇文章找同事review了一下,收到的反馈是铺垫太长了,我尽量直入正题,哈哈 最近dbd压测时发现内存泄漏,其实这个问题去年已经暴露了,参见这篇博客[压测周].当时排查不够仔细,在此检讨下.关于d ...

  9. 连续改变Chrome浏览器窗口大小,可以导致内存泄漏

    最近在做响应式布局的页面,在开发测试过程中,为了看到页面在不同尺寸的窗口中的表现,因此要不停的拖动浏览器来改变其窗口大小:开始在Chrome浏览器下查看页面,拖动了几次,感觉电脑明显的卡了下来,刚开没 ...

随机推荐

  1. sqlldr 远程数据库

    connect username/password@hostname:port/SERVICENAME select sys_context('USERENV','SERVICE_NAME') fro ...

  2. Win10 重装后,必须修改的设置

    作为一个程序猿,系统易用性是相当重要,每次重装WIN10 都会遇到一头包的问题,比如不能远程,打开文件各种提示需要管理员权限(mlgb很想骂人,我明明是管理员权限) ,然后开了管理员权限,结果又不能用 ...

  3. 【推导】Codeforces Round #478 (Div. 2) D. Ghosts

    题意:给你一条直线以及初始时刻这条直线上的一些人的坐标,以及他们的速度矢量.让你对每个人计算他在过去无限远到将来无限远的时间内会与多少人处于同一个点,然后对每个人的这个值求和. 列方程组:两个人i,j ...

  4. GITC简单感触

    GITC短暂的2天,去参加主要是想参与其中,了解其他家的技术,技术使用, 那些大牛,及大牛公司,大牛团队的一些事. 早上的主会场主要是介绍和宣传.半小时后就出去逛逛外面的分会场: 参与听了下 智能硬件 ...

  5. [BZOJ4832]抵制克苏恩

    [BZOJ4832]抵制克苏恩 思路: \(f[i][j][k][l]\)表示打了\(i\)次,血量为\(1\sim 3\)的随从有\(j,k,l\)个的期望.转移时注意避免重复. 源代码: #inc ...

  6. Codeforces Round #356 (Div. 2) A. Bear and Five Cards 水题

    A. Bear and Five Cards 题目连接: http://www.codeforces.com/contest/680/problem/A Description A little be ...

  7. 洛谷OJ U552 守墓人 线段树模板题

    题目描述 Description 在一个荒凉的墓地上 有一个令人尊敬的守墓人, 他看守的墓地从来 没有被盗过, 所以人们很放心的把自己的先人的墓 安顿在他那 守墓人能看好这片墓地是必然而不是偶然... ...

  8. 处理 CALayer 变形后的抗锯齿问题

    处理锯齿当然要用抗锯齿,iOS 可以通过修改 Plist 实现全局抗锯齿,但是这样容易出现性能问题. 所以就要使用对单个 Layer 开启抗锯齿的方法 layer.allowsEdgeAntialia ...

  9. vue父子组件、兄弟组件之间的通信和访问

    注意:1. vue组件间的通信其实有很多种方法,最常用的还是属性传值.事件传值.vuex; 其他方法参考 https://juejin.im/post/5bd18c72e51d455e3f6e4334 ...

  10. Apache参数的优化(转)

    按照前面提到的版本问题,Apache可以直接使用2.0版本产品线.针对Apache的优化主要是针对httpd.conf的优化,当然还有其他地方,如果特别留意的话,网上常有专家惊呼“居然这么多人忽略xx ...