专项:Android 内存泄露实践分析

定义

内存泄漏也称作“存储渗漏”,用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。(其实说白了就是该内存空间使用完毕之后未回收)即所谓内存泄漏。
 
内存泄漏形象的比喻是“操作系统可提供给所有进程的存储空间正在被某个进程榨干”,最终结果是程序运行时间越长,占用存储空间越来越多,最终用尽全部存储空间,整个系统崩溃。所以“内存泄漏”是从操作系统的角度来看的。这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。由程序申请的一块内存,如果没有任何一个指针指向它,那么这块内存就泄漏了。
 
——来自《百度百科》

影响

  • 导致OOM
  • 糟糕的用户体验
  • 鸡肋的App存活率

成效

  • 内存泄露是一个持续的过程,随着版本的迭代,效果越明显
  • 由于某些原因无法改善的泄露(如框架限制),则尽量降低泄露的内存大小
  • 内存泄露实施后的版本,一定要验证,不必马上推行到正式版,可作为beta版持续观察是否影响/引发其他功能/问题
内存泄露实施后,项目的收获:
  • OOM减少30%以上
  • 平均使用内存从80M稳定到40M左右
  • 用户体验上升,流畅度提升
  • 存活率上升,推送到达率提升

类型

  • IO  

    • FileStream
      Cursor
  • Bitmap
  • Context
    • 单例
    • Callback
  • Service
    • BraodcastReceiver
    • ContentObserver
  • Handler
  • Thread

技巧

类型 垃圾回收时间 生存时间
强引用 永远不会 JVM停止运行时终止
软引用 内存不足时 内存不足时终止
弱引用 垃圾回收时 垃圾回收时终止
虚引用 垃圾回收时 垃圾回收时终止
Java引用介绍(http://blog.csdn.net/mazhimazh/article/details/19752475)
Java四种引用由高到低依次为:强引用  >  软引用  >  弱引用  >  虚引用
表格说明

分析

 

原理

根本原因

  • 关注堆内存

怎么解决

  • 详见方案

 实践分析

  • 详见实践

方案

  • StrictMode
  • StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy
  •                    .Builder()
  •                    .detectAll()
  •                    .penaltyLog()
  •                    .build());StrictMode.setVmPolicy(new StrictMode.VmPolicy
  •                    .Builder()
  •                    .detectAll()
  •                    .penaltyLog()
                       .build());

    • 主要检查项:内存泄露、耗时操作等
      使用方法:AppContext的onCreate()方法加上
  • Leakcanary
    GitHub地址(https://github.com/square/leakcanary)
    使用方法(http://www.liaohuqiu.net/cn/posts/leak-canary-read-me/)
  • Leakcanary + StrictMode + monkey (推荐)
    使用阶段:功能测试完成后,稳定性测试开始时
    使用方法:安装集成了Leakcanary的包,跑monkey
    收获阶段:一段时间后,会发现出现N个泄露
    实战分析:逐条分析每个泄露并改善/修复
    StrictMode:查看日志搜索StrictMode关键字
  • Adb命令
    手动触发GC
    通过adb shell dumpsys meminfo packagename -d查看
    查看Activity以及View的数量
    越接近0越好
    对比进入Activity以及View前的数量和退出Activity以及View后的数量判断
  • Android Monitor
    使用介绍(http://wetest.qq.com/lab/view/?id=99)
  • MAT
    使用介绍(http://blog.csdn.net/xiaanming/article/details/42396507)

实践(示例)

Bitmap泄露

Bitmap泄露一般会泄露较多内存,视图片大小、位图而定

  • 经典场景:App启动图
  • 解决内存泄露前后内存相差10M+,可谓惊人
  • 解决方案:
    App启动图Activity的onDestroy()中及时回收内存

@Override
  protected void onDestroy() {
      // TODO Auto-generated method stub
      super.onDestroy();
      recycleImageView(imgv_load_ad);
      }

  public static void recycleImageView(View view){
          if(view==null) return;
          if(view instanceof ImageView){
              Drawable drawable=((ImageView) view).getDrawable();
              if(drawable instanceof BitmapDrawable){
                  Bitmap bmp = ((BitmapDrawable)drawable).getBitmap();
                  if (bmp != null && !bmp.isRecycled()){
                      ((ImageView) view).setImageBitmap(null);
                      bmp.recycle();
                      bmp=null;
                  }
              }
          }
      }

IO流未关闭

  • 分析:通过日志可知FileOutputStream()未关闭
  • 问题代码:
     
    public static void copyFile(File source, File dest) {
             FileChannel inChannel = null;
             FileChannel outChannel = null;
             Log.i(TAG, "source path: " + source.getAbsolutePath());
             Log.i(TAG, "dest path: " + dest.getAbsolutePath());
             try {
                 inChannel = new FileInputStream(source).getChannel();
                 outChannel = new FileOutputStream(dest).getChannel();
                 inChannel.transferTo(0, inChannel.size(), outChannel);
             } catch (IOException e) {
                 e.printStackTrace();
             }
         }
  • 解决方案:

    • 及时关闭IO流,避免泄露
public static void copyFile(File source, File dest) {
          FileChannel inChannel = null;
          FileChannel outChannel = null;
          Log.i(TAG, "source path: " + source.getAbsolutePath());
          Log.i(TAG, "dest path: " + dest.getAbsolutePath());
          try {
              inChannel = new FileInputStream(source).getChannel();
              outChannel = new FileOutputStream(dest).getChannel();
              inChannel.transferTo(0, inChannel.size(), outChannel);
          } catch (IOException e) {
              e.printStackTrace();
          } finally {
              if (inChannel != null) {
                  try {
                      inChannel.close();
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
              if (outChannel != null) {
                  try {
                      outChannel.close();
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
          }
      }
E/StrictMode: A resource was acquired at attached stack trace but never released.
See java.io.Closeable for information on avoiding resource leaks.
java.lang.Throwable: Explicit termination method 'close' not called
    at dalvik.system.CloseGuard.open(CloseGuard.java:180)
    at java.io.FileOutputStream.<init>(FileOutputStream.java:89)
    at java.io.FileOutputStream.<init>(FileOutputStream.java:72)
    at com.heyniu.lock.utils.FileUtil.copyFile(FileUtil.java:44)
    at com.heyniu.lock.db.BackupData.backupData(BackupData.java:89)
    at com.heyniu.lock.ui.HomeActivity$11.onClick(HomeActivity.java:675)
    at android.support.v7.app.AlertController$ButtonHandler.handleMessage(AlertController.java:157)
    at android.os.Handler.dispatchMessage(Handler.java:102)
    at android.os.Looper.loop(Looper.java:148)
    at android.app.ActivityThread.main(ActivityThread.java:5417)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)

单例模式泄露

  • 分析:通过截图我们发现SplashActivity被ActivityUtil的实例activityStack持有
  • 引用代码:
     ActivityUtil.getAppManager().add(this);
  • 持有代码:

     public void add(Activity activity) {
           if (activityStack == null) {
               synchronized (ActivityUtil.class){
                   if (activityStack == null) {
                       activityStack = new Stack<>();
                   }
               }
           }
           activityStack.add(activity);
       }
  • 解决方案:
  • 在SplashActivity的onDestroy()生命周期移除引用
     
    @Override
         protected void onDestroy() {
             super.onDestroy();
             ActivityUtil.getAppManager().remove(this);

     

     

静态变量持有Context实例泄露

  • 分析:长生命周期持有短什么周期引用导致泄露,详见上文四大组件Context和Application的context使用
  • 示例引用代码:

     private static HttpRequest req;
     public static void HttpUtilPost(Context context, int TaskId, String url, String requestBody,ArrayList<HttpHeader> Headers, RequestListener listener) {
           // TODO Auto-generated constructor stub
           req = new HttpRequest(context, url, TaskId, requestBody, Headers, listener);
           req.post();
       }
  • 解决方案:
-  public static void cancel(int TaskId) {
-         if(req != null && req.get() != null){
-           req.get().AsyncCancel(TaskId);
 -       }
    }
private static WeakReference<HttpRequest> req;public static void HttpUtilPost(Context context, int TaskId, String url, String requestBody,ArrayList<HttpHeader> Headers, RequestListener listener) {
        // TODO Auto-generated constructor stub
        req = new WeakReference<HttpRequest>(new HttpRequest(context, url, TaskId, requestBody, Headers, listener));
        req.get().post();
    }
private static HttpRequest req;public static void HttpUtilPost(Context context, int TaskId, String url, String requestBody,ArrayList<HttpHeader> Headers, RequestListener listener) {
        // TODO Auto-generated constructor stub
        req = new HttpRequest(context.getApplicationContext(), url, TaskId, requestBody, Headers, listener);
        req.post();
    }
  • 改为长生命周期
  • 改为弱引用
  • pass:弱引用随时可能为空,使用前先判空
  • 示例代码:

服务未解绑注册泄露

  • 分析:一般发生在注册了某服务,不用时未解绑服务导致泄露
  • 引用代码:

     private void initSensor() {
             // 获取传感器管理器
             sm = (SensorManager) container.activity.getSystemService(Context.SENSOR_SERVICE);
             // 获取距离传感器
             acceleromererSensor = sm.getDefaultSensor(Sensor.TYPE_PROXIMITY);
             // 设置传感器监听器
             acceleromererListener = new SensorEventListener() {
             ......
             };
             sm.registerListener(acceleromererListener, acceleromererSensor, SensorManager.SENSOR_DELAY_NORMAL);
         }
  • 解决方案:
    在Activity的onDestroy()方法解绑服务
     
    @Override
     protected void onDestroy() {
       super.onDestroy();
       sm.unregisterListener(acceleromererListener,acceleromererSensor);
     }

Handler泄露

  • 分析:由于Activity已经关闭,Handler任务还未执行完成,其引用了Activity的实例导致内存泄露
  • 引用代码:

     handler.sendEmptyMessage(0);
  • 解决方案:
    在Activity的onDestroy()方法回收Handler

     @Override
     protected void onDestroy() {
       super.onDestroy();
       handler.removeCallbacksAndMessages(null);
     }
  • 图片后续遇到再补上

异步线程泄露

  • 分析:一般发生在线程执行耗时操作时,如下载,此时Activity关闭后,由于其被异步线程引用,导致无法被正常回收,从而内存泄露
  • 引用代码:

     new Thread() {
       public void run() {
         imageArray = loadImageFromUrl(imageUrl);
       }.start();
  • 解决方案:
    把线程作为对象提取出来
    在Activity的onDestroy()方法阻塞线程

     thread = new Thread() {
       public void run() {
         imageArray = loadImageFromUrl(imageUrl);
       };
     thread.start();
    @Override
     protected void onDestroy() {
       super.onDestroy();
       if(thread != null){
         thread.interrupt();
         thread = null;
       }
     }

后面

  • 欢迎补充实际中遇到的泄露类型
  • 文章如有错误,欢迎指正
  • 如有更好的内存泄露分享方法,欢迎一起讨论

未完待续。。。
阿里云测移动质量中心(以下简称MQC)是为广大企业客户和移动开发者提供真机测试服务的云平台,拥有大量热门机型,提供7x24全天候服务。 我们致力于提供专业、稳定、全面、高价值的自动化测试能力,以及简单易用的使用流程、贴心的技术服务,并且帮助客户以最低的成本、最高的效率发现APP中的各类隐患(APP崩溃、各类兼容性问题、功能性问题、性能问题等),减少用户流失,提高APP质量和市场竞争力。

联系我们:
 网站地址:https://mqc.aliyun.com
 开发者交流旺旺群:335334143
 开发者交流QQ群:492028798
 客服邮箱:mqc_group@service.alibaba.com
更多精彩技术分享 欢迎关注 MQC公众号  

时间: 2024-08-30 10:48:54

专项:Android 内存泄露实践分析的相关文章

(转)专项:Android 内存泄露实践分析

今天看到一篇关于Android 内存泄露实践分析的文章,感觉不错,讲的还算详细,mark到这里. 原文发表于:Testerhome: 作者:ycwdaaaa ;  原文链接:https://testerhome.com/topics/5822 定义 ​内存泄漏也称作"存储渗漏",用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元.直到程序结束.(其实说白了就是该内存空间使用完毕之后未回收)即所谓内存泄漏.  内存泄漏形象的比喻是"操作系统可提供给

java程序内存不下降,但是也没有内存泄露怎么分析?

问题描述 java程序内存不下降,但是也没有内存泄露怎么分析? 我们用java开发的程序.运行过程中使用任务管理器观察内存可以达到700M.使用java自带的jvisualvm.exe查看内存申请了200多用了100M,请问剩下的500M去哪里了.用什么方法可以找到这些内存的去向.

android内存泄露分析工具MAT详解

一.准备 1.什么是MAT Eclipse提供的一个内存分析工具.它是一个功能丰富的 JAVA 堆转储文件分析工具,可以帮助你发现内存漏洞和减少内存消耗. android studio未集成该插件 需要你下载独立版 android studio的DDMS可以生成hprof是什么文件,不过需要进行一下格式转化(.hprof文件从Dalvik格式转换成J2SE HPROF格式),才可以导入MAT独立版软件. 2.hprof是什么文件 heap dumps,中文翻译,堆转储,快照.即堆内存某个时刻的情

Android内存泄露常见问题总结

 在介绍内存泄漏之前很有必要提及一下Android系统的垃圾回收机制.Java GC(Garbage Collection,垃圾收集,垃圾回收)机制,是Java与C++/C的主要区别之一,作为Java开发者,不需要专门编写内存回收和垃圾清理代码,对内存泄露和溢出的问题,也不需要像C程序员那样战战兢兢.这是因为在Java虚拟机中,存在自动内存管理和垃圾清扫机制.概括地说,该机制对虚拟机中的内存进行标记,并确定哪些内存需要回收,根据一定的回收策略,自动的回收内存,永不停息(Nerver Stop)的

Android内存泄露总结(附内存检测工具)

Java 中的内存分配 主要是分三块: 静态储存区:编译时就分配好,在程序整个运行期间都存在.它主要存放静态数据和常量. 栈区:当方法执行时,会在栈区内存中创建方法体内部的局部变量,方法结束后自动释放内存. 堆区:通常存放 new 出来的对象.由 Java 垃圾回收器回收. 栈与堆的区别 栈内存用来存放局部变量和函数参数等.它是先进后出的队列,进出一一对应,不产生碎片,运行效率稳定高.当超过变量的作用域后,该变量也就无效了,分配给它的内存空间也将被释放掉,该内存空间可以被重新使用. 堆内存用于存

ThreadLocal是否会引发内存泄露的分析 good

这篇文章,主要解决一下疑惑: 1. ThreadLocal.ThreadLocalMap中提到的弱引用,弱引用究竟会不会被回收? 2. 弱引用什么情况下回收? 3. JAVA的ThreadLocal和在什么情况下会内存泄露?   带着这些疑问,自己模拟了一下ThreadLocal.ThreadLocalMap的结构,先展示下自己涉及的结构: 自己实现一个simple的ThreadLocalMap,里面用一个entry用来存放由自己模拟的ThreadLocal调用set方法set进去的值. 并且和

Flex应用内存泄露的分析与诊断

引言 Flex 采用 ActionScript 语言作为脚本语言,编译后的二进制代码在 FlashPlayer 虚拟机 AVM(Actionscript Virtual Machine)中运行.和 Java 语言类似, AVM 中也有一个垃圾收集器(Garbage Collection),对于不用的对象,隔一段时间会进行 收集并销毁,释放内存.和 C++ 语言相比,程序员不需要时刻关注内存的分配和释放,大大 减轻了负担.但是垃圾收集器不能从根本上解决 Flex 内存泄露的问题,这得从 Flash

Java内存泄露问题分析

很多人在谈论内存泄露问题,当然对于c/c++来说,这个应该是老掉牙的问题,但是很多Java人员也越来越多得讨论这个问题,我这里写个小结,希望对大家有一定的参考价值. 内存泄漏的慨念 1.c/c++是程序员自己管理内存,Java内存是由GC自动回收的. 我虽然不是很熟悉C++,不过这个应该没有犯常识性错误吧. 2.什么是内存泄露? 内存泄露是指系统中存在无法回收的内存,有时候会造成内存不足或系统崩溃. 在C/C++中分配了内存不释放的情况就是内存泄露. 3.Java存在内存泄露 我们必须先承认这个

Android 内存溢出问题分析。

 Android虽然会自动管理内存,JAVA也有garbage collection (GC )内存回收机制. 但是如果程序在一次操作中打开几个M的文件,那么通常会出现下面的错误信息.   02-04 21:46:08.703: ERROR/dalvikvm-heap(2429): 1920000-byte external allocation too large for this process.或02-04 21:52:28.463: ERROR/AndroidRuntime(2429):