------《Android内存泄漏》
- 什么是内存泄漏
- 常见的内存泄漏以及规避方式
- 单例模式引用Activity
- 非静态内部类
- 注册的反注册
- 定时器Timer
- WebView的内存泄漏
- 资源未关闭
- 属性动画
- 怎么定位内存泄漏
- LeakCanary
- 接入:
- 使用:
- 检测:
- Android Proflier
- 开始检测:
- 定位:
- MAT
- 下载MAT
- 配置MAT环境(mac)
- 用profile获取内存分析文件
- 打开终端,进行文件转换
- 打开mat工具,导入我们的-66 文件
什么是内存泄漏
首先我们创建对象的时候比如我们申请一个String类型的List声明为对象A
List A = new ArrayList<>();
这时候等号前面的A,程序会为我们分配到栈中作为引用。
等号后面的new ArrayList,会被我们分配到堆中。
但是由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序卡顿甚至OOM崩溃。此类情况就称为内存泄漏。
常见的内存泄漏以及规避方式
有的同学可能会问JVM有本身的GC机制为什么还会内存泄漏。
虽然有GC机制,但是有些对象在特殊情况下没有满足GC回收条件(引用计数、根搜索)
①:强引用持有的对象。
②:虽然不用这个对象了,但是没释放这个引用导致没有满足GC回收条件。
简单的总结就是:一个对象的引用生命周期超过了本应该存活的存活周期。
单例模式引用Activity
单例模式引用Activity的上下文,单例的生命周期在声明后和应用程序相同。
当Activity关闭时,但是由于单例模式还引用着所以导致不满足GC无法被回收。
解决:用全局的上下文,比如application.getContext。
非静态内部类
非静态内部类(包括匿名内部类)默认就会持有外部类的引用,当非静态内部类被引用导致外部类也无法释放。
典型的就是Handler的使用。很多开发者会去像下面快速的实现Handler的使用:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
start();
}
private void start() {
Message msg = Message.obtain();
msg.what = 1;
mHandler.sendMessage(msg);
}
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == 1) {
// 做相应逻辑
}
}
};
}
当Activity退出后,msg可能仍然存在于消息对列MessageQueue中未处理或者正在处理,那么这样就会导致Activity无法被回收,以致发生Activity的内存泄露
解决:用静态内部类加弱引用(会被GC快速的回收)。去声明我们的Handler
public class MainActivity extends AppCompatActivity {
private Handler mHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mHandler = new MyHandler(this);
start();
}
private void start() {
Message msg = Message.obtain();
msg.what = 1;
mHandler.sendMessage(msg);
}
private static class MyHandler extends Handler {
private WeakReference<MainActivity> activityWeakReference;
public MyHandler(MainActivity activity) {
activityWeakReference = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = activityWeakReference.get();
if (activity != null) {
if (msg.what == 1) {
// 做相应逻辑
}
}
}
}
}
相同的还有内部类造成内存泄露还有一种情况就是使用Thread或者AsyncTask。那么解决方法都是相同的,因为他们的生成原因是相同的。
注册的反注册
比如我们在Activity中注册广播,如果在Activity销毁后不取消注册,那么这个广播会一直存在系统中,同上面所说的非静态内部类一样持有Activity引用,导致内存泄露。同样的问题还会存在一些需要注册和反注册的自定义监听中。
解决:因此注册广播、监听后在Activity销毁后一定要取消注册。
定时器Timer
Timer和TimerTask在Android中通常会被用来做一些计时或循环任务,比如实现无限轮播的ViewPager
解决:当我们Activity销毁的时候要立即cancel掉Timer和TimerTask,以避免发生内存泄漏。
WebView的内存泄漏
关于WebView的内存泄露,因为WebView在加载网页后会长期占用内存而不能被释放,因此我们在Activity销毁后要调用它的destory()方法来销毁它以释放内存。
解决:在销毁WebView之前需要先将WebView从父容器中移除,然后在销毁WebView。
资源未关闭
在使用IO、File流或者Sqlite、Cursor等资源时要及时关闭。这些资源在进行读写操作时通常都使用了缓冲,如果及时不关闭,这些缓冲对象就会一直被占用而得不到释放,以致发生内存泄露。
解决:因此我们在不需要使用它们的时候就及时关闭,以便缓冲能及时得到释放,从而避免内存泄露。
属性动画
动画同样是一个耗时任务,比如在Activity中启动了属性动画(ObjectAnimator),但是在销毁的时候,没有调用cancle方法,虽然我们看不到动画了,但是这个动画依然会不断地播放下去,动画引用所在的控件,所在的控件引用Activity,这就造成Activity无法正常释放。
解决:因此同样要在Activity销毁的时候cancel掉属性动画,避免发生内存泄漏。
怎么定位内存泄漏
LeakCanary
接入:
因为Leaks会安装到我们的终端上,所以我们只需要在debug时安装,release时不安装
因此需要使用debugImplementation在module的build.gradle中添加一行
debugImplementation ‘com.squareup.leakcanary:leakcanary-android:2.7’
没错,只需要添加这一行就完事了,其他的初始化操作会在LeakCanary库中自动进行。
使用:
LeakCanary启动起来后在Logcat里会有一行日志
D LeakCanary: LeakCanary is running and ready to detect leaks
这说明我们已经将Leaks运行起来了
检测:
我们只需要随意进入一些页面再退出,或者如果想专门查看某一个Activity是否有泄漏,就多次进入这个Activity再退出,过一会后,会弹出一个这样的toast。就说明我们这个页面是有内存泄漏问题的。
如果没有提示的话,那么我们需要主动打开终端上携带安装的Leaks
进去入口后点击这个Dump Heap Now按钮,也可以立马执行内存分析
这时候,我们去查看我们的日志台
主要关注Leaking字段
Leaking有三个状态,代表不同的含义
Leaking:YES:发生了内存泄漏
Leaking:NO:未发生内存泄漏
Leaking:UNKNOW:未知,可能发生泄漏
这里我们的截图显示
在MainActivity2发生了一处内存泄漏,具体位置是在MyHandler,通过这个日志台,我们就可以清楚的知道内存泄漏的情况和具体未知了。
或者在我们的Leaks上也会有日志输出:
之后我就可以按照提示对我们的内存泄漏问题进行修改了。
Android Proflier
开始检测:
当我们的LeakCanary的信息无法帮我们定位到内存泄漏的位置,但是又提示我们有内存泄漏的。那么我们可以在相关的位置利用Android Proflier进行协助我们定位。
1、打开我们的Android Studio上工具栏处的Profiler
2、点击加号
3、选择我们要检测的进程
4、绿灯表示正在检测的进程
定位:
双击内存条区域
出现下面的界面
这时候去操作你要检测的位置,或者动作。也就是你Leaks提示但是你找不到的区域。
之后多次点击鼠标右键“Force garbage collection”手动回收内存等待几秒
再右键点击dump java head如果没有的话(高版本的)在左侧会有Capture heap dump之后去进行record
就会进入自动内存泄漏分析界面
选择我们的包名,选择有内存泄露(红色框框标记处)的类或对象
有黄色感叹号的“This is 1 leadk”即有内存泄露
最后分析内存泄露源码对象,更改后按照上面步骤重试即可,直至没有泄露。
MAT
下载MAT
下载地址
配置MAT环境(mac)
因为从 android profile直接获取到的hprof文件格式与mat的格式不兼容,所以需要使用工具转换一下
1、打开终端输入:
echo $HOME
2、继续输入:
touch .bash_profile
3、继续输入:
open -e .bash_profile
4、在打开的bash文件中输入:
export PATH=${PATH}:/Users/用户名/你的sdk路径/platform-tools
5、最后输入:
source .bash_profile
用profile获取内存分析文件
保存为memory-99.hprof
打开终端,进行文件转换
转换格式 : hprof-conv before.hprof after.hprof
我们这里输入 : hprof-conv memory-99.hprof 66.hprof 能看懂吧,吧我们的源文件 -99 转换成 -66文件
打开mat工具,导入我们的-66 文件
点击红框的选项,这个是进行内存泄漏分析的
下面就是这段时间所产生的对象,点击红框 可以直接搜索你要分析的对象
这里找到了我们的VideoPlayerActivity
鼠标右键选择
可以看到,意思就是我们排除掉软、弱、虚引用,因为这几种是不会造成内存泄漏的,可以不用管它,我们只需要看排除后还有没引用存在,有的话 那就是强引用了,也就发生了内存泄漏了。
最后得到这样的结果
然后结合代码分析并解决问题!!至于还有很多可做的一些操作,那就需要自己去发现。