Android 性能优化以内存泄漏检测以及内存优化(中)

  上篇博客咱们写到了 Java/Android 内存的分配以及相关 GC 的详细分析,这篇博客咱们会继续分析 Android 中内存泄漏的检测以及相关案例,和 Android 的内存优化相关内容。
  上篇:Android 性能优化以内存泄漏检测以及内存优化(上)
  中篇:Android 性能优化以内存泄漏检测以及内存优化(中)
  下篇:Android 性能优化以内存泄漏检测以及内存优化(下)
  转载请注明出处:blog.csdn.net/self_study/…
  对技术感兴趣的同鞋加群544645972一块儿交流。javascript

Android 内存泄漏检测

  经过上篇博客咱们了解了 Android JVM/ART 内存的相关知识和泄漏的缘由,再来归类一下内存泄漏的源头,这里咱们简单将其归为一下三类:html

  • 自身编码引发
  • 由项目开发人员自身的编码形成;
  • 第三方代码引发
  • 这里的第三方代码包含两类,第三方非开源的 SDK 和开源的第三方框架;
  • 系统缘由
  • 由 Android 系统自身形成的泄漏,如像 WebView、InputMethodManager 等引发的问题,还有某些第三方 ROM 存在的问题。

Android 内存泄漏的定位,检测与修复

  内存泄漏不像闪退的 BUG,排查起来相对要困难一些,比较极端的状况是当你的应用 OOM 才发现存在内存泄漏问题,到了这种状况才去排查处理问题的话,对用户的影响就太大了,为此咱们应该在编码阶段尽早地发现问题,而不是拖到上线以后去影响用户体验,下面总结一下经常使用内存泄漏的定位和检测工具:java

Lint

  Lint 是 Android studio 自带的静态代码分析工具,使用起来也很方便,选中须要扫描的 module,而后点击顶部菜单栏 Analyze -> Inspect Code ,选择须要扫描的地方便可:
android

这里写图片描述
      
这里写图片描述


这里写图片描述

最后在 Performance 里面有一项是 Handler reference leaks,里面列出来了可能因为内部 Handler 对象持有外部 Activity 引用致使内存泄漏的地方,这些地方均可以根据实际的使用场景去排查一下,由于毕竟不是每一个内部 Handler 对象都会致使内存泄漏。Lint 还能够自定义扫描规则,使用姿式不少很强大,感兴趣的能够去了解一下,除了 Lint 以外,还有像 FindBugs、Checkstyle 等静态代码分析工具也是很不错的。

StrictMode

  StrictMode 是 Android 系统提供的 API,在开发环境下引入能够更早的暴露发现问题给开发者,于开发阶段解决它,StrictMode 最常被使用来检测在主线程中进行读写磁盘或者网络操做等耗时任务,把这些耗时任务放置于主线程会形成主线程阻塞卡顿甚至可能出现 ANR ,官方例子:git

public void onCreate() {
     if (DEVELOPER_MODE) {
         StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                 .detectDiskReads()
                 .detectDiskWrites()
                 .detectNetwork()   // or .detectAll() for all detectable problems
                 .penaltyLog()
                 .build());
         StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                 .detectLeakedSqlLiteObjects()
                 .detectLeakedClosableObjects()
                 .penaltyLog()
                 .penaltyDeath()
                 .build());
     }
     super.onCreate();
 }复制代码

把上面这段代码放在早期初始化的 Application、Activity 或者其余应用组件的 onCreate 函数里面来启用 StrictMode 功能,通常 StrictMode 只是在测试环境下启用,到了线上环境就不要开启这个功能。启用 StrictMode 以后,在 logcat 过滤日志的地方加上 StrictMode 的过滤 tag,若是发现一堆红色告警的 log,说明可能就出现了内存泄漏或者其余的相关问题了:
github

这里写图片描述

好比上面这个就是由于调用 registerReceiver 以后忘记调用 unRegisterReceiver 致使的 activity 泄漏,根据错误信息即可以定位和修复问题。

LeakCanary

   LeakCanary 是一个 Android 内存泄漏检测的神器,正确使用能够大大减小内存泄漏和 OOM 问题,地址:web

https://github.com/square/leakcanary复制代码

集成 LeakCanary 也很简单,在 build.gradle 文件中加入:正则表达式

dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
   testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
 }复制代码

而后在 Application 类中添加下面代码:shell

public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // This process is dedicated to LeakCanary for heap analysis.
      // You should not init your app in this process.
      return;
    }
    LeakCanary.install(this);
    // Normal app init code...
  }
}复制代码

上面两步作完以后就算是集成了 LeakCanary 了,很是简单方便,若是程序出现了内存泄漏会弹出 notification,点击这个 notification 就会进入到下面这个界面,或者集成 LeakCanary 以后在桌面会有一个 LeakCanary 的图标,点击进去是全部的内存泄漏列表,点击其中一项一样是进入到下面界面:
数据库

这里写图片描述

这个界面就会详细展现引用持有链,一目了然,对于问题的解决方便了不少,堪称神器,更多实用姿式能够看看 LeakCanary FAQ
  还有一点须要提到的是,LeakCanary 在检测内存泄漏的时候会阻塞主界面,这是一点体验有点不爽的地方,可是这时候阻塞确定是必要的,由于此时必需要挂起线程来获取当前堆的状态。而后也并非每一个 LeakCanary 提示的地方都有内存泄漏,这时候可能须要借助 MAT 等工具去具体分析。不过 LeakCanary 有一点很是好的地方是由于 Android 系统也会有一些内存泄漏,而 LeakCanary 对此则提供了一个 AndroidExcludedRefs 类来帮助咱们排除这些问题。

Android Memory Monitor

  Memory Monitor 是 Android Studio 自带的一个监控内存使用状态的工具,入口以下所示:

这里写图片描述

在 Android Monitor 点开以后 logcat 的右侧就是 Monitor 工具,其中能够检测内存、CPU、网络等内容,咱们这里只用到了 Memory Monitor 功能,点击红色箭头所指的区域,就会 dump 此时此刻的 Memory 信息,而且生成一个 .hprof 文件,dump 完成以后会自动打开这个文件的显示界面,若是没有打开,能够经过点击最左侧的 Capture 界面或者 Tool Window 里面的 Capture 进入 dump 的 .hprof 文件列表:
这里写图片描述

  接着咱们来分析一下这个生成的 .hprof 文件所展现的信息:
这里写图片描述

首先左上角的下拉框,能够选择 App Heap、Image Heap 和 Zygote Heap,对应的就是上篇博客讲到的 Allocation Space,Image Space 和 Zygote Space,咱们这里选择 Allocation Space,而后第二个选择 PackageTreeView 这一项,展开以后就能看见一个树形结构了,而后继续展开咱们应用包名的对应对象,就能够很清晰的看到有多少个 Activity 对象了,上面那两栏展现的信息按照从左到右的顺序,定义以下所示:

Column Description
Class Name 占有这块内存的类名
Total Count 未被处理的数量
Heap Count 在上面选择的指定 heap 中的数量
Sizeof 这个对象的大小,若是在变化中,就显示 0
Shallow Size 在当前这个 heap 中的全部该对象的总数
Retained Size 这个类的全部对象占有的总内存大小
Instance 这个类的指定对象
Reference Tree 指向这个选中对象的引用,还有指向这个引用的引用
Depth 从 GC Root 到该对象的引用链路的最短步数
Shallow Size 这个引用的大小
Dominating Size 这个引用占有的内存大小

而后能够点击展开右侧的 Analyzer Tasks 项,勾选上须要检测的任务,而后系统就会给你分析出结果:

这里写图片描述

从分析的结果能够看到泄漏的 Activity 有两个,很是直观,而后点开其中一个,观察下面的 ReferenceTree 选项:
这里写图片描述

能够看到 Thread 对象持有了 SecondActivity 对象的引用,也就是 GC Root 持有了该 Activity 的引用,致使这个 Activity 没法回收,问题的根源咱们就发现了,接下来去处理它就行了。
  关于更多 Android Memory Monitor 的使用能够去看看这个官方文档: HPROF Viewer and Analyzer

MAT

  MAT(Memory Analyzer Tools)是一个 Eclipse 插件,它是一个快速、功能丰富的 JAVA heap 分析工具,它能够帮助咱们查找内存泄漏和减小内存消耗,MAT 插件的下载地址:Eclipse Memory Analyzer Open Source Project,上面经过 Android studio 生成的 .hprof 文件由于格式稍有不一样,因此须要通过一个简单的转换,而后就能够经过 MAT 去打开了:

这里写图片描述

经过 MAT 去打开转换以后的这个文件:
这里写图片描述

用的最多的就是 Histogram 功能,点击 Actions 下的 Histogram 项就能够获得 Histogram 结果:
这里写图片描述

咱们能够在左上角写入一个正则表达式,而后就能够对全部的 Class Name 进行筛选了,很方便,顶栏展现的信息 "Objects" 表明该类名对象的数量,剩下的 "Shallow Heap" 和 "Retained Heap" 则和 Android Memory Monitor 相似。我们接着点击 SecondActivity,而后右键:
这里写图片描述

在弹出来的菜单中选择 List objects->with incoming references 将该类的实例所有列出来:
这里写图片描述

经过这个列表咱们能够看到 SecondActivity@0x12faa900 这个对象被一个 this$00x12c65140 的匿名内部类对象持有,而后展开这一项,发现这个对象是一个 handler 对象:
这里写图片描述

快速定位找到这个对象没有被释放的缘由,能够右键 Path to GC Roots->exclude all phantom/weak/soft etc. references 来显示出这个对象到 GC Root 的引用链,由于强引用才会致使对象没法释放,因此这里咱们要排除其余三种引用:
这里写图片描述

这么处理以后的结果就很明显了:
这里写图片描述

一个很是明显的强引用持有链,GC Root 咱们前面的博客中说到包含了线程,因此这里的 Thread 对象 GC Root 持有了 SecondActivity 的引用,致使该 Activity 没法被释放。
  MAT 还有一个功能就是可以对比两个 .hprof 文件,将两个文件都添加到 Compare Basket 里面:
这里写图片描述

添加进去以后点击右上角的 ! 按钮,而后就会生成两个文件的对比:
这里写图片描述

一样适用正则表达式将须要的类筛选出来:
这里写图片描述

结果也很明显,退出 Activity 以后该 Activity 对象未被回收,仍然在内存中,或者能够调整对比选项让对比结果更加明显:
这里写图片描述

也能够对比两个对象集合,方法与此相似,都是将两个 Dump 结果中的对象集合添加到 Compare Basket 中去对比,找出差别后用 Histogram 查询的方法找出 GC Root,定位到具体的某个对象上。

adb shell && Memory Usage

  能够经过命令 adb shell dumpsys meminfo [package name] 来将指定 package name 的内存信息打印出来,这种模式能够很是直观地看到 Activity 未释放致使的内存泄漏:

这里写图片描述

或者也能够经过 Android studio 的 Memory Usage 功能进行查看,最后的结果是同样的:
这里写图片描述

Allocation Tracker

  Android studio 还自带一个 Allocation Tracker 工具,功能和 DDMS 中的基本差很少,这个工具能够监控一段时间以内的内存分配:

这里写图片描述

在内存图中点击途中标红的部分,启动追踪,再次点击就是中止追踪,随后自动生成一个 .alloc 文件,这个文件就记录了此次追踪到的全部数据,而后会在右上角打开一个数据面板:
这里写图片描述

这个工具详细的介绍能够看看这个博客: Android性能专项测试之Allocation Tracker(Android Studio)

常见的内存泄漏案例

  咱们来看看常见的致使内存泄漏的案例:

静态变量形成的内存泄漏

  因为静态变量的生命周期和应用同样长,因此若是静态变量持有 Activity 或者 Activity 中 View 对象的应用,就会致使该静态变量一直直接或者间接持有 Activity 的引用,致使该 Activity 没法释放,从而引起内存泄漏,不过须要注意的是在大多数这种状况下因为静态变量只是持有了一个 Activity 的引用,因此致使的结果只是一个 Activity 对象未能在退出以后释放,这种问题通常不会致使 OOM 问题,只能经过上面介绍过的几种工具在开发中去观察发现。
  这种问题的解决思路很简单,就是不让静态变量直接或者间接持有 Activity 的强引用,能够将其修改成 soft reference 或者 weak reference 等等之类的,或者若是能够的话将 Activity Context 更换为 Application Context,这样就能保证生命周期一致不会致使内存泄漏的问题了。

内部类持有外部类引用

  咱们上面的 demo 中模拟的就是内部类对象持有外部类对象的引用致使外部类对象没法释放的问题,在 Java 中非静态内部类和匿名内部类会持有他们所属外部类对象的引用,若是这个非静态内部类对象或者匿名内部类对象被一个耗时的线程(或者其余 GC Root)直接或者间接的引用,甚至这些内部类对象自己就在作一些耗时操做,这样就会致使这个内部类对象直接或者间接没法释放,内部类对象没法释放,外部类的对象也就没法释放形成内存泄漏,并且若是没法释放的对象积累起来就会形成 OOM,示例代码以下所示:

public class SecondActivity extends AppCompatActivity{
    private Handler handler;
    private Bitmap bitmap;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.pic);//decode 一个大图来模拟内存没法释放致使的崩溃
        findViewById(R.id.btn_second).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                finish();
            }
        });

        handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);

            }
        };
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                handler.sendEmptyMessage(0);
            }
        }).start();
    }
}复制代码

  这个问题的解决方法能够根据实际状况进行选择:

  • 将非静态内部类或者匿名内部类修改成静态内部类,好比 Handler 修改成静态内部类,而后让 Handler 持有外部 Activity 的一个 Weak Reference 或者 Soft Reference;
  • 在 Activity 页面销毁的时候将耗时任务中止,这样就能保证 GC Root 不会间接持有 Activity 的引用,也就不会致使内存泄漏;

错误使用 Activity Context

  这个很好理解,在一个错误的地方使用 Activity Context,形成 Activity Context 被静态变量长时间引用致使没法释放而引起的内存泄漏,这个问题的处理方式也很简单,若是能够的话修改成 Application Context 或者将强引用变成其余引用。

资源对象没关闭形成的内存泄漏

  资源性对象好比(Cursor,File 文件等)每每都用了一些缓冲,咱们在不使用的时候应该及时关闭它们,以便它们的缓冲对象被及时回收,这些缓冲不只存在于 java 虚拟机内,还存在于 java 虚拟机外,若是咱们仅仅是把它的引用设置为 null 而不关闭它们,每每会形成内存泄漏。可是有些资源性对象,好比 SQLiteCursor(在析构函数 finalize(),若是咱们没有关闭它,它本身会调 close() 关闭),若是咱们没有关闭它系统在回收它时也会关闭它,可是这样的效率过低了。所以对于资源性对象在不使用的时候,应该调用它的 close() 函数,将其关闭掉,而后再置为 null,在咱们的程序退出时必定要确保咱们的资源性对象已经关闭。
  程序中常常会进行查询数据库的操做,可是常常会有使用完毕 Cursor 后没有关闭的状况,若是咱们的查询结果集比较小,对内存的消耗不容易被发现,只有在常时间大量操做的状况下才会出现内存问题,这样就会给之后的测试和问题排查带来困难和风险,示例代码:

Cursor cursor = getContentResolver().query(uri...); 
if (cursor.moveToNext()) { 
... ... 
}复制代码

更正代码:

Cursor cursor = null;
try {
    cursor = getContentResolver().query(uri...);
    if (cursor != null && cursor.moveToNext()) {
        ... ...
    }
} finally {
    if (cursor != null) {
        try {
            cursor.close();
        } catch (Exception e) {
            //ignore this
        }
    }
}复制代码

集合中对象没清理形成的内存泄漏

  在实际开发过程当中不免会有把对象添加到集合容器(好比 ArrayList)中的需求,若是在一个对象使用结束以后未将该对象从该容器中移除掉,就会形成该对象不能被正确回收,从而形成内存泄漏,解决办法固然就是在使用完以后将该对象从容器中移除。

WebView形成的内存泄露

  具体的能够看看个人这篇博客:android WebView详解,常见漏洞详解和安全源码(下)

未取消注册致使的内存泄漏

  一些 Android 程序可能引用咱们的 Android 程序的对象(好比注册机制),即便咱们的 Android 程序已经结束了,可是别的应用程序仍然还持有对咱们 Android 程序某个对象的引用,这样也会形成内存不能被回收,好比调用 registerReceiver 后未调用unregisterReceiver。假设咱们但愿在锁屏界面(LockScreen)中,监听系统中的电话服务以获取一些信息,则能够在 LockScreen 中定义一个 PhoneStateListener 的对象,同时将它注册到 TelephonyManager 服务中,对于 LockScreen 对象,当须要显示锁屏界面的时候就会建立一个 LockScreen 对象,而当锁屏界面消失的时候 LockScreen 对象就会被释放掉,可是若是在释放 LockScreen 对象的时候忘记取消咱们以前注册的 PhoneStateListener 对象,则会间接致使 LockScreen 没法被回收,若是不断的使锁屏界面显示和消失,则最终会因为大量的 LockScreen 对象没有办法被回收而引发 OOM,虽然有些系统程序自己好像是能够自动取消注册的(固然不及时),可是咱们仍是应该在程序结束时明确的取消注册。

由于内存碎片致使分配内存不足

  还有一种状况是由于频繁的内存分配和释放,致使内存区域里面存在不少碎片,当这些碎片足够多,new 一个大对象的时候,全部的碎片中没有一个碎片足够大以分配给这个对象,可是全部的碎片空间加起来又是足够的时候,就会出现 OOM,并且这种 OOM 从某种意义上讲,是彻底可以避免的。
  因为产生内存碎片的场景不少,从 Memory Monitor 来看,下面场景的内存抖动是很容易产生内存碎片的:

这里写图片描述

最多见产生内存抖动的例子就是在 ListView 的 getView 方法中未复用 convertView 致使 View 的频繁建立和释放,针对这个问题的处理方式那固然就是复用 convertView;或者是 String 拼接建立大量小的对象(好比在一些频繁调用的地方打字符串拼接的 log 的时候);若是是其余的问题,就须要经过 Memory Monitor 去观察内存的实时分配释放状况,找到内存抖动的地方修复它,或者若是当出现下面这种状况下的 OOM 时,也是因为内存碎片致使没法分配内存:
这里写图片描述

出现上面这种类型的 Crash 时就要去分析应用里面是否是存在大量分配释放对象的地方了。

Android 内存优化

  内存优化请看下篇:Android 性能优化以内存泄漏检测以及内存优化(下)

引用

blog.csdn.net/luoshengyan…
blog.csdn.net/luoshengyan…
blog.csdn.net/luoshengyan…
blog.csdn.net/luoshengyan…
blog.csdn.net/luoshengyan…
mp.weixin.qq.com/s?__biz=MzA…
geek.csdn.net/news/detail…
www.jianshu.com/p/216b03c22…
zhuanlan.zhihu.com/p/25213586
joyrun.github.io/2016/08/08/…
www.cnblogs.com/larack/p/60…
source.android.com/devices/tec…
blog.csdn.net/high2011/ar…
gityuan.com/2015/10/03/…
www.ayqy.net/blog/androi…
developer.android.com/studio/prof…
zhuanlan.zhihu.com/p/26043999

相关文章
相关标签/搜索