起源
有趣的灵魂千奇百怪,内存泄漏的也是各式各样
我在15年写过一遍 文章 《 android中常见的内存泄漏和解决办法》http://blog.csdn.net/wanghao200906/article/details/50426881,时隔三年居然还有人我问 该如何解决 内存泄漏的问题,因为 有趣的灵魂 千奇百怪,所以 内存泄漏的也是各式各样,所以想避免 内存泄漏 ,不能只记住 常见 问题的代码,而是要学会如果发现内存泄漏的方法。
学习内容
- 内存泄漏的一些 基础支持(估计有你不会的)
- 学会使用android studio 3.0 自带的 android profile 检查内存泄漏
- 使用 mat 工具 来检查 内存泄漏
内存泄漏的基础
内存泄漏:内存不在GC掌控之内了。当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用从而就导致,对象不能被回收。这种导致了本该被回收的对象不能被回收而停留在堆内存中,就产生了内存泄漏
四中引用:
- StrongReference强引用:
回收时机:从不回收 使用:对象的一般保存 生命周期:JVM停止的时候才会终止 - SoftReference软引用
回收时机:当内存不足的时候;使用:SoftReference结合- ReferenceQueue构造有效期短;生命周期:内存不足时终止 - WeakReference,弱引用
回收时机:在垃圾回收的时候;使用:同软引用; 生命周期:GC后终止 - PhatomReference 虚引用
回收时机:在垃圾回收的时候;使用:合ReferenceQueue来跟踪对象呗垃圾回收期回收的活动; 生命周期:GC后终止
开发时,为了防止内存溢出,处理一些比较占用内存大并且生命周期长的对象的时候,可以尽量使用软引用和弱引用。
软引用比LRU算法更加任性,回收量是比较大的,你无法控制回收哪些对象。
因为我们主要是 查找 内存泄漏 ,这里不对 基础知识 做过多的扩充
内存分配
- 成员变量全部存储在堆中(包括基本数据类型,引用及引用的对象实体)—因为他们属于类,类对象最终还是要被new出来的。
- 局部变量的基本数据类型和引用存储于栈当中,引用的对象实体存储在堆中。—–因为他们属于方法当中的变量,生命周期会随着方法一起结束。
看下面代码
public class Main{
int a = 1; // a 和1 都在堆里
Student s = new Student();// s 和new d的Student()都在 堆里
public void XXX(){
int b = 1;//b 和 1 栈里面
Student s2 = new Student();// s2 在栈里, new的 Student() 在堆里
}
}
ok 如果你不同意,在你查询完资料之后, 欢迎来讨论
基础知识就到这吧。下面开始主题
Android Profiler 查找内存泄漏
该方法可以解决一部分问题,但不是很好用,之所以得学习他是因为为后面做个铺垫
下面来介绍一下,因为不是特别好用 所以用一个简单的例子介绍一下,
看下面的代码
package sven.com.practise32_performance_optimization;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
CommonUtils.getInstance(this);
}
}
package sven.com.practise32_performance_optimization;
import android.content.Context;
/**
* Created by wanghao on 2018/2/8.
*/
public class CommonUtils {
private static CommonUtils instance;
private Context context;
private CommonUtils(Context context) {
this.context = context;
}
public static CommonUtils getInstance(Context context) {
if (instance == null) {
instance = new CommonUtils(context);
}
return instance;
}
}
我们看一下 android profiler
这样的。然后我们点击中间的MEMORY.
然后我们执行如下操作。
我们 横屏 , 然后在竖屏
- 横屏一下 就会将该MainActivity onDestory掉。然后在onCreate();
横屏竖屏,操作结束后我们点击 箭头指向的按钮,如下图
我们看到最下面
这就是 堆的一些信息。
一些信息的含义
- Heap Count:堆中的实例数。
- Shallow Size:此堆中所有实例的总大小(以字节为单位)。
Retained Size:为此类的所有实例而保留的内存总大小(以字节为单位)。
在类列表顶部,您可以使用左侧下拉列表在以下堆转储之间进行切换:Default heap:系统未指定堆时。
- App heap:您的应用在其中分配内存的主堆。
- Image heap:系统启动映像,包含启动期间预加载的类。 此处的分配保证绝不会移动或消失。
Zygote heap:写时复制堆,其中的应用进程是从 Android 系统中派生的。
默认情况下,此堆中的对象列表按类名称排列。 您可以使用其他下拉列表在以下排列方式之间进行切换:Arrange by class:基于类名称对所有分配进行分组。
- Arrange by package:基于软件包名称对所有分配进行分组。
- Arrange by callstack:将所有分配分组到其对应的调用堆栈。 此选项仅在记录分配期间捕获堆转储时才有效。 即使如此,堆中的对象也很可能是在您开始记录之前分配的,因此这些分配会首先显示,且只按类名称列出。
默认情况下,此列表按 Retained Size 列排序。 您可以点击任意列标题以更改列表的排序方式。
在 Instance View 中,每个实例都包含以下信息:
- Depth:从任意 GC 根到所选实例的最短 hop 数。
- Shallow Size:此实例的大小。
- Retained Size:此实例支配的内存大小(根据 dominator 树)。
接下来我们查看app Heap 中的内容
经过漫长的查找。我们找到了MainActivity (这就是我觉得 android profiler 不要用的地方,必须得挨个查找,效率太低了)。
点击MainActivity. 然后右边出来了 Instance View ,里面出现了 三个 MainActivity 。
分析1
- 我们看到Depth 从上到下 是 2 ,0,3 ,上文说了 Depth:从任意 GC 根到所选实例的最短 hop 数。
- 在回忆刚才,我们 打开app,横屏,竖屏,创建了3个MainAvtivity ,这点儿可以理解。
2,0,3 怎么解释呢。
0 就代表该activity,可以被 gc 回收
2 ,3 就代表 最短的hop数不为零。肯定不会被gc回收。- 我们点击第一个MainAvtivity看看
我们看到 instance 这个代码, 是我们自己定义的,它持有了MainActivity 。
- 在看第三个MainAvtivity
我们看到这里就没有刚才 被CommonUtils中 instance 持有的activity。说明 当前的activity。是我们看到的MainActivity。他当然不可能被销毁。同时因为 第一次创建的MainActivity的时候 生成了 CommonUtils中 instance 它不为null。所以 在创建MainActivity的时候 它的上下文不会再次被持有。
到这里我们执行以下gc操作,然后在点击dump操作
在根据刚才的方法 找到MainAvtivity。点击。 结果如下
我们看到 这个 MainAvtivity被持有了。所以MainActivity 在横屏的时候,虽然执行了onDestory(),但依然不会被销毁,他无法被回收,所以就会内存泄露。
到此 android profiler 中 查找内存泄漏的方法就介绍完了。
我觉得的缺点就是,如果代码过于复杂,我们又不知道查找那些代码。只能挨个点击,挨个看,太耗时耗力了。
所以 还是MAT 工具是最好用的。
MAT 完美查找内存泄漏
为了 说明MAT 的强大 我们写一个稍微复杂的内存泄漏的代码
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private static Link linkInstance;
class Link {
public void dosomething() {
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (linkInstance == null) {
linkInstance = new Link();
}
CommonUtils.getInstance(this);
new MyThread().start();
}
class MyThread extends Thread {
@Override
public void run() {
while (true) {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
然后执行才做: 运行 ,横屏 ,竖屏,在android profile中 点击 gc,然后在点击dump java heap。效果如下
点击Export capture to file 保存到本地文件夹。格式为 xxx.hprof 比如 叫 test1.hprof
然后执行命令行
hprof-conv /Users/mypro/Desktop/mat/test1.hprof /Users/mypro/Desktop/mat/test2.hprof
- /Users/mypro/Desktop/mat/test1.hprof 是我们在as中保存的hprof文件
- /Users/mypro/Desktop/mat/test2.hprof 是我们用命令行要生成的 hprof文件,为了mat可以识别
然后 使用MAT 工具了。可惜的是 as没有Mat 工具。只能下载一个 Mat工具
http://www.eclipse.org/mat/downloads.php
下载完之后就安装,打开。然后直接把test2.hprof 文件 拖到 Mat 工具中。
为了 以后的方面快捷,真的很有必要下载一个Mat工具,你是程序员,为了完美的找到内存泄漏的地方,难道还怕麻烦么,难道你看到测试组 报告后 ,你的代码有好多内存泄漏的地方,你说你面子往哪搁。
在选择框中选择第一个,生成报告。
然后点击 histogram,效果如下
我们看到了 有好多的类。跟在android profile中看到的一样。但是这么多。我们不用挨个检查,只要检查我们自己写的代码即可,如果在最上面可以输入包名,然后回车。
可以看到 出现的都是我们自己写的代码了。进行挨个分析
分析
选择第一个右键,List objects -> with incoming references ->回车
- with incoming references : 表示该类被 哪些外部对应 引用了
- with outgoing references : 表示 该类 持有了 哪些外部对象的引用
回车 之后我们发现有三个 类如下图。
我们发现 第一个MainActivity 被 好多 对象引用着了。该如挨个查找呢。莫慌。如下图操作
我们选择 去掉 弱引用,软引用 所 引用的对象。
可以看到 我们的MainAvtivity类,被 一个 MyThread的线程 所引用了。找到以一个 内存泄漏的地方。
因为我们 内部类会持有 外部类的引用。所以 MyThread 类持有了MainActiviy的 上下文。又因为 thread 是一个 while(true)的死循环,所以 不会释放。
我们现在 依据上面 的分析流程,继续查看第二个类。
还是右键->Path to Gc Root -> exclude all phantom/weak/soft etc. references- >回车
ok,我们发现MainActivity 的上下文被好几个类引用了。但是除了MyThread 类是我们自己定义的,其他的类都是 安卓源码,所以我们可以推断,当前的MainActivity类可能是 正在展示在手机页面上的 MainActivity 类。所以该类并不存在内存泄漏的问题。
我们看完第二个 再看看三个 。
还是右键->Path to Gc Root -> exclude all phantom/weak/soft etc. references- >回车
ok,我们看到 三个地方引用 了MainActivity 的 上下文。这三个地方度出现了 内存溢出。
- MainActivity 创建了三次,一共创建三次 MyThread 类,MyThread 类属于内部类,他持有外部类的额引用
- CommonUtils 属于 单利,当CommonUtils 的instance 不为null的时候 就不会再继续持有 MainActivity的上下文引用。所以只持有了一次
- Link 属于 内部类 持有MainActivity 的引用,但是在onCreate里面有判断只有当 Link的instance为null的时候才会创建一次。所以也只持有了 一次MainActivity的上下文引用。
到这里 内存泄漏的方法就介绍完了。学会该方法,不管是什么内存泄漏都可以轻松的找到了。
能找到内存泄漏的地方,基本上就可以解决了。
下面在稍微总结一下 常见内存泄漏的就该方法。。
- 静态变量引起的内存泄露
当调用getInstance时,如果传入的context是Activity的context。只要这个单利没有被释放,那么这个
Activity也不会被释放一直到进程退出才会释放。
public class CommUtil {
private static CommUtil instance;
private Context context;
private CommUtil(Context context){
this.context = context;
}
public static CommUtil getInstance(Context mcontext){
if(instance == null){
instance = new CommUtil(mcontext);
}
// else{
// instance.setContext(mcontext);
// }
return instance;
}
- 非静态内部类引起内存泄露(包括匿名内部类)
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
// private static Link linkInstance;
private Link linkInstance;
private boolean flag = true;
class Link {
public void dosomething() {
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (linkInstance == null) {
linkInstance = new Link();
}
// CommonUtils.getInstance(this);
CommonUtils.getInstance(getApplicationContext());
new MyThread().start();
}
// 错误写法
// class MyThread extends Thread {
// @Override
// public void run() {
// while (true) {
// try {
// sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
// }
// }
// 解决方法 方法1
// class MyThread extends Thread {
// @Override
// public void run() {
// while (flag) { //MainActivity 销毁的时候让thread 停止
// try {
// sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
// }
//
// 解决方法2 让MyThread 为 静态内部类,静态内部类就不会持有 外部类的引用
//
static class MyThread extends Thread {
@Override
public void run() {
while (true) {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
flag = false;
}
}
再来一个 handler的内部类 错误示范
//错误的示范:
//mHandler是匿名内部类的实例,会引用外部对象MainActivity.this。如果Handler在Activity退出的时候,它可能还活着,这时候就会一直持有Activity。
// private Handler mHandler = new Handler(){
// @Override
// public void handleMessage(Message msg) {
// super.handleMessage(msg);
// switch (msg.what){
// case 0:
// //加载数据
// break;
//
// }
// }
// };
//解决方案:
private static class MyHandler extends Handler{
// private MainActivity mainActivity;//直接持有了一个外部类的强引用,会内存泄露
private WeakReference<MainActivity> mainActivity;//设置软引用保存,当内存一发生GC的时候就会回收。
public MyHandler(MainActivity mainActivity) {
this.mainActivity = new WeakReference<MainActivity>(mainActivity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
MainActivity main = mainActivity.get();
if(main==null||main.isFinishing()){
return;
}
switch (msg.what){
case 0:
//加载数据
// MainActivity.this.a;//有时候确实会有这样的需求:需要引用外部类的资源。怎么办?
int b = main.a;
break;
}
}
};
- 不需要用的监听未移除会发生内存泄露
例子1:
// tv.setOnClickListener();//监听执行完回收对象
//add监听,放到集合里面
tv.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() {
@Override
public void onWindowFocusChanged(boolean b) {
//监听view的加载,view加载出来的时候,计算他的宽高等。
//计算完后,一定要移除这个监听
tv.getViewTreeObserver().removeOnWindowFocusChangeListener(this);
}
});
例子2:
SensorManager sensorManager = getSystemService(SENSOR_SERVICE);
Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
sensorManager.registerListener(this,sensor,SensorManager.SENSOR_DELAY_FASTEST);
//不需要用的时候记得移除监听
sensorManager.unregisterListener(listener);
- 资源未关闭引起的内存泄露情况
比如:BroadCastReceiver、Cursor、Bitmap、IO流、自定义属性attribute
attr.recycle()回收。
当不需要使用的时候,要记得及时释放资源。否则就会内存泄露。
- 无限循环动画
没有在onDestroy中停止动画,否则Activity就会变成泄露对象。
比如:轮播图效果。