文档章节

【转载】专项:Android 内存泄露实践分析

beijing_zbs
 beijing_zbs
发布于 2016/09/18 15:49
字数 1864
阅读 22
收藏 0

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

定义

​ 内存泄漏也称作“存储渗漏”,用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。(其实说白了就是该内存空间使用完毕之后未回收)即所谓内存泄漏。

 

内存泄漏形象的比喻是“操作系统可提供给所有进程的存储空间正在被某个进程榨干”,最终结果是程序运行时间越长,占用存储空间越来越多,最终用尽全部存储空间,整个系统崩溃。所以“内存泄漏”是从操作系统的角度来看的。这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。由程序申请的一块内存,如果没有任何一个指针指向它,那么这块内存就泄漏了。

 

​ ——来自《百度百科》

影响

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

成效

  • 内存泄露是一个持续的过程,随着版本的迭代,效果越明显
  • 由于某些原因无法改善的泄露(如框架限制),则尽量降低泄露的内存大小
  • 内存泄露实施后的版本,一定要验证,不必马上推行到正式版,可作为beta版持续观察是否影响/引发其他功能/问题

内存泄露实施后,项目的收获:

  • OOM减少30%以上
  • 平均使用内存从80M稳定到40M左右
  • 用户体验上升,流畅度提升
  • 存活率上升,推送到达率提升

类型

  • IO
    • FileStream
    • Cursor
  • Bitmap
  • Context

    • 单例
    • Callback
  • Service

    • BraodcastReceiver
    • ContentObserver
  • Handler

  • Thread

技巧

  • 慎用Context

    • Context概念
    • 四大组件Context和Application的context使用参见下表

 

  • 善用Reference

    • Java引用介绍
    • Java四种引用由高到低依次为:强引用  >  软引用  >  弱引用  >  虚引用
    • 表格说明
    类型 垃圾回收时间 生存时间
    强引用 永远不会 JVM停止运行时终止
    软引用 内存不足时 内存不足时终止
    弱引用 垃圾回收时 垃圾回收时终止
    虚引用 垃圾回收时 垃圾回收时终止
  • 复用ConvertView

  • 对象释放

    • 遵循谁创建谁释放的原则
    • 示例:显示调用clear列表、对象赋空值

分析

​ 原理

​ 根本原因

  • 关注堆内存

​ 怎么解决

  • 详见方案

​ 实践分析

  • 详见实践

方案

  • StrictMode

    • 使用方法:AppContext的onCreate()方法加上
    StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy
                        .Builder()
                        .detectAll()
                        .penaltyLog()
                        .build());
    StrictMode.setVmPolicy(new StrictMode.VmPolicy
                        .Builder()
                        .detectAll()
                        .penaltyLog()
                        .build());
    • 主要检查项:内存泄露、耗时操作等
  • Leakcanary

  • 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

  • MAT

实践(示例)

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();
    }
  • 解决方案:

    • 改为弱引用
    • pass:弱引用随时可能为空,使用前先判空
    • 示例代码:
      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();
        }

 

Context泄露

Callback泄露

服务未解绑注册泄露

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

  • 引用代码:

  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;
    }
  }

 

后面

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

本文转载自:https://testerhome.com/topics/5822

beijing_zbs
粉丝 7
博文 150
码字总数 31629
作品 0
塘沽
QA/测试工程师
私信 提问
Android内存泄露与内存溢出

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zping0808/article/details/53889115 一、 内存泄漏与内存溢出(OOM) 1. 内存泄露 垃圾回收器无法回收原本应...

_zping
2016/12/26
0
0
Android性能优化:手把手带你全面实现内存优化

前言 在 Android开发中,性能优化策略十分重要 本文主要讲解性能优化中的内存优化,希望你们会喜欢 目录 1. 定义 优化处理 应用程序的内存使用、空间占用 2. 作用 避免因不正确使用内存 & 缺...

codeGoogle
2018/05/08
0
0
内存泄露和LeakCanary的故事

新鲜文章请关注微信公众号: JueCode 今天我们来聊一聊Android中的内存泄露和内存泄露的检测工具LeakCanary。Java有垃圾回收线程为什么还会发生内存泄露?原因就是外部人为持有对象引用,持有...

juexingzhe
2018/05/22
0
0
一步步拆解 LeakCanary

本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 java 源码系列 - 带你读懂 Reference 和 ReferenceQueue https://blog.csdn.net/gdutxiaoxu/article/details/80738581 一步步拆解 ...

xujun9411
2018/07/04
0
0
Android性能优化:关于 内存泄露 的知识都在这里了!

前言 在中,内存泄露的现象十分常见;而内存泄露导致的后果会使得应用 本文 全面介绍了内存泄露的本质、原因 & 解决方案,最终提供一些常见的内存泄露分析工具,希望你们会喜欢。 目录 } Li...

Carson_Ho
2018/04/19
0
0

没有更多内容

加载失败,请刷新页面

加载更多

thinkcmf 渗透测试漏洞修复解决方案

近段时间发现很多APP程序用的是thinkcmf,此程序源码存在getshell漏洞,我们Sine安全紧急对此高危漏洞进行了分析和漏洞修复,攻击者可以通过构造特定的请求包get请求即可在远程服务器上执行任意...

网站安全
30分钟前
6
0
MySQL的IP地址与数字互转原理

一、inet_aton与inet_ntoa inet_aton是把ip地址转为数字的函数,记忆小技巧,inet表示网络相关,在c语言中a习惯性代表字符串,to就是转换的,n代表数字,aton就是字符串转数字,同理inet_nt...

trayvon
41分钟前
6
0
【翻译】全新16英寸MacBook Pro评测:开发人员的梦想成真

要问现在适合开发者用的笔记本,市面上还是有很多选择的,比如Dell的XPS系列,外星人系列(游戏也是杠杠滴),联想拯救者系列,还有形形色色的高配机型,价格也从几千到几万不等。 但是,笔吧...

Dimple91
42分钟前
8
0
IT兄弟连 HTML5教程 CSS3属性特效 CSS3分栏布局

CSS3中新出现的多列布局(multi-column)是传统HTML网页中块状布局模式的有力扩充。这种新语法能够让WEB开发人员轻松的让文本呈现多列显示。我们知道,当一行文字太长时,读者读起来就比较费劲...

老码农的一亩三分地
59分钟前
11
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部