51Testing软件测试论坛

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

QQ登录

只需一步,快速开始

微信登录,快人一步

手机号码,快捷登录

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

Android 内存泄露实践分析

[复制链接]
  • TA的每日心情
    无聊
    前天 09:07
  • 签到天数: 11 天

    连续签到: 2 天

    [LV.3]测试连长

    跳转到指定楼层
    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
    回复

    使用道具 举报

  • TA的每日心情
    无聊
    前天 09:07
  • 签到天数: 11 天

    连续签到: 2 天

    [LV.3]测试连长

    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-9-21 04:39 , Processed in 0.067700 second(s), 23 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

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