Android内存泄漏4讲

Android内存泄漏案例4讲#

概述#

主要讲解内存泄漏的4种情况:Activity、fragment、view、异步任务

内存问题概述#

内存泄漏的关注点——对象占据的内存

  • 本该回收的对象,无法被垃圾处理器回收;
  • 程序不再使用的对象,无法被垃圾处理器回收;
  • 在页面生命周期销毁后,页面持有、创建过的对象无法被垃圾处理器回收;
  • 在任务(线程、Service、广播)结束后,任务持有、创建过的对象无法被垃圾处理器回收

Android内存问题l类型概述

内存抖动的表现形式:

  1. 忽高忽低,锯齿状

内存泄漏的表现形式:

  1. 页面或任务已经结束,相关的对象仍然被GC root索引,导致垃圾回收器无法回收,内存片段仍然存在

内存溢出的表现形式:

  1. App可用内存已到达系统规定的上限,如某型号手机规定单个App最大可分配内存为196mb

  2. 系统能为App分配的内存不足,系统可分配内存已经到达上限,如系统可用只有1mb,app需要20mb

本文要讲的是内存泄漏的4种类型。

内存泄漏的危害#

典型的例子:创建8000个实例就会导致内存溢出了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package org.example.a;

import java.util.ArrayList;
import java.util.List;

class Outer{
private int[] data;

public Outer(int size) {
this.data = new int[size];
}

class Innner{

}

Innner createInner() {
return new Innner();
}
}

public class Demo {
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
int counter = 0;
while (true) {
list.add(new Outer(100000).createInner());
System.out.println(counter++);
}
}
}

Android中内存泄漏的判断方式#

判断一个对象是否无法被回收,最佳的判断方式是定义清楚这个对象预期的生命周期,通常对象是随着页面、任务的生命周期产生和消亡的,Android中可以借此机制判断是否存在泄漏现象:

  1. Activity:通过查看Activity#mDestroyed属性来判断Activity是否已经销毁,如果为true,表明该Activity已经被标记为销毁状态,此时hprof文件中若仍然存在此Activity,则表明这个Activity占据的内存处于泄漏状态;
  2. Fragment:通过Fragment#mFragmentManager属性来判断该Fragment是否处于无用状态,如果mFragmentManager为空,且hprof文件中若仍然存在Fragment,则表明Fragment占据的内存处于泄漏状态;
  3. View:通过un wrapper#mContext获得Activity,如果Activity不为空,则按照判断Activity的方式判断Activity是否泄漏
  4. ContextWrapper:通过unwrapper ContextWrapper获Activity,如果Activity不为空,则按照判断Activity的方式判断Activity是否泄漏
  5. Dialog,通过判断mDecor是否为空。mDecor为空,表明Dialog处于不被引用的状态,mDecor仍然存在在hporf里且为空,则表明Dialog泄漏了
  6. MessageQueue:通过判断MessageQueue#mQuittingmQuiting是否退出,如果该值是true且未被回收,则认为是泄漏。
  7. ViewRootImpl:通过ViewRootImpl是否为空来判断,为空表明处于无用状态,为空且未被回收则被认为是泄漏
  8. Window:通过Window#mDestroyed来判断window是否处于无用状态,mDestroyedtrue且未被回收,则认为是泄漏
  9. Toast:拿到mTN,通过mTN#mView是否为空来判断当前Toast是否已经hide,如果mView为空,表明Toast已经hide,此时Toast未被回收则认为是泄漏
  10. EditorEditor是用于TextView处理editable text的辅助类,通过ditor#mTextView为空来判断Ediator是否处于无用状态;如果mTextView为空且未被回收则认为Editor泄漏了

内存泄漏的典型 场景

  1. 非静态内部类、非静态匿名内部类持有外部类对象的引用:常见的如ListenerCallBackHandlerDialog
  2. 非静态的Handler,持有ActivityMessage持有HandlerMessageMessageQueue持有,主线程MessageQueue持久存在,导致Activity不会被释放
  3. 资源对象未关闭:数据库连接、CursorIO流使用完后未close
  4. 属性动画:未及时使用cancel关闭;Animator持续存在,导致Animator持有的ActivityFragmentView泄漏(Animator#updateListener一般都是匿名内部类,匿名内部类的问题参考场景1)
  5. 逻辑问题:广播监听后未及时解注册;

认识Hprof文件#

Hprof文件导出

  • 通过调用Debug.dumpHprofData(String filePath)方法来生成hprof文件
  • 通过执行shell命令adb shell am dumpheap pid /data/local/tmp/x.hprof来生成指定进程的hprof文件到目标目录

Heap分区:

  1. app heap:当前App在堆中占据的内存
  2. image heap:系统启动镜像,包括启动期间预加载的类
  3. zygote heap:所有App的父进程,所有App共享zygote的内存空间,zygote预加载了许多资源和代码,供所有App读取

Instance View:

  1. depth:从gc root到当前对象的引用链最短深度。被gc root引用到的对象不会被回收。

    个人收获:depath = 0,表示不被gc root引用,即会被垃圾回收器回收。

    个人收获:常见gc root有5种——局部变量、Activity threads、静态变量、JNI引用、类加载器

  2. native sizenative对象占据的内存大小

  3. shallow size:对象本身占用的内存,何为对象本身?不包括它引用的其他实例

    个人收获:shallow size = 类定义+ 父类变量所占空间大小 + 类成员变量所占空间 + [ alignment]

    类定义:固定为8Byte

    父类变量所占空间大小:当前类继承了其他类的成员变量,显然这些变量是占用空间的

    自身变量:当前类的成员变量,如果是基本数据类型,则按基本类型计算;如果是引用数据类型,则固定为4byte,当前类仅持有变量名,

    alignment:指位数对齐,目的是让shallow zie的值为8的倍数,如某个类A,前三项计算结果未15byte,则shallo size为了达到8的倍数,会设置一个alignment值,凑够16byte

  4. retained size:Retained Size是指, 当实例A被回收时, 可以同时被回收的实例的Shallow Size之和

Instance View易混淆概念:

shallow size:

Shallow Size是指实例自身占用的内存, 可以理解为保存该’数据结构’需要多少内存, 注意不包括它引用的其他实例

retained size:

实例A的Retained Size是指, 当实例A被回收时, 可以同时被回收的实例的Shallow Size之和

img

图中A, B, C, D四个实例, 为了方便计算, 我们假设所有实例的Shallow Size都是1kb

删除D实例:垃圾回收期会移除D实例;D实例的Retained Size=Shallow Size=1kb

删除C实例:垃圾回收器会移除C和D实例;C实例的Retained Size = C实例的Shallow Size + D实例的Shallow Size = 2kb

删除B实例:垃圾回收器会移除B实例,因为C仍然被A引用,所以C不会被移除,同理D也不会被移除;B实例的Retained Size=Shallow Size=1kb

删除A实例:垃圾回收器会移除B、C、D实例;A实例的Retained Size=4kb

怎么从prof文件内分析内存泄漏?

熟能生巧——memory profiler

实时预览功能区:

hprof分析-profile-memory实时预览功能

Hprof文件预览区:

hprof分析-hprof文件预览功能

案例1——hprof文件显示出了内存泄漏#

本案例如图所示,Profiler将内存泄漏的分析结果直接显示到了工具栏上,我们只需要按步骤展开方法调用链即可找到泄漏点

hprof文件分析内存泄漏——有leaks的

①点+导入hprof文件

②选择app 堆存储

③根据类名排序

④仅展示ActivityFragment内存泄漏

⑤过滤关键字,如app进程的包名

⑥点击Leaks,下方Class Name区域会展示出内存泄漏的类名

⑦在Class Name区域点击选择要观察的类名

⑧在Instance List区域点击选择该类的实例

⑨在Instace Details区域点击References 选项卡

⑩选中 GC root only,一层一层展开引用链

也可以在第9步时候观察Activity的生命周期来判断内存泄漏

可以看到一个页面存在三个实例,这三个实例都因为某些原因无法被垃圾回收器回收。我们逐一分析下。

第三个Activity实例:

hprof-观察Activity生命周期判断是否泄漏

首先点击Fields,查看Activity的生命周期,可以看到Instacen Details - Fields -Instance视图Activity#mDestroyed = true ,表明此页面已经销毁了

Android中内存泄漏的判断方式#Activity内存泄漏判断方式一节也提到观察mDestroyed字段来判断Activity是否出现了内存泄漏

image-20221114174703825

其次点击References,点一层层展开引用链,可以看到Activity使用了PlayUtilPlayUtil初始化的时候传入了此Activity,而PlayUtil是一个单例类,该单例类全局持有了此Activity的实例,导致Activity一直被PlayUtil持有着,在Activity生命周期结束后,Activity占据的堆内存,无法被垃圾回收器回收,出现了泄漏的情况。

单例类持有Activity,导致Activity占据的内存无法被释放,遇到这种问题,解决起来也很容易:

  1. 替换context,使用ApplicationContext
  2. 使用一个更轻量无任何业务的Activity来初始化PlayUtil
  3. 如无必要不要使用context

PlayUtil问题,笔者采用了方法3:

修改前

1
2
3
4
private PlayUtil(Context context) {
m_MediaPlayerIml = MediaPlayerIml.getInstance();
this.m_context = context;
}

修改后

1
2
3
4
5
  private PlayUtil(Context context) {
m_MediaPlayerIml = MediaPlayerIml.getInstance();
// 兼容老代码,保留入参context,但不使用,避免内存泄漏
// this.m_context = context;
}

这样可以达到PlayUtil不持有Activity的目的,在Activity#onDestroyed时候,Activity占据的内存将被垃圾回收器回收。

接着我们继续看该Activity的第二个实例,查看该实例的生命周期可知该Activity已经处于Destroyed状态,但内存未被回收,这是为什么呢?

hprof分析-dialog持有Activity导致泄漏

我们继续查看References,可以看到Activity被LoadProgress持有了,且无法被释放。

笔者分析可能是Activity#showLoading期间,Activity转入后台或其他因素,并未来得及调用dissLoading导致LoadProgress工具类持续持有该Activity的引用,产生了内存泄漏。我们来看看问题代码

问题代码

Activity#initLoading,传入了当前Activity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void initLoading() {
if (this.viewModel != null) {
this.viewModel.showLoading.observe(this, new Observer<Boolean>() {
public void onChanged(Boolean aBoolean) {
if (aBoolean) {
if (!LoadProgress.get().getDialog(BaseActivity.this).isShowing()) {
LoadProgress.get().getDialog(BaseActivity.this).show();
}
} else {
LoadProgress.get().dismissDialog(BaseActivity.this);
}

}
});
}
}

LoadProgress内使用HashMap缓存了传入的Activity

1
this.dialogs.put(context, lp);

解决方法

遇到这种问题也很好处理——在适当的时机清除HashMap缓存的Activity引用。

按照这种思路,我们看到:LoadProgress工具类里提供了HashMap的清空方法LoadProgress#cleanUpTrash,那么我们在合适的地方,清空`Activity缓存即可

Activity#onDestroyed调用

1
2
3
4
5
6
@Override
protected void onDestroy() {
super.onDestroy();
// 清空缓存
LoadProgress.get().cleanUpTrash();
}

这样,当Activity生命周期处于onDestroyed的时候,HashMap会清空持有的Activity,避免Activity内存泄漏

最后我们来看Activity的第一个实例

有前两个泄漏点的分析经验,可得知Activity此时已经处于onDestroyed状态,被某个类引用了,导致Activity的内存无法回收.

hprof分析-MediaplayerListener未解绑

熟能生巧,我们按图索骥可看到是MediaplayerIml通过mMediaPlayListenerCacheList持有了该Activity,该类是用于存储播放回调的,用于AIDL服务端通知Client的回调,更新播放界面。

问题代码

Activity#onCreate时候调用了注册回调

1
mediaPlayerIml.registerListener(playListener);

MediaPlayerIml#registerListener内部通过list缓存了Activity

1
2
3
4
5
6
7
   private List<MediaPlayListener> mMediaPlayListenerCacheList = new ArrayList<>();

public synchronized void registerListener(MediaPlayListener listener) {
if (!mMediaPlayListenerCacheList.contains(listener)) {
mMediaPlayListenerCacheList.add(listener);
}
}

解决方法

在适当的时机清除MediaPlayerIml持有的Activity:

Activity#onDestroy是个很不错的选择

1
2
3
4
5
6
7
8
9
10
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if(mediaPlayerIml!=null){
            mediaPlayerIml.unregisterListener(playListener);
        }
        // 清空缓存
        LoadProgress.get().cleanUpTrash();
    }

总结3个内存泄漏点

  1. PlayUtil构造函数持有Activity未释放
  2. LoadProgress成员变量HashMap持有Activity未释放
  3. AIDL远程服务端持有ListenerListener持有Activity未释放

优化效果

优化前Activity进入3次创建了3份实例:

优化前,多次进入该页面,产生4个实例,有3个实例无法被垃圾回收,出现了内存泄漏的情况

hprof分析-专辑列表页优化前

优化后,多次进入该页面,只产生一个实例

hprof分析-专辑列表页优化后

根据Retained Szie来看,优化前后节省内存,效果减少至原先的 (49937/332 = )$1/150$ 。由此可见Activity及时回收,极大的节省了内存占用。

案例2——hprof文件显示出Fragment内存泄漏#

接下来我们来看fragment内存泄漏,老规矩查看fields和references,确保它符合内存泄漏的情形;我们点击jump to source查看泄漏的位置

hprof分析-fragment内存泄漏

问题代码:Fragment#MZBannerView#内部类Runnbale

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Banner 切换时间间隔
*/
private int mDelayedTime = 5000;
private final Runnable mLoopRunnable = new Runnable() {
@Override
public void run() {
if (mIsAutoPlay) {
mCurrentItem = mViewPager.getCurrentItem();
mCurrentItem++;
if (mCurrentItem > mAdapter.getCount() - 1) {
mCurrentItem = 0;
mViewPager.setCurrentItem(mCurrentItem, false);
mHandler.postDelayed(this, mDelayedTime);
} else {
mViewPager.setCurrentItem(mCurrentItem);
mHandler.postDelayed(this, mDelayedTime);
}
} else {
mHandler.postDelayed(this, mDelayedTime);
}
}
};

可以看到Fragment内存泄漏的第一个原因,内部类runnable持有了view的实例,每个Runnbale会发送一个延时5秒的消息,消息发送期间,有可能viewfragment已经结束了生命周期,此时产生了内存泄漏。

解决办法也很简单,view离开窗口的时候,释放Handler中消息,释放Runnbaleview 的引用。

  1. 静态Runnbale内部类+对view的弱引用(此部分代码与前面的示例很相似,不重复贴代码了)
  2. 离开窗口remove#handler消息

viewActivity暴露了pause方法,在Activity销毁时,强制清空handler的任务;

1
2
3
4
5
6
7
8
9
10
/**
* 停止轮播
*/
public void pause() {
mIsAutoPlay = false;
mHandler.removeCallbacks(mLoopRunnable);
mBannerPageClickListener = null;
}


Activity代码:

1
2
3
4
5
6
7
@Override
public void onDestroy() {
super.onDestroy();
dataBing.homeBanner.pause();
}


案例3——view内存泄漏#

前文提到,profile#Leaks视图无法展示非Activity、非Fragment的内存泄漏,换言之,除了ActivityFragment的内存泄漏外,其他类的内存问题我们只能自己检索hprof文件查询了。

下面有一个极佳的view内存泄漏例子,它的操作步骤为:

  1. 播放音乐,唤醒音乐悬浮窗
  2. 播放一段时间后,关闭音乐悬浮窗
  3. 重复步骤1和2

悬浮窗长这个样

右侧悬浮窗

我们重复三次之后,得到一份hprof文件,下面我们来分析一下内存泄漏问题

hprof分析-view内存泄漏-优化前

①输入view的名称

②选择view

③可以看到分配了3个实例对象

Instance List 视图显示,view有3个实例对象及其引用

我们从上至下依次看3分实例的调用链

view的第一个实例

先查看Fields区域,观察mLayoutmode值,判断view是否离开了窗口,如果已经离开了窗口,表明view未被回收,存在内存泄漏

image-20221116103835001

可以看到mLayoutMode = -1 ,表明布局已经离开屏幕了,此实例存在内存泄漏的情况

hprof分析-message持有view

接着我们查看References区域,逐级点开我们发现Handler发送的Message持有了当前view,导致view在离开窗口的时候,无法被垃圾回收器回收。

右键点击查看问题代码

image-20221115164344291

问题代码:

1
2
3
4
5
6
7
8
9
playHandler.post(new Runnable() {
@Override
public void run() {
tv_play.setText(playItem.getProgramTitle());
tb_play.setSelected(true);
initView();
}
});

看到new Runnbale,这是是匿名内部类,匿名内部类持有当前类的引用,匿名Runnbale未执行完毕,Runnbale内存未释放的时候,view就无法被释放,而匿名Runnbale的释放时机不可控,由HandlerLooperRunnbale执行情况影响。

那么我们该怎么优化呢?

  1. 使用非匿名或静态的Handler+弱引用,处理此任务
  2. 在主线程处理此任务
  3. view退出的时候释放Messageview的引用

笔者采用了方案3:

1
2
3
4
tv_play.setText(playItem.getProgramTitle());
tb_play.setSelected(true);
initView();

遇到了异常提示非UI线程更新UI,可知该代码块默认在子线程执行

1
2
3
ViewRootImpl: Accessibility content change on non-UI thread. Future Android versions will throw an exception.
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

遂作罢放弃方案2,方案1代码与下面view的第三个实例写法一致,不重复写了;我们解释一下方案3:

view退出的时候释放Message对view的引用

根据上图所示,我们看到Message-Runnbale-View的引用关系可知,Looper中的Message持续的引用view,我们最高效释放内存的做法是view离开窗口的时候,斩断Messageview的引用关系,那么我们该怎么做呢?答案是:

  1. 结束子线程任务
  2. 清空Looper缓存的Message
  3. 释放Handler

第一步:结束子线程任务很简单

1
2
thread.interrupt()

本案例给Handler传入的是RunnbaleHandler未提供结束Runnbale的接口,此项优化搁置

第二步:清空Message

已知Looper提供了清空Message的接口

  1. Looper#quit
  2. Looper#quitSafely
  3. 主线程的Looper无法退出

已知Handler提供了释放Message的接口

  1. Handler#removeCallbacksAndMessages

那我们优化起来就很简单了,清空Handler持有的Message

1
2
3
4
5
6
7
8
9
10
11
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
...
// 释放message,断开message-Runnbale-view的引用链
if (playHandler != null) {
playHandler.removeCallbacksAndMessages(null);
playHandler = null;
}
}

主线程调quitquitSafely会抛异常,所以Looper相关API无法在主线程调用,不适合当前的例子,遂作罢:

1
2
3
4
5
java.lang.IllegalStateException: Main thread not allowed to quit.
at android.os.MessageQueue.quit(MessageQueue.java:417)
at android.os.Looper.quitSafely(Looper.java:284)
at com.unilife.common.floatwindow.view.MediaPlayerRight.onDetachedFromWindow(MediaPlayerRight.java:256)

我们继续看view的第二个实例

先查看Fields区域,观察mLayoutmode值,判断view是否离开了窗口,如果已经离开了窗口,表明view未被回收,存在内存泄漏

hprof分析-view未回收

可以看到mLayoutMode = -1 ,表明布局已经离开屏幕了,此实例存在内存泄漏的情况

接着我们看References区域,观察调用链

image-20221115180242196

可以看到MediaPlayerIml有一个成员变量mMediaPlayListenerCacheList,缓存了MediaPlayListenerMediaPlayListener又是在view实例里面创建的,并且作为内部类,它持有view的实例。现在我们得到了清晰的调用链,MediaPlayerIml->mMediaPlayListenerCacheList->MediaPlayListener->view,MediaPlayerIml引用view导致view实例无法被释放

查看问题代码:

笔者发现view#onDetachedFromWindow已经触发了移除list#listener操作

1
2
3
4
5
6
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mediaPlayerIml.unregisterListener(playListener);
}

可以看到内部实现是remove调引用的

1
2
3
4
5
6
7
8
9
10
/**
* 取消注册listener
*
* @param listener
*/
public synchronized void unregisterListener(MediaPlayListener listener) {
mMediaPlayListenerCacheList.remove(listener);
}


那为什么会未回收持续占用内存呢?

  1. 抓拍hprof文件期间,代码未执行到unregisterListener,导致view内存未得到释放
  2. mMediaPlayListenerCacheList添加的listenerremovelistener不是同一个
  3. 此处没有产生内存泄漏,判断view是否应该被回收的依据有问题

搁置疑问,接着我们来看view的第三个实例,节省时间,笔者直接调到代码索引出,展示问题代码:

view#非静态内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 播放进度条刷新控
*/
private Handler m_handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case MSG_FLUSH_SEEKBAR:
boolean isPlaying = mediaPlayerIml != null && mediaPlayerIml.getPlayStatus() == QingtingConfig.PLAY;
if (isPlaying) {
int currentTime = mediaPlayerIml.getCurrentTime();
int totalTime = mediaPlayerIml.getTotalTime();
mSeekBar.setMax(totalTime);
mSeekBar.setProgress(currentTime);
mPrograssBar.setMaxProgress(totalTime);
mPrograssBar.setCurrentProgress(currentTime);
LoggerUtils.instance().logE("mediaPlayJindu", "mediaPlayJindu" + totalTime + "/" + currentTime);
}
m_handler.sendEmptyMessageDelayed(MSG_FLUSH_SEEKBAR, MSG_FLUSH_TIME);
break;
}
}
};

可以看到此处还是使用了非静态内部类m_handlerm_handler持有当前view 的引用,m_handler如果长期存在,那么view的内存也不会被释放

解决方法如下:

  1. 定义外部类Handler
  2. 定义静态内部类
  3. 定义静态内部类+弱引用

笔者采用了方案3:

定义静态内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private  static  class UpdateHandler extends Handler {
private final WeakReference<MediaPlayerIml> mediaPlayerImlWeakReference;
private final WeakReference<SeekBar> seekBarWeakReference;
private final WeakReference<QQCircleProgressBar> progressBarWeakReference;

public UpdateHandler(MediaPlayerIml mediaPlayerIml, SeekBar seekBar, QQCircleProgressBar progressBar) {
mediaPlayerImlWeakReference = new WeakReference<MediaPlayerIml>(mediaPlayerIml);
seekBarWeakReference = new WeakReference<SeekBar>(seekBar);
progressBarWeakReference = new WeakReference<QQCircleProgressBar>(progressBar);
}

@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == MSG_FLUSH_SEEKBAR) {
MediaPlayerIml mediaPlayerIml = mediaPlayerImlWeakReference.get();
SeekBar seekBar = seekBarWeakReference.get();
QQCircleProgressBar qqCircleProgressBar =progressBarWeakReference.get();
boolean isPlaying = mediaPlayerIml != null && mediaPlayerIml.getPlayStatus() == QingtingConfig.PLAY;
if (isPlaying && seekBar!=null && qqCircleProgressBar != null) {
int currentTime = mediaPlayerIml.getCurrentTime();
int totalTime = mediaPlayerIml.getTotalTime();
seekBar.setMax(totalTime);
seekBar.setProgress(currentTime);
qqCircleProgressBar.setMaxProgress(totalTime);
qqCircleProgressBar.setCurrentProgress(currentTime);
LoggerUtils.instance().logE("mediaPlayJindu", "mediaPlayJindu" + totalTime + "/" + currentTime);
}
sendEmptyMessageDelayed(MSG_FLUSH_SEEKBAR, MSG_FLUSH_TIME);
}
}
}

view使用时,初始化handler,构造参数传入组件id;

1
2
3
m_handler = new UpdateHandler(MediaPlayerIml.getInstance(),mSeekBar,mPrograssBar);
m_handler.sendEmptyMessage(MSG_FLUSH_SEEKBAR);

在view离开窗口时候,销毁handler数据;

1
2
3
4
5
6
7
8
9
10
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
...
if(m_handler!=null){
m_handler.removeCallbacksAndMessages(null);
m_handler = null;
}
}

总结我们针对此按理做的优化

  1. 静态Handler+弱引用,释放了对handlerview的引用,让view及时销毁,view占据的内存及时被垃圾回收器释放
  2. 释放了Messageview的引用,在view及时退出界面的时候,立即斩断messageview

回顾一下优化前的实例数量,多次操作,隐藏展示悬浮窗之后,内存中存在多份悬浮窗实例,之前创建过的悬浮窗内存一直无法被回收:

hprof分析-view内存泄漏-优化前

优化后效果,多次操作,当屏幕上存在一个view时,只存在一份view实例:

image-20221115152920373

InstanceList里可以看到Instance数量为1,同样的操作次数下,优化前占用内存280824byte;优化后750byte,内存减少至原先的$1/374$;由此可见,view的就及时回收,极大的节省了内存占用。

案例4——异步任务内存泄漏#

异步任务,代指起子线程异步完成一些数据操作、网络接口请求等,通常会使用以下API:

  1. RunnbaleThread,线程池
  2. RxJava
  3. HandlerThread
  4. Timer定时器

而这些异步任务很有可能操作内存泄漏,下面我们以Rxjava为例,演示此问题,线程、线程池的问题也类似,就不再一一演示了。

大多数项目的网络基础库,传入Rxjava的是匿名Observer,任务过多时,未执行的任务的Observer会持有当前页面的引用,造成内存泄漏,演示出这个场景

那么问题来了

  1. rxjava就会存在内存泄漏吗?
  2. subscribe传入的匿名内部类Consumer实例不会造成内存泄漏吗?
  3. 异步任务返回时,Activity已经处于onDestroyed状态,Observer持有``Activity引用,Activity`内存还能被回收吗?

我们来验证一下rxjava的泄漏场景:

假设我们在Activity#onResume方法里,写了异步任务,任务结束后,设置view的属性,在任务结束之前,我们会调用Activity#finsh操作退出当前页面,如下坨屎:页面在12秒后实际已经处于`onDestroyed状态了

为了演示问题,我将延时时间增大,写成12秒,模拟异步任务返回的情况

1
2
3
4
5
6
7
8
9
10
Observable.timer(12000, TimeUnit.MILLISECONDS)
.subscribeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Long>() {
@Override
public void accept(Long aLong) throws Exception {
dataBinding.layoutWelcome.setVisibility(View.GONE);
...其他view 的引用
}
});

测试步骤为:进入Activity-立刻退出Activity-一段时间之后观察Activity的内存是否被回收

我们得到一份hprof文件,来分析下

hprof分析-rxjava内存泄漏1

老规矩先看下Instance-Details-Instance区域,Activity的生命周期onDestroyed的值是否为true,按步骤点击一看,确实为true,证明Activity已经离开窗口了,处于销毁的生命周期中,我们期望的时候垃圾回收器可以回收Activity占据的内存,但事实上我们在Hprof文件看到了,表明Activity占据的内存未回收。

紧着着我们面临下一个问题,如何找到导致Activity内存泄漏的原因呢?谁引用了Activity

点击Instance-Details-References区域,我们可以很快得到答案,按步骤点击Jump to Sourcehprof分析-rxjava内存泄漏2-定位代码位置

果然,立刻跳转到内存泄漏所在的代码块,终于我们通过分析hprof文件找到了问题所在:

image-20221118150623915

那么如何解决此问题呢?rxjava提供了CompositeDisposable解决此类泄漏问题,做法如下:

  1. 创建实例对象
1
2
3
4
5
/**
* 管理rxjava的任务,及时释放,不执行emitter#onNext
*/
public CompositeDisposable compositeDisposable = new CompositeDisposable();

  1. compositeDisposable实例去控制任务的生命周期
1
2
3
4
5
6
7
8
9
10
compositeDisposable.add(Observable.timer(12000, TimeUnit.MILLISECONDS)
.subscribeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Long>() {
@Override
public void accept(Long aLong) throws Exception {
dataBinding.layoutWelcome.setVisibility(View.GONE);
processIntent(getIntent());
}
}));

  1. 页面生命周期onDestroyed期间清空任务
1
2
3
4
5
6
@Override
protected void onDestroy() {
super.onDestroy();
compositeDisposable.clear();
}

优化后的效果:

hprof分析-rxjava内存泄漏-优化后

优化后可看到Depth为空,GC root 为空,表明没有其他实例引用Activity了,当垃圾回收器扫描到此实例,该实例内存会被回收。

还记得开头的问题吗?

  1. rxjava就会存在内存泄漏吗?答:会存在,consumer作为Activity的内部类,持有当前Activity的引用,任务未结束,Activity已销毁就会出现内存泄漏
  2. subscribe传入的匿名内部类Consumer实例不会造成内存泄漏吗?答:只要是匿名内部类,就很有可能内存泄漏,上例子已经证明会产生内存问题。
  3. 异步任务返回时,Activity已经处于onDestroyed状态,Observer持有``Activity引用,Activity`内存还能被回收吗?答:无法被回收

片段式的经验#

  1. 静态内部类多次创建,有什么问题吗?-不确定
  2. 静态内部类的实例,需要手动释放吗?-不确定
  3. 仍未找到准确的属性能够判定view是否离开窗口
  4. 静态类要调用一些非静态方法,可能需要扩展更多的弱引用实例通过静态类构造函数传进来
  5. 跨App跳转,很难避免标准模式的Activity不被多次创建,但是可以做到Activity退出一次,就释放一次Activity内存,这是基本要求
  6. 静态内部类使用某些实例前,可能需要判断该实例是否为空——在异步场景很容易出现空指针
  7. 尽量不要在xml中定义一些view,如TextureViewSurfaceview,他们默认是与Activity绑定的,他们的构造函数传入的contextActivity实例,有可能造成内存泄漏,最好改为代码动态创建这些复杂的view
  8. glide为什么会内存泄漏?
  9. MVP为什么会内存泄漏?参考DeviceSetFragment的泄漏点3例子
  10. contextview持有,view需要及时释放对context的引用,释放方式为view = null;这样就可以减少指向context的引用了;当指向context的引用总数为0context(Activity)就会被回收了
  11. 有些内存问题需要特定的条件才能出现,如开启关闭摄像头、输入搜索框之后清空搜索框、先进入B页面再回退刷新A页面,monkey无法替代人来操作这些步骤,人为点击还是有意义的
  12. 发现一个泄漏点,解决完之后,再重复测试,才有可能发现下一个泄漏点,只有解决第一个泄漏点,才能发现其他未出现的泄漏点
  13. 优化完之后可能会发现原先无法复现的新问题,优化之后要继续进行大量的重复测试,直到确保不出现泄漏情况才能结束优化工作

待求证的疑问#

  1. System.exit(0)杀死进程,能释放该进程下的所有内存吗?
  2. 非静态内部类持有当前类的引用,还是非静态内部类的实例持有当前类的引用?
  3. 有必要把所有的内部类都定义成静态吗?参考java - Java 内部类有坑。。100 % 内存泄露!_个人文章 - SegmentFault 思否(8 封私信 / 80 条消息) 为什么Java内部类要设计成静态和非静态两种? - 知乎 (zhihu.com)
点击查看
-------------------本文结束 感谢您的阅读-------------------