(Android优化)跟内存泄漏说再见

饥饿让人充满智慧

说到内存泄漏导致OOM(out of memory·)很多人都觉得和自己的应用遥不可及,现在市场上开始出现6G RAM,8 GRAM内存泄漏那么一点点又怎样?其每台android机在出厂时就已经配置了单个APP内存的最大可用内存,所以6G甚至8GRAM 除了分配的自留地,其他都是公家地盘,你并不能利用起来,我们可以通过adb命令来查看当前设备配置的可用内存大小

1
2
3
4
5
6
7
8
9
10
//当配置了heapgrowthlimit的值时以heapgrowthlimit为准,heapsize
//的值表示极限堆的大小,使得应用即使OOM也不会导致系统崩溃,但是一般超
//过heapgrowthlimit大小就要报OOM了,应用也就崩溃了
$adb shell getprop dalvik.vm.heapgrowthlimit

320m

$adb shell getprop dalvik.vm.heapsize

512m

所以当应用不断运行不能被回收的内存越来越多,离OOM也就不远了.所以解决内存泄漏从而避免导致OOM刻不容缓。

什么是内存泄漏

当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。

Android中常见的内存泄漏

1.内部类/匿名内部类持有外部类引用

首先:内部类和静态内部类有什么区别?那就是内部类持有外部类的引用,静态内部类不持有外部类引用
内部类在编译的时候和外部类会生成两个class文件,其中内部类会默认创建一个含有外部类的构造,我们在写内部类时访问外部类变量直接就可以访问,就是因为内部持有外部类的引用.那为什么静态内部类又不持有呢?这也很好理解,静态的内部类的创建不依赖外部类,如果持有外部类引用,当静态内部类被new出来的时候外部类还没初始化那就会造成空指针异常,弄清楚上面两点我们。我们平时都犯了什么错!

  • 1.Handler使用时的内存泄漏

看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//当加载数据完毕需要更新界面数据
Handler mhandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case SUCCESS_LOAD:
//加载成功
break;
case FAIL_LOAD:
//加载失败
break;
case ERR_LOAD:
//加载失败
break;
default:
break;
}

}
};

我们平时创建handler就信手拈来,直接在Activity中创建匿名内部类的handler,在进入界面或则界面需要更新数据的时候来个网络请求,请求完将数据sendMessage给了handler,在handler中来更新UI,但是当数据到来的时候你这个当前的Activity还在吗?如果用户退出了当前Activity,那么被handler持有的外部类Activity将得不到释放.所以,得改

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
34
35
36
private DataHandler mhandler=new DataHandler(this);
//当加载数据完毕需要更新界面数据
private static class DataHandler extends Handler{
private WeakReference<Context> wf_context;

public DataHandler(Context context) {
wf_context=new WeakReference<Context>(context);
}

@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);

switch (msg.what) {
//视频新闻加载成功
case SUCCESS_LOAD:

break;
//视频新闻加载失败
case FAIL_LOAD:

break;
case ERR_LOAD:

break;
default:
break;
}
}
}

@Override
protected void onDestroy() {
super.onDestroy();
mhandler.removeCallbacksAndMessages(null);
}

我们将匿名内部类的handler改成静态内部类,因为我们可能要在handle中更改ui。所以我们将Context通过构造传入,在传入之后用弱引用WeakReference来包裹Context,使得当发生GC操作时弱应用持有的对象在GCRoot不可达的情况下被回收。我们甚至直接在onDestroy中执行
handler的removeCallbacksAndMessages来清空Looper中的消息.

JAVA中有四种引用类型,详细介绍另找文献
StrongReference强引用,这是最为普遍的引用类型,当一个对象中依赖另一个对象这就是强引用类型,当程序内存不够时,程序甚至宁愿抛出OOM也不愿释放强引用
SoftReference软引用,被SoftReference包裹的引用在应用的内存不够用的时候将被释放
WeakReference弱引用,被WeakReference包裹的引用在发生GC操作并GCRoot不可达时被释放
PhantomReference虚引用,被PhantomReference包裹的引用时,你get()获取的对象总是为空。

  • 2.延时、耗时、异步操作

我们从handler的例子可以看出,其实主要原因还是信息的加载不同同步的导致在消息到来时,持有的变量生命周期已经走完但是由于被handler强引用导致不能被GC回收。由此举一反三很多操作也是如此,如new Thread(),new AsyncTask,接口回调,如果在Activty中创建这些对象或执行这些操作,确保内部类是静态内部类,不管是否重新新建一个Class文件,确保其持有的外部变量能够被GC回收(比如用WeakRefrence包裹)

2.单例中的内存泄漏

在创建单例时,很多时候我们都需要外部资源来帮助其初始化,比如

1
2
3
4
5
6
7
8
9
10
11
12
// 用于返回一个VolleyController单例
public static VolleyController getInstance(Context context) {
if (mInstance == null) {
synchronized(VolleyController.class)
{
if (mInstance == null) {
mInstance = new VolleyController(context);
}
}
}
return mInstance;
}

很多时候单例的创建我们十分的随意,想起来就getInstance(this),仔细想想,如果在一个Activity,或则Service中创建了这个单例,当这个Activity或Service生命周期走完,那么持有的context将被泄漏,对于传入Context我们可以这样改

1
2
3
4
5
6
7
8
9
10
11
12
// 用于返回一个VolleyController单例
public static VolleyController getInstance(Context context) {
if (mInstance == null) {
synchronized(VolleyController.class)
{
if (mInstance == null) {
mInstance = new VolleyController(context.getApplicationContext());
}
}
}
return mInstance;
}

这样使得不管传入什么Conetxt,持有的都将是ApplicationContext,而Application存活在整个应用的生命周期中所以Application不会需要被回收除非应用结束。对于单例还有个建议就是如果单例的初始化不是特别消耗资源和耗时可以放在Application中先初始化好。

3.资源未被释放

我们在注册广播registerReceiver之后android都会要求我们在onDestroy中执行unregisterReceiver来释放资源,在我们的编程中也是这样,比如BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时或直接是使用完时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。这也包括监听,我们写接口回调来监听却很少人执行removeListener(),所以在onDestroy时执行反监听操作是十分必要的。

4.非静态内部类创建静态实例造成的内存泄漏

很多时候为了方便在静态方法中调用或则是别的类中调用,我们很可能会写出这样的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MainActivity extends AppCompatActivity {
private static ResourceInit mResource = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(mResource == null){
mResource = new ResourceInit ();
}
//...
}
class ResourceInit {
//...
}
}

这样每当Activity再次进入那么就会new一个新的对象,并且这个对象将得不到回收,如果非要这样做,可以把内部类声明为静态内部类,或则将其另创一个类实现单例模式,
上面就是几种常见的内存泄漏了。

内存泄漏的查找

人非圣贤,所以除了在写代码的时候注意内存泄漏更重要的是找出内存泄漏的地方。

1.初步排查

1.1利用日志初步判断
这里写图片描述
运行应用后,在DDMS或Android Device Monitor中设备选中需要调试的应用,然后点击GC按钮手动触发GC再操作应用回到之前的界面后再次点击GC操作异常反复多次我们在Logcat中过滤GC日志可以看到(因为只有GC不能回收的内存才能算是泄漏了)
这里写图片描述

类似上面logcat打印一样,触发垃圾回收的主要原因有以下几种:
GC_MALLOC——内存分配失败时触发;
GC_CONCURRENT——当分配的对象大小超过384K时触发;
GC_EXPLICIT——对垃圾收集的显式调用(System.gc()) ;
GC_EXTERNAL_ALLOC——外部内存分配失败时触发;

其中我们手动触发的GC类型为GC_EXPLICIT
我们看到最后一条日志

1
Explicit concurrent mark sweep GC freed 8705(576KB) AllocSpace objects, 22(3MB) LOS objects, 29% free, 9MB/13MB, paused 549us total 10.308ms

我们可以看到最后一次释放了AllocSpace 对象8705共576KB,LOS(Large Object Space) objects 22个共3MB ,已用9MB共,13MB可用29% 。在回收时主线程暂停了549us 耗时10.308ms
我们可以从以上几条日志看出可用空间越来越小,但是我们还是在之前的页面所以初步判定可能存在内存泄漏!
1.2查看对象个数变化
这里写图片描述
运行程序,操作运用,回到某个界面点击GC,然后点击旁边的 Ssytem info选择Memory Usage 多次操作比较多个生成文件的Object个数,观看Activty,APPContext等的个数,因为一般都是Activity和Context内存泄漏了

2.利用hprof文件进一步排查

这里写图片描述
操作应用,回到之前界面,点击GC回收一波内存,点击dump java heap按钮,生成hprof文件
这里写图片描述
我们可以根据自己的习惯选择查看方式,比如现在是App Heap仅看APP的内存分配,package tree view包的树状图,下面出现了对应的类,上面的描述分别是
列名 | 描述
—-|——
Class Name| 类名,Heap中的所有Class
Heap Count | 堆内存中这个类 对象的个数
Sizeof | 每个该实例占用的内存大小
Shallow Size| 所有该类的实例占用的内存大小
Retained Size| 所有该类对象被释放掉,会释放多少内存
我们可以看到多个Activty的个数大于1,所以很明显内存泄漏了,我们点击其中一个NewsActivity可以到右侧显示了详细信息
列名 | 描述
—-|——
Instance | 该类的实例
Depth | 深度, 从任一GC Root点到该实例的最短路径
Dominating Size | 该实例可支配的内存大小
我们分别点击可以看到第二个的详细信息,很明显现在News_Pageadapter这个类的内部类MyThread中context发生了内存泄漏,选中右键,选择jump to source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//图片轮播的线程
class MyThread extends Thread {
@Override
public void run() {
super.run();
while (true) {
while (!isstop) {
try {
sleep(4500);
index++;
handler.sendEmptyMessage(R.id.image_thread);

} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}

到这里就很明显了,一个线程的内部类持有了外部类的引用使得Activity退出时GC没能将其回收。

3.利用MAT分析

如果你没能分析出来也可以借助另一个工具MAT,这是一个Eclipse的插件,可以单独安装(点击下载)这是用来分析Java的内存信息,我们可以在Android Studio中点击Captures找到刚才生成的hprof文件,MAT只能识别标准的hprof文件我们需要在这转换,并保存他
这里写图片描述
用下载好的MAT打开
这里写图片描述
其中常用的有直方图(Histogram),Dominator Tree(支配树)在他们的第一行是检索输入框,支持正则表达式,我们输入之前的NewsActivity在直方图和支配树中都可以看到有两个实例
我们右键其中一个可以看到很多的菜单供选择
List objects with (以Dominator Tree的方式查看)

  • incoming references 引用到该对象的对象
  • outcoming references 被该对象引用的对象
    Show objects by class(以class的方式查看)
  • incoming references 引用到该对象的对象
  • outcoming references被该对象引用的对象
    我们右键选择exclude all phantom/weak/soft etc.references, 意思是查看排除虚引用/弱引用/软引用等的引用链 (这些引用最终都能够被GC干掉,所以排除)
    这里写图片描述
    我们可以看到结果和之前分析的一样
    这里写图片描述
    相对于Android Atudio自带的分析工具MAT具有更加强大的功能,适用于复杂的情况来跟踪内存泄漏的痕迹。
    4.Leakcanary
    相对于上面一个个精确的查找分析,在APP优化的初期我还是更加倾向于第三方库Leakcanary,Leakcanary是大名鼎鼎的Square的开源项目,使用Leakcanary,当有内存泄漏时,会直观的在手机通知栏显示,并可点击进去查看详情
    配置
    在项目的build.gradle文件添加:
1
2
3
4
dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'
}

在自定义的Application中初始化(别忽略了在清单文件中配置自定义的Application)

1
2
3
4
5
public class App extends LitePalApplication{
@Override
public void onCreate() {
super.onCreate();
LeakCanary.install(this);

}
}

1
如果你想观察Fragment的内存泄露需要这样改下:

private RefWatcher refWatcher;
public static RefWatcher getRefWatcher(Context context) {
App application = (App) context.getApplicationContext();
return application.refWatcher;
}

public class App extends LitePalApplication{
@Override
public void onCreate() {
super.onCreate();
LeakCanary.install(this);

1
2
}
}

在Fragment中

1
2
3
4
5
6
7
8
public abstract class BaseFragment extends Fragment {

@Override public void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = APP.getRefWatcher(getActivity());
refWatcher.watch(this);
}
}

配置完成直接运行之前的APP,当有内存泄漏时他会在通知栏弹出(需要注意的是,这不是实时的,可能会有好几秒的延迟,因为他的原理也是分析生成的hprof文件,耗时可能比较长)
这里写图片描述
我们点击进去可以查看内存泄漏详情
这里写图片描述
很明显这是在代码中让AudioManager强引用了VedioNewsDetailActivity

内存泄漏分析基础大概就是这些了,可以解决大多数的内存泄漏问题.

您的一份奖励,就是我的一份激励