zoukankan      html  css  js  c++  java
  • Android 内存泄漏

    • Android中,内存泄露的现象十分常见;而内存泄露导致的后果会使得应用Crash

    1. 简介

    • 即 ML (Memory Leak)
    • 指 程序在申请内存后,当该内存不需再使用 但 却无法被释放 & 归还给 程序的现象

    2. 对应用程序的影响

    • 容易使得应用程序发生内存溢出,即 OOM

    3. 发生内存泄露的本质原因

    • 特别注意

    从机制上的角度来说,由于 Java存在垃圾回收机制(GC),理应不存在内存泄露;出现内存泄露的原因仅仅是外部人为原因 = 无意识地持有对象引用,使得 持有引用者的生命周期 > 被引用者的生命周期

    4. 储备知识:Android 内存管理机制

    4.1 简介

    下面,将针对回收 进程、对象 、变量的内存分配 & 回收进行详细讲解

    4.2 针对进程的内存策略

    a. 内存分配策略

    由 ActivityManagerService 集中管理 所有进程的内存分配

    b. 内存回收策略

    • 步骤1:Application Framework 决定回收的进程类型
      Android中的进程 是托管的;当进程空间紧张时,会 按进程优先级低->>高的顺序 自动回收进程

    示意图

    • 步骤2:Linux 内核真正回收具体进程 
      1. ActivityManagerService 对 所有进程进行评分(评分存放在变量adj中)
      2. 更新评分到Linux 内核
      3. Linux 内核完成真正的内存回收

    4.3 针对对象、变量的内存策略

    • Android的对于对象、变量的内存策略同 Java
    • 内存管理 = 对象 / 变量的内存分配 + 内存释放

    下面,将详细讲解内存分配 & 内存释放策略

    a. 内存分配策略

    • 对象 / 变量的内存分配 由程序自动 负责
    • 共有3种:静态分配、栈式分配、 & 堆式分配,分别面向静态变量、局部变量 & 对象实例
    • 具体介绍如下

    示意图

    注:用1个实例讲解 内存分配

    public class Sample {    
        int s1 = 0;
        Sample mSample1 = new Sample();   
         
         // 方法中的局部变量s2、mSample2存放在 栈内存
         // 变量mSample2所指向的对象实例存放在 堆内存
           // 该实例的成员变量s1、mSample1也存放在栈中
        public void method() {        
            int s2 = 0;
            Sample mSample2 = new Sample();
        }
    }
        // 变量mSample3所指向的对象实例存放在堆内存
        // 该实例的成员变量s1、mSample1也存放在栈中
        Sample mSample3 = new Sample();

    b. 内存释放策略

    • 对象 / 变量的内存释放 由Java垃圾回收器(GC) / 帧栈 负责
    • 此处主要讲解对象分配(即堆式分配)的内存释放策略 = Java垃圾回收器(GC
    • Java垃圾回收器(GC)的内存释放 = 垃圾回收算法,主要包括:

    垃圾收集算法类型

    • 具体介绍如下

    总结


    5. 常见的内存泄露原因 & 解决方案

    • 常见引发内存泄露原因主要有:
    1. 集合类
    2. Static关键字修饰的成员变量
    3. 非静态内部类 / 匿名类
    4. 资源对象使用后未关闭
    • 下面,我将详细介绍每个引发内存泄露的原因

    5.1 集合类

    • 内存泄露原因
      集合类 添加元素后,仍引用着 集合元素对象,导致该集合元素对象不可被回收,从而 导致内存泄漏

    • 实例演示

    // 通过 循环申请Object 对象 & 将申请的对象逐个放入到集合List
    List<Object> objectList = new ArrayList<>();        
           for (int i = 0; i < 10; i++) {
                Object o = new Object();
                objectList.add(o);
                o = null;
            }
    // 虽释放了集合元素引用的本身:o=null)
    // 但集合List 仍然引用该对象,故垃圾回收器GC 依然不可回收该对象
    • 解决方案
    • 集合类 添加集合元素对象 后,在使用后必须从集合中删除
    • 由于1个集合中有许多元素,故最简单的方法 = 清空集合对象 & 设置为null
    •  // 释放objectList
              objectList.clear();
              objectList=null;

    5.2 Static 关键字修饰的成员变量

    • 储备知识
      被 Static 关键字修饰的成员变量的生命周期 = 应用程序的生命周期

    • 泄露原因
      若使被 Static 关键字修饰的成员变量 引用耗费资源过多的实例(如Context),则容易出现该成员变量的生命周期 > 引用实例生命周期的情况,当引用实例需结束生命周期销毁时,会因静态变量的持有而无法被回收,从而出现内存泄露

    • 实例讲解

    public class ClassName {
     // 定义1个静态变量
     private static Context mContext;
     //...
    // 引用的是Activity的context
     mContext = context; 
    
    // 当Activity需销毁时,由于mContext = 静态 & 生命周期 = 应用程序的生命周期,故 Activity无法被回收,从而出现内存泄露
    
    }
    • 解决方案
    1. 尽量避免 Static 成员变量引用资源耗费过多的实例(如 Context
    1. 使用 弱引用(WeakReference) 代替 强引用 持有实例

    注:静态成员变量有个非常典型的例子 = 单例模式

    • 储备知识
      单例模式 由于其静态特性,其生命周期的长度 = 应用程序的生命周期

    • 泄露原因
      若1个对象已不需再使用 而单例对象还持有该对象的引用,那么该对象将不能被正常回收 从而 导致内存泄漏

    • 实例演示

    • // 创建单例时,需传入一个Context
      // 若传入的是Activity的Context,此时单例 则持有该Activity的引用
      // 由于单例一直持有该Activity的引用(直到整个应用生命周期结束),即使该Activity退出,该Activity的内存也不会被回收
      // 特别是一些庞大的Activity,此处非常容易导致OOM
      
      public class SingleInstanceClass {    
          private static SingleInstanceClass instance;    
          private Context mContext;    
          private SingleInstanceClass(Context context) {        
              this.mContext = context; // 传递的是Activity的context
          }  
        
          public SingleInstanceClass getInstance(Context context) {        
              if (instance == null) {
                  instance = new SingleInstanceClass(context);
              }        
              return instance;
          }
      }
    • 解决方案
      单例模式引用的对象的生命周期 = 应用的生命周期
    • 如上述实例,应传递ApplicationContext,因Application的生命周期 = 整个应用的生命周期
    • public class SingleInstanceClass {    
          private static SingleInstanceClass instance;    
          private Context mContext;    
          private SingleInstanceClass(Context context) {        
              this.mContext = context.getApplicationContext(); // 传递的是Application 的context
          }    
      
          public SingleInstanceClass getInstance(Context context) {        
              if (instance == null) {
                  instance = new SingleInstanceClass(context);
              }        
              return instance;
          }
      }

    5.3 非静态内部类 / 匿名类

    • 储备知识
      非静态内部类 / 匿名类 默认持有 外部类的引用;而静态内部类则不会
    • 常见情况
      3种,分别是:非静态内部类的实例 = 静态、多线程、消息传递机制(Handler

    5.3.1 非静态内部类的实例 = 静态

    • 泄露原因
      若 非静态内部类所创建的实例 = 静态(其生命周期 = 应用的生命周期),会因 非静态内部类默认持有外部类的引用 而导致外部类无法释放,最终 造成内存泄露
    • 实例演示
    // 背景:
       a. 在启动频繁的Activity中,为了避免重复创建相同的数据资源,会在Activity内部创建一个非静态内部类的单例
       b. 每次启动Activity时都会使用该单例的数据
    
    public class TestActivity extends AppCompatActivity {  
        
        // 非静态内部类的实例的引用
        // 注:设置为静态  
        public static InnerClass innerClass = null; 
       
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {        
            super.onCreate(savedInstanceState);   
    
            // 保证非静态内部类的实例只有1个
            if (innerClass == null)
                innerClass = new InnerClass();
        }
    
        // 非静态内部类的定义    
        private class InnerClass {        
            //...
        }
    }
    
    // 造成内存泄露的原因:
        // a. 当TestActivity销毁时,因非静态内部类单例的引用(innerClass)的生命周期 = 应用App的生命周期、持有外部类TestActivity的引用
        // b. 故 TestActivity无法被GC回收,从而导致内存泄漏
    • 解决方案 
      1. 将非静态内部类设置为:静态内部类(静态内部类默认不持有外部类的引用)
      2. 该内部类抽取出来封装成一个单例
      3. 尽量 避免 非静态内部类所创建的实例 = 静态

    5.3.2 多线程:AsyncTask、实现Runnable接口、继承Thread类

    • 储备知识
      多线程的使用方法 = 非静态内部类 / 匿名类;即 线程类 属于 非静态内部类 / 匿名类
    • 泄露原因
      当 工作线程正在处理任务 & 外部类需销毁时, 由于 工作线程实例 持有外部类引用,将使得外部类无法被垃圾回收器(GC)回收,从而造成 内存泄露
    • 实例演示
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 解决方案
      从上面可看出,造成内存泄露的原因有2个关键条件:
    1. 存在 ”工作线程实例 持有外部类引用“ 的引用关系
    2. 工作线程实例的生命周期 > 外部类的生命周期,即工作线程仍在运行 而 外部类需销毁

    解决方案的思路 = 使得上述任1条件不成立 即可。

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    5.3.3 消息传递机制:Handler

    具体请看文章:Android 内存泄露:详解 Handler 内存泄露的原因

    5.4 资源对象使用后未关闭

    • 泄露原因
      对于资源的使用(如 广播BraodcastReceiver、文件流File、数据库游标Cursor、图片资源Bitmap等),若在Activity销毁时无及时关闭 / 注销这些资源,则这些资源将不会被回收,从而造成内存泄漏

    • 解决方案
      Activity销毁时 及时关闭 / 注销资源

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    5.5 其他使用

    • 除了上述4种常见情况,还有一些日常的使用会导致内存泄露
    • 主要包括:ContextWebViewAdapter,具体介绍如下

    示意图

    5.6 总结

    下面,我将用一张图总结Android中内存泄露的原因 & 解决方案

    示意图


    6. 辅助分析内存泄露的工具

    内存泄露方面使用MAT工具定位分析。

    • 准备条件:

    1)手机是开发版room(建议使用小米的开发版room)

    2)手机开启root权限,可以下载一个root explorer获取root权限

    • 操作步骤:

    使用Androidstudio—>tools—>Android Device Monitor->进入DDMS 

    1)界面中选出应用程序的包名 

    2)点击Update Heap来更新统计信息(实现界面操作) 

    3)开始进行测试,测试结束后,点击Cause GC

    4)点击Dump HPROF file,将该应用当前的内存信息保存成hprof文件

    图片
    图1

    如果是用 MATEclipse 插件获取的 Dump文件,则不需要经过转换,Adt会自动进行转换然后打开;如果不是DDMS Dump 出的文件要经过转换才能被 MAT识别,进入到android sdk提供hprof-conv工具目录下,(一般位于sdk/platform-tools下)。输入命令 ./hprof-convxxx-a.hprof xxx-b.hprof进行转换。

    利用MAT打开转换后的 hprof文件:

    图片
    图2

    图片
    图3

    Histogram图表中主要统计了消耗占比较高的类的实例数量及占用空间 
    Dorminator Tree(支配树)支配树可以直观地反映一个对象的retained heap

    1、Histogram查询

    用的最多的功能是 Histogram,点击 Actions下的 Histogram项将得到 Histogram结果,它按类名将所有的实例对象列出来,可以点击表头进行排序,在表的第一行可以输入正则表达式来匹配结果,我们一般查看Activity和Fragment是否有内存泄露,一般Fragmen会有混淆,所以需要QA保留好对应的mapping文件。

    图片
    图4

    2、选中占用内存最大的Activity->右键—>Merge Shortest Path To GC Roots->exclude allphantom/weak/soft etc. references

    Merge Shortest Path To GC Roots 它能够从当前内存映像中找到一条指定对象所在的到GC Root的最短路径。这个功能还附带了其他几个选项,这几个选项分别指明了计算最短路径的时候是否是需要排除弱引用、软引用及影子引用等,一般来说这三种类型的引用都不会是造成内存泄漏的原因,因为JVM迟早是会回收只存在这三种引用的资源的,所以在dump内存映像之前我们都会手动触发一次gc,同时在找最短引用路径的时候也会选择上exclude all phantom/weak/soft etc. references选项,排除来自这三种引用的干扰。

    3、结果如下图,由图可以看出   com.android.org.chromium.android_webview.Aw-

    PasswordHandler @0x42cbee10 组件始终调用com.baidu.next.tieba.reply.activity.

    ReplyDetailActivity @0x42bd52d8这个函数,导致内存泄漏问题。

    图片
    图5

    日行一善, 日写一撰
  • 相关阅读:
    【学习笔记】2020寒假数据结构集训总结
    ThreadPoolExecutor源码分析
    Java并发专题(三)深入理解volatile关键字
    内部类与静态内部类
    Java并发专题(二)线程安全
    Java并发专题(一)认识线程
    Redis部署
    MySQL部署
    JDK部署
    SpringBoot+solr
  • 原文地址:https://www.cnblogs.com/xiyuan2016/p/14296669.html
Copyright © 2011-2022 走看看