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

简介: 内存泄漏也称作“存储渗漏”,用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。(其实说白了就是该内存空间使用完毕之后未回收)即所谓内存泄漏。   内存泄漏形象的比喻是“操作系统可提供给所有进程的存储空间正在被某个进程榨干”,最终结果是程序运行时间越长,占用存储空间越来越多,最终用尽全部存储空间,整个系统崩溃。

定义

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

 

——来自《百度百科》

影响

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

成效

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

类型

  • IO  

    • FileStream
      Cursor
  • Bitmap
  • Context

    • 单例
    • Callback
  • Service

    • BraodcastReceiver
    • ContentObserver
  • Handler
  • Thread

技巧

类型 垃圾回收时间 生存时间
强引用 永远不会 JVM停止运行时终止
软引用 内存不足时 内存不足时终止
弱引用 垃圾回收时 垃圾回收时终止
虚引用 垃圾回收时 垃圾回收时终止

分析

 

原理

根本原因

  • 关注堆内存

怎么解决

  • 详见方案

 实践分析

  • 详见实践

方案

  • 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.(FileOutputStream.java:89)
   at java.io.FileOutputStream.(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);
 }    
 ```   
 ![image](https://yqfile.alicdn.com/1998bcf9cd08cdac7d473153af1a2618ef7acd4c.png)

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

 private static HttpRequest req;
 public static void HttpUtilPost(Context context, int TaskId, String url, String requestBody,ArrayList 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 req;public static void HttpUtilPost(Context context, int TaskId, String url, String requestBody,ArrayList Headers, RequestListener listener) {
       // TODO Auto-generated constructor stub
       req = new WeakReference(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 Headers, RequestListener listener) {
       // TODO Auto-generated constructor stub
       req = new HttpRequest(context.getApplicationContext(), url, TaskId, requestBody, Headers, listener);
       req.post();
   }

- 改为长生命周期
- 改为弱引用
- pass:弱引用随时可能为空,使用前先判空
- 示例代码:  

![image](https://yqfile.alicdn.com/5a2b696c2306c1c4fe5916cce33a6b62feea4687.png)  

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

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

image

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公众号  
17

目录
相关文章
|
24天前
|
搜索推荐 Android开发 iOS开发
安卓与iOS系统的用户界面设计对比分析
本文通过对安卓和iOS两大操作系统的用户界面设计进行对比分析,探讨它们在设计理念、交互方式、视觉风格等方面的差异及各自特点,旨在帮助读者更好地理解和评估不同系统的用户体验。
18 1
|
25天前
|
编解码 算法 Java
构建高效的Android应用:内存优化策略详解
随着智能手机在日常生活和工作中的普及,用户对移动应用的性能要求越来越高。特别是对于Android开发者来说,理解并实践内存优化是提升应用程序性能的关键步骤。本文将深入探讨针对Android平台的内存管理机制,并提供一系列实用的内存优化技巧,以帮助开发者减少内存消耗,避免常见的内存泄漏问题,并确保应用的流畅运行。
|
26天前
|
调度 数据库 Android开发
构建高效Android应用:Kotlin协程的实践与优化
在Android开发领域,Kotlin以其简洁的语法和平台友好性成为了开发的首选语言。其中,Kotlin协程作为处理异步任务的强大工具,它通过提供轻量级的线程管理机制,使得开发者能够在不阻塞主线程的情况下执行后台任务,从而提升应用性能和用户体验。本文将深入探讨Kotlin协程的核心概念,并通过实例演示如何在实际的Android应用中有效地使用协程进行网络请求、数据库操作以及UI的流畅更新。同时,我们还将讨论协程的调试技巧和常见问题的解决方法,以帮助开发者避免常见的陷阱,构建更加健壮和高效的Android应用。
35 4
|
28天前
|
移动开发 Java Android开发
构建高效Android应用:Kotlin协程的实践之路
【2月更文挑战第31天】 在移动开发领域,性能优化和流畅的用户体验一直是开发者追求的目标。随着Kotlin语言的流行,其异步编程解决方案——协程(Coroutines),为Android应用带来了革命性的并发处理能力。本文将深入探讨Kotlin协程的核心概念、设计原理以及在Android应用中的实际应用案例,旨在帮助开发者掌握这一强大的工具,从而提升应用的性能和响应能力。
|
29天前
|
移动开发 调度 Android开发
构建高效Android应用:探究Kotlin协程的优势与实践
【2月更文挑战第30天】 在移动开发领域,尤其是针对Android平台,性能优化和应用流畅度始终是开发者关注的重点。近年来,Kotlin语言凭借其简洁性和功能性成为Android开发的热门选择。其中,Kotlin协程作为一种轻量级的线程管理解决方案,为异步编程提供了强大支持,使得编写非阻塞性代码变得更加容易。本文将深入分析Kotlin协程的核心优势,并通过实际案例展示如何有效利用协程提升Android应用的性能和响应速度。
|
23天前
|
存储 算法 Linux
【Linux 应用开发 共享内存】深入理解和实践 ftruncate:共享内存的有效管理
【Linux 应用开发 共享内存】深入理解和实践 ftruncate:共享内存的有效管理
54 5
|
17天前
|
Java Android开发 开发者
构建高效Android应用:Kotlin协程的实践与优化
在响应式编程范式日益盛行的今天,Kotlin协程作为一种轻量级的线程管理解决方案,为Android开发带来了性能和效率的双重提升。本文旨在探讨Kotlin协程的核心概念、实践方法及其在Android应用中的优化策略,帮助开发者构建更加流畅和高效的应用程序。通过深入分析协程的原理与应用场景,结合实际案例,本文将指导读者如何优雅地解决异步任务处理,避免阻塞UI线程,从而优化用户体验。
|
1天前
|
移动开发 Android开发 开发者
构建高效Android应用:采用Kotlin进行内存优化的策略
【4月更文挑战第18天】 在移动开发领域,性能优化一直是开发者关注的焦点。特别是对于Android应用而言,由于设备和版本的多样性,确保应用流畅运行且占用资源少是一大挑战。本文将探讨使用Kotlin语言开发Android应用时,如何通过内存优化来提升应用性能。我们将从减少不必要的对象创建、合理使用数据结构、避免内存泄漏等方面入手,提供实用的代码示例和最佳实践,帮助开发者构建更加高效的Android应用。
4 0
|
3天前
|
缓存 移动开发 Java
构建高效的Android应用:内存优化策略
【4月更文挑战第16天】 在移动开发领域,尤其是针对资源有限的Android设备,内存优化是提升应用性能和用户体验的关键因素。本文将深入探讨Android应用的内存管理机制,分析常见的内存泄漏问题,并提出一系列实用的内存优化技巧。通过这些策略的实施,开发者可以显著减少应用的内存占用,避免不必要的后台服务,以及提高垃圾回收效率,从而延长设备的电池寿命并确保应用的流畅运行。
|
10天前
|
存储 算法
深入理解操作系统内存管理:原理与实践
【4月更文挑战第8天】 在现代计算机系统中,操作系统扮演着关键角色,特别是在内存资源的管理上。本文将深入探讨操作系统中的内存管理机制,包括虚拟内存、物理内存的分配与回收,以及页面置换算法等关键技术。通过分析不同内存管理策略的优势与局限性,本文旨在为读者提供一套系统的内存管理知识框架,帮助理解操作系统如何高效、安全地管理有限的内存资源以满足多任务处理的需求。

热门文章

最新文章