MemoryMonitor

Introduction: 一个给开发者使用的 Android App 内存清理、监控工具。
More: Author   ReportBugs   DemoAPK   
Tags:
MemoryMonitor-

一个给开发者使用的 Android App 内存清理、监控工具。

主要包括三部分内容:

  • 内存清理

通过内存清理可以模拟系统内存不足时对进程的回收。

  • Pss 监控

通过内存监控可以监控指定应用程序使用的 total Pss 以及当前手机的内存使用情况,从而检测该应用是否存在内存泄漏。

  • 内存优化

整理了一些关于内存优化的 tips,以及一些可能导致内存溢出的场景示例,包含错误的写法和正确的写法。

1.内存清理

类似各种手机管家的 加速球,获取系统已用内存比率、可用内存大小,一键清理。

可以用于测试自己开发的 Activity、Fragment 健壮性,模拟 Activity、Fragment 被回收的场景,测试自己的程序是否完好的保存、恢复当前场景。

比如:打开你开发的某个 Activity、Fragment,切到后台,清理一次内存,在将其切回前台后,看会不会出现空指针异常,以及程序状态是否被恢复。

2.Pss 监控

Android 系统中的内存和 Linux 系统一样,存在着大量的共享内存。每个 APP 占内存会有私有和公共的两部分,我们可以通过 App 的 Pss 值,可以获取到这两部分内存。

Pss(Proportional Set Size):实际使用的物理内存,即:自身应用占有的内存+共享内存中比例分配给这个应用的内存。

通过该程序,每隔 1 秒,获取一次被监控 App 的 Total Pss 值。

使用某个功能(可能会导致 OOM 的那些都要试试),查看 Pss 是否飙升,或者使用过许久都没有降低。

如果使用后飙升并且长时间都降不下来,那就说明肯定会导致 OOM(对象使用过之后还被引用着未释放),如果使用之后 Total Pss 飙升,但是使用过之后能降下来,也可能会导致 OOM,我们还是需要去一点一点排查是什么原因导致的。

如果使用后飙升并且长时间都降不下来,我们就需要 使用 MAT 来进一步分析问题所在

此处提到的 Pss,也可以使用 adb 命令

adb shell dumpsys meminfo your packageName

查看:

total Pss

3.内存优化

Android 的虚拟机是基于寄存器的 Dalvik,它的最大堆大小一般比较小(最低端的设备 16M,后来出的设备变成了 24M,48M 等等),因此我们所能利用的内存空间是有限的。如果我们使用内存占用超过了一定的限额后就会出现 OutOfMemory 的错误。

可能会导致内存溢出的情况有以下几种:

对静态变量的错误使用

如果一个变量为 static 变量,它就属于整个类,而不是类的具体实例,所以 static 变量的生命周期是特别的长,如果 static 变量引用了一些资源耗费过多的实例,例如 Context,就有内存溢出的危险。

Google 开发者博客,给出了一个例子,专门介绍长时间引用 Context 导致内存溢出的情况。

示例代码:

private static Drawable sBackground;

@Override
protected void onCreate(Bundle state) {
    super.onCreate(state);

    TextView textView = new TextView(this);
    textView.setText("Leaks are bad");

    if (sBackground == null) {
        sBackground = getResources().getDrawable(R.drawable.large_bitmap);
    }

    textView.setBackgroundDrawable(sBackground);

    setContentView(textView);
}

这种情况下,静态的 sBackground 变量,虽然没有显式的持有 Context 的引用,但当我们执行view.setBackgroundDrawable(Drawable drawable);的时候,drawable 对象会将当前 view 设置为一个回调,通过 View.setCallback(this) 方法。

具体可见 View 类的源码:

public void setBackgroundDrawable(Drawable background) {
    //...

    if (mBackground == null || mBackground.getMinimumHeight() != background.getMinimumHeight() ||
                mBackground.getMinimumWidth() != background.getMinimumWidth()) {
                requestLayout = true;
        }

        background.setCallback(this);
            if (background.isStateful()) {
                background.setState(getDrawableState());
        }

        background.setVisible(getVisibility() == VISIBLE, false);
        mBackground = background;

    //...
}

background.setCallback(this); 代码块就是我们说的设置回调。

所以,这种情况就会存在这么一个隐式的引用链:Drawable 持有 View,而 View 持有 Context,sBackground 是静态的,生命周期特别的长,于是就会导致了 Context 的溢出。

解决办法:

1.不用 activity 的 context 而是用 Application 的 Context;

private static Drawable sBackground;

@Override
protected void onCreate(Bundle state) {
    super.onCreate(state);

    TextView textView = new TextView(this.getApplication());
    textView.setText("Leaks are bad");

    if (sBackground == null) {
        sBackground = getResources().getDrawable(R.drawable.large_bitmap);
    }

    textView.setBackgroundDrawable(sBackground);

    setContentView(textView);
}

2.在 onDestroy()方法中,解除 Activity 与 Drawable 的绑定关系,从而去除 Drawable 对 Activity 的引用,使 Context 能够被回收;

@Override
protected void onDestroy() {
    super.onDestroy();

    ViewUtils.unbindDrawables(findViewById(android.R.id.content));

    System.gc();
}

public static void unbindDrawables(View view) {
    if (view.getBackground() != null) {
        view.getBackground().setCallback(null);
    }

    if (view instanceof ViewGroup) {
        for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
            unbindDrawables(((ViewGroup) view).getChildAt(i));
        }
        ((ViewGroup) view).removeAllViews();
    }
}

长周期内部类、匿名内部类长时间持有外部类引用导致相关资源无法释放

长周期内部类、匿名内部类,如 Handler,Thread,AsyncTask 等。

HandlerOutOfMemoryActivity 所示的是 Handler 引发的内存溢出。

ThreadOutOfMemoryActivity 所示的是 Thread 引发的内存溢出。

AsyncTaskOutOfMemoryActivity 所示的时 AsyncTask 引发的内存溢出。

Bitmap 导致的内存溢出

一般是因为尝试加载过大的图片到内存,或者是内存中已经存在的过多的图片,从而导致内存溢出。

数据库 Cursor 未关闭

正常情况下,如果查询得到的数据量较小时不会有内存问题,而且虚拟机能够保证 Cusor 最终会被释放掉,如果 Cursor 的数据量特表大,特别是如果里面有 Blob 信息时,应该保证 Cursor 占用的内存被及时的释放掉,而不是等待 GC 来处理。

单例模式引用 Context 导致的内存泄露

如果在某个 Activity 中使用 Singleton instance = Singleton.getInstance(this); 就会造成该 Activity 一直被 Singleton 引用着,不能释放。这时候,正确的做法是使用 getApplicationContext() 来替代 Activity 的 Context ,这样就能避免内存泄露。

代码中一些细节

  • 尽量使用 9path
  • Adapter 要使用 convertView
  • 各种监听,广播等,注册的同时要记得取消注册
  • 使用完对象要及时销毁,能使用局部变量的不要使用全局变量,功能用完成后要去掉对他的引用
  • 切勿在循环调用的地方去产生对象,比如在 getview()里 new OnClicklistener(),这样的话,拖动的时候会 new 大量的对象出来。
  • 使用 Android 推荐的数据结构,比如 HashMap 替换为 SparseArray,避免使用枚举类型(在 Android 平台,枚举类型的内存消耗是 Static 常量的的 2 倍)
  • 使用 lint 工具优化工程
  • 字符串拼接使用 StringBuilder 或者 StringBuffer
  • 尽量使用静态匿名内部类,如果需要对外部类的引用,使用弱引用
  • for 循环的使用 用 final int size = array.length; for(int i = 0; i< size;i++) 来替代: for(int i =0;i < array.length;i++)

最后,整理了一些开发中可能会导致内存溢出的场景,放在 com.cundong.memory.demo.wrong 中,并且给出了优化方法,放在 com.cundong.memory.demo.right 中。

4.截图

截屏

Apps
About Me
GitHub: Trinea
Facebook: Dev Tools