51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

查看: 1560|回复: 2
打印 上一主题 下一主题

Android 内存泄露实践分析

[复制链接]

该用户从未签到

跳转到指定楼层
1#
发表于 2017-8-15 13:34:50 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
专项: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列表、对象赋空值
分析​    原理
  • Java内存分配机制
  • Java垃圾回收机制
​    根本原因
  • 关注堆内存
​    怎么解决
  • 详见方案
​    实践分析
  • 详见实践
方案
  • StrictMode

    • 使用方法:AppContext的onCreate()方法加上
    1. StrictMode.setThreadPolicy(new
    2. StrictMode.ThreadPolicy
    3.                     .Builder()
    4.                     .detectAll()
    5.                     .penaltyLog()
    6.                     .build());
    7. StrictMode.setVmPolicy(new
    8. StrictMode.VmPolicy
    9.                     .Builder()
    10.                     .detectAll()
    11.                     .penaltyLog()
    12.                     .build());
    复制代码


    • 主要检查项:内存泄露、耗时操作等
  • Leakcanary

    • GitHub地址
    • 使用方法
  • 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()中及时回收内存
  1. @Override
  2. protected void onDestroy() {
  3.     // TODO Auto-generated method stub
  4.     super.onDestroy();
  5.     recycleImageView(imgv_load_ad);
  6.     }

  7. public static void recycleImageView(View view){
  8.         if(view==null) return;
  9.         if(view instanceof ImageView){
  10.             Drawable drawable=((ImageView) view).getDrawable();
  11.             if(drawable instanceof BitmapDrawable){
  12.                 Bitmap bmp = ((BitmapDrawable)drawable).getBitmap();
  13.                 if (bmp != null && !bmp.isRecycled()){
  14.                     ((ImageView) view).setImageBitmap(null);
  15.                     bmp.recycle();
  16.                     bmp=null;
  17.                 }
  18.             }
  19.         }
  20.     }
复制代码

IO流未关闭
  • 分析:通过日志可知FileOutputStream()未关闭
  • 问题代码:

  1. public static void copyFile(File source, File dest) {
  2.         FileChannel inChannel = null;
  3.         FileChannel outChannel = null;
  4.         Log.i(TAG, "source path: " + source.getAbsolutePath());
  5.         Log.i(TAG, "dest path: " + dest.getAbsolutePath());
  6.         try {
  7.             inChannel = new FileInputStream(source).getChannel();
  8.             outChannel = new FileOutputStream(dest).getChannel();
  9.             inChannel.transferTo(0, inChannel.size(), outChannel);
  10.         } catch (IOException e) {
  11.             e.printStackTrace();
  12.         }
  13.     }
复制代码


  • 解决方案:

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




分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏1
回复

使用道具 举报

该用户从未签到

2#
 楼主| 发表于 2017-8-15 13:34:57 | 只看该作者
单例模式泄露
  • 分析:通过截图我们发现SplashActivity被ActivityUtil的实例activityStack持有
  • 引用代码:

  1. ActivityUtil.getAppManager().add(this);
复制代码
持有代码:
  1. public void add(Activity activity) {
  2.       if (activityStack == null) {
  3.           synchronized (ActivityUtil.class){
  4.               if (activityStack == null) {
  5.                   activityStack = new Stack<>();
  6.               }
  7.           }
  8.       }
  9.       activityStack.add(activity);
  10.   }
复制代码

  • 解决方案:

    • 在SplashActivity的onDestroy()生命周期移除引用
    1. @Override
    2.     protected void onDestroy() {
    3.         super.onDestroy();
    4.         ActivityUtil.getAppManager().remove(this);
    5.     }
    复制代码


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

    1. private static HttpRequest req;
    2. public
    3. static void HttpUtilPost(Context context, int TaskId, String url,
    4. String requestBody,ArrayList<HttpHeader> Headers, RequestListener
    5. listener) {
    6.       // TODO Auto-generated constructor stub
    7.       req = new HttpRequest(context, url, TaskId, requestBody, Headers, listener);
    8.       req.post();
    9.   }
    复制代码

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

    1. private static HttpRequest req;
    2. public
    3. static void HttpUtilPost(Context context, int TaskId, String url,
    4. String requestBody,ArrayList<HttpHeader> Headers, RequestListener
    5. listener) {
    6.         // TODO Auto-generated constructor stub
    7.         req = new HttpRequest(context.getApplicationContext(), url, TaskId, requestBody, Headers, listener);
    8.         req.post();
    9.     }
    复制代码


    Context泄露
    Callback泄露
    服务未解绑注册泄露
  • 分析:一般发生在注册了某服务,不用时未解绑服务导致泄露
  • 引用代码:
    1. private void initSensor() {
    2.         // 获取传感器管理器
    3.         sm = (SensorManager) container.activity.getSystemService(Context.SENSOR_SERVICE);
    4.         // 获取距离传感器
    5.         acceleromererSensor = sm.getDefaultSensor(Sensor.TYPE_PROXIMITY);
    6.         // 设置传感器监听器
    7.         acceleromererListener = new SensorEventListener() {
    8.         ......
    9.         };
    10.         sm.registerListener(acceleromererListener, acceleromererSensor, SensorManager.SENSOR_DELAY_NORMAL);
    11.     }

    复制代码

  • 解决方案:

    • 在Activity的onDestroy()方法解绑服务
  1. @Override
  2. protected void onDestroy() {
  3.   super.onDestroy();
  4.   sm.unregisterListener(acceleromererListener,acceleromererSensor);
  5. }
复制代码


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

handler.sendEmptyMessage(0);
  • 解决方案:

    • 在Activity的onDestroy()方法回收Handler
  1. @Override
  2. protected void onDestroy() {
  3.   super.onDestroy();
  4.   handler.removeCallbacksAndMessages(null);
  5. }
复制代码

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

  1. new Thread() {
  2.   public void run() {
  3.     imageArray = loadImageFromUrl(imageUrl);
  4.   }.start();
复制代码

  • 解决方案:

    • 把线程作为对象提取出来
    • 在Activity的onDestroy()方法阻塞线程
后面
  • 欢迎补充实际中遇到的泄露类型
  • 文章如有错误,欢迎指正
  • 如有更好的内存泄露分享方法,欢迎一起讨论

回复 支持 反对

使用道具 举报

本版积分规则

关闭

站长推荐上一条 /1 下一条

小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

GMT+8, 2024-4-19 13:09 , Processed in 0.069829 second(s), 22 queries .

Powered by Discuz! X3.2

© 2001-2024 Comsenz Inc.

快速回复 返回顶部 返回列表