关于ViewGroup$ViewLocationHolder$mRoot的内存泄漏

2023-10-27

今儿遇到个场景:在Android P(API 28)中,在退出了含有RecyclerView的RelativeLayout中,LeakCanary报了这么一个内存泄漏:
在这里插入图片描述

1. 定位问题

1.1 定位源码

在AndroidP中ViewGroup内部有这么一个静态内部类ViewLocationHolder

// ViewGroup.java
    /**
     * Pooled class that holds a View and its location with respect to
     * a specified root. This enables sorting of views based on their
     * coordinates without recomputing the position relative to the root
     * on every comparison.
     */
    static class ViewLocationHolder implements Comparable<ViewLocationHolder> {
        private static final int MAX_POOL_SIZE = 32;
        private static final SynchronizedPool<ViewLocationHolder> sPool =
                new SynchronizedPool<ViewLocationHolder>(MAX_POOL_SIZE);
        public static final int COMPARISON_STRATEGY_STRIPE = 1;
        public static final int COMPARISON_STRATEGY_LOCATION = 2;
        private static int sComparisonStrategy = COMPARISON_STRATEGY_STRIPE;
        private final Rect mLocation = new Rect();
        private ViewGroup mRoot;   // 1
        public View mView;
        private int mLayoutDirection;

        public static ViewLocationHolder obtain(ViewGroup root, View view) {
            ViewLocationHolder holder = sPool.acquire();  // 2
            if (holder == null) {
                holder = new ViewLocationHolder();
            }
            holder.init(root, view);  // 3
            return holder;
        }
        
        private void init(ViewGroup root, View view) {
            Rect viewLocation = mLocation;
            view.getDrawingRect(viewLocation);
            root.offsetDescendantRectToMyCoords(view, viewLocation);
            mView = view;
            mRoot = root;   // 4
            mLayoutDirection = root.getLayoutDirection();
        }

        private void clear() {  //5
            mView = null;
            mLocation.set(0, 0, 0, 0);
        }
       .....
    }

从英文注释可以看出来,这个类的作用是保存一个View和它的位置(Rect),使用它的类能做通过 List<ViewLocationHolder>的compare,能把这些View在原来的ViewGroup上排列好,而不用重新去计算这些view在ViewGroup上的顺序位置了。
我们在来解析一下源码:
注释1:这个静态类有个全局变量 mRoot,表示的是这个View的父View
注释2:因为他是以池子的形式存储,所以它的获取方式是 obtain(),在池子中取出一个空的ViewLoacationHolder,如果取不出,就new一个出来。

注释3:拿到注释2的 ViewLocationHodler,调用 init()对它初始化
注释4:赋值mRoot

注释5:在clear()方法中,并没有把mRoot置空…

但从这里看,我们就已经知道了为什么泄漏了,在ViewGroup销毁的时候,由于其静态内部类ViewLocationHolder的mRoot字段没有释放,所以它持有着这个ViewGroup的引用,导致ViewGroup的内存也不能释放,产生了内存泄漏。

1.2 是否能解决

解决方法是在 clear()中将mRoot字段置空,或者将 ViewLocationHolder.mRoot字段设置为弱引用
但是,我们修改不了ViewGroup的源码,它是属于framework层的= = ,他是来自于framework层的Bug,所以我们只能任由这个泄漏出现…

2. 源码反推

这里不得不产生了更多的问号。
(1)为什么是只有Andorid P有这个玩意?
(2)我用到ViewGroup的地方这么多,那是不是只要在Android P上,我随时随地都可能出现这个Bug?

对于这样的问题,我不得不再往下深入代码了= =
首先,我们得先找到ViewLocationHolder会在什么时候拿出来用,它的入口方法是 ViewLocationHolder.obtain(),我们要看看是谁调用了obtain:

// ViewGroup.java
    /**
     * Pooled class that orderes the children of a ViewGroup from start
     * to end based on how they are laid out and the layout direction.
     */
    static class ChildListForAccessibility {

        private static final int MAX_POOL_SIZE = 32;
        private static final SynchronizedPool<ChildListForAccessibility> sPool =
                new SynchronizedPool<ChildListForAccessibility>(MAX_POOL_SIZE);
        private final ArrayList<View> mChildren = new ArrayList<View>();
        private final ArrayList<ViewLocationHolder> mHolders = new ArrayList<ViewLocationHolder>();  // 1
        
        public static ChildListForAccessibility obtain(ViewGroup parent, boolean sort) {  
            ChildListForAccessibility list = sPool.acquire(); // 2
            if (list == null) {
                list = new ChildListForAccessibility();
            }
            list.init(parent, sort);   // 3
            return list;
        }

        private void init(ViewGroup parent, boolean sort) {
            ArrayList<View> children = mChildren;   // 4
            final int childCount = parent.getChildCount();
            for (int i = 0; i < childCount; i++) {  
                View child = parent.getChildAt(i);
                children.add(child);    // 5
            }
            if (sort) { // 6
                ArrayList<ViewLocationHolder> holders = mHolders; // 7
                for (int i = 0; i < childCount; i++) {
                    View child = children.get(i);
                    ViewLocationHolder holder = ViewLocationHolder.obtain(parent, child); // 8
                    holders.add(holder);
                }
                sort(holders);  // 9
                for (int i = 0; i < childCount; i++) {  // 10
                    ViewLocationHolder holder = holders.get(i);
                    children.set(i, holder.mView); 
                    holder.recycle();   // 11
                }
                holders.clear();
            }
        }
        ...
    }    

这里又出现了了一个ViewGroup的静态内部类:ChildListForAccessibility ,从英文注释中可以看出,它的作用就是管理所有的ViewLocationHolder,而且它同样被放在一个池子中。在注释1中,它持有了一个ViewLocationHolder类型的list。来解析下这个源代码:

注释2、3:从池子中取出一个空的ChildListForAccessibility,然后调用其 init()

注释4、5:调用ViewGroup.getChildCountViewGroup.getChildAt,拿到所有的子View存放到 children对象中。
注释6:判断是否需要对这些子View进行排序,如果要,则进入到if语句中去。

注释7:创建 ViewLocationHolder类型的list
注释8:为注释5中的 children对象里面的每一个子View创建一个 ViewLocationHolder,并放入到注释7的list中
注释9:给这个list排序。排序后,里面所有的子View都有了顺序。
注释10:遍历这个排序的list,重新将排好序的list的子View放到 children对象中去。
注释11:注释7的list已经没用了,所以调用每个 ViewLocationHolder.recycler(),这个方法就会调用上节中的 clear()释放资源。

这个类的作用是对ViewGroup的所有子类进行排序,所以我们要找到从哪里进行排序的,因为ChildListForAccessibility.obtain()是入口方法,所以我们要找到使用到这个方法的地方,我发现有两处ViewGroup的方法调用了它,他们分别是

  • ViewGroup.addChildrenForAccessibility
    将可以访问(即可以有焦点)的子View添加到 outChildren这个对象中
  • ViewGroup.dispatchPopulateAccessibilityEventInternal
    用来分发焦点事件,遍历所有排序后的子View,如果某个子View获取焦点,则退出循环。

这个方法是处理一个ViewGroup里面可以获得焦点的子View的一类方法。也就是说,无论是哪个版本,都可以执行这些方法。

下面是截取的Android7.0的ViewGroup的 ViewLocationHolder类:
在这里插入图片描述
下面是截取自Android8.0的代码:
在这里插入图片描述
下面是Android10.0的代码:

    static class ViewLocationHolder implements Comparable<ViewLocationHolder> {
        ....
        private ViewGroup mRoot;
        ....
        private void clear() {
            mView = null;
            mRoot = null; // 这里置空了
            mLocation.set(0, 0, 0, 0);
        }
    }

这里发现,Android10.0中在clear()方法里,对mRoot置空了,就把这个Bug给修了…

3. 结论

  1. 该问题是基于Andorid9.0 Framewrok层 ViewGroup的一个Bug,静态内部类的mRoot没有及时释放持有的外部引用导致的泄漏。在Android9.0以前没有mRoot,Android10在释放资源时将mRoot置空修复该Bug。
    在Android9.0的Java代码层无法进行修复。
  2. 基于手机厂商可能会修改fwk层的代码,有些厂商可能发现了这个bug所以进行了修复,但是有些厂商没有发现,所以这就导致了并非每个手机都会出现这样的问题。
  3. 该问题比较容易出现在多获取焦点子View的ViewGroup中,比如有RecyclerView、ListView的ViewGroup里。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

关于ViewGroup$ViewLocationHolder$mRoot的内存泄漏 的相关文章

随机推荐

  • vue3组件内判断是否进行路由跳转

    import onBeforeRouteLeave from vue router import ElMessageBox from element plus onBeforeRouteLeave to from next gt if fo
  • 【毕业设计】机器视觉停车位识别检测系统 - python 深度学习

    文章目录 1 简介 2 检测效果 3 实现方式 3 1 整体思路 3 2 检测空车位 3 3 车辆识别 4 最后 1 简介 Hi 大家好 这里是丹成学长的毕设系列文章 对毕设有任何疑问都可以问学长哦 这两年开始 各个学校对毕设的要求越来越高
  • PHP7 连oracle 11g 可能出现的pdo_oci_handle_factory,Check the character问题和解决方法

    错误代码 SQLSTATE HY000 pdo oci handle factory Error while trying to retrieve text for error ORA 12541 错误代码 SQLSTATE HY000 O
  • 分布式日志系统解决方案

    一 什么是PlumeLog PlumeLog 是一款无入侵的分布式日志系统 基于log4j log4j2 logback搜集日志 设置链路ID 方便查询关联日志 基于elasticsearch作为查询引擎 实现日志报错预警 PlumeLog
  • C++ Primer 16 类模板部分特例化

    模板与泛型编程 面向对象编程和泛型编程都能矗立在编写程序时不知道类型的情况 不同之处在于 OPP 能处理类型在程序运行之前都未知的情况 而泛型编程中 在编译时就能知获类型了 当我们编写一个泛型程序时 是独立于任何特定类型来编写代码的 模板是
  • 基于mbedTLS算法库实现国密SM2签名和验签算法

    网上有大量的基于OpenSSL实现的国密算法库 比如著名的GmSSL 可以直接拿来用 我自己常用的是mbedTLS的算法库 比较小巧简单 在mbedTLS的大数算法的基础上实现了国密SM2的签名和验签算法 在基于mbedTLS实现SM2签名
  • 配置pacemaker时用到的一些CRM CLI命令

    官方网站 http www clusterlabs org doc crm cli html CRM CLI是分层设计 常见层级如下 node 主要用于节点的各种操作 resource 主要用于资源的各种操作 ra 查看RA的各种属性 co
  • python 均方误差_python如何实现均方误差和均方根误差?

    一 python实现均方误差 均方误差是各数据偏离真实值的距离平方和的平均数 也即误差平方和的平均数 用法 一般被用在机器学习的预测值与真实值之间的距离 均方误差对应的是最小二乘法 coding utf 8 import math def
  • python程序员 培训

    非常感谢大家这么长时间对我们的喜爱和关注 我们都知道 python是当前的大趋势 无论是就业前景 发展空间 还是薪资待遇都是当下最为火爆的行业 所以我们特意联系了央视公开课曾推荐的万门大学 向大家赠送一份人工智能的课程 这个课程我们已经了解
  • QSlider风格设置

    QT的滑动条在开发的过程中经常使用 默认的QSlider风格比较简陋 一般达不到UI设计的效果 本篇记录一个QSlider使用过程中风格的设置 1 qss常用的字段属性 1 1背景属性 属性 值 意思 background Backgrou
  • 用C语言实现经典游戏——贪吃蛇

    目录 1 游戏实现思想 1 定义蛇对象 2 食物对象 3 分数 4 初始化蛇 5 初始化食物 6 修改控制台光标位置 7 画出蛇和食物 8 蛇的移动控制 9 开始游戏 10 蛇移动 11 画墙 12 去除蛇尾 13 去除光标 14 显示分数
  • 06.数据库日常应用实例

    生活事例 01 用户注册 A select操作 搜索数据库所有用户名 B 与新注册的用户名进行比对 C 如果相同 新用户需从新改名 D 直到与数据库中的用户名不同 方能注册 02 转账 A select操作 搜索数据库所有账号 B 收款方账
  • Please enable Javascript to view this page

    今天进防火墙时出错 以下设置解决 internet选项 gt 安全 gt 受信任站点 gt 添加站点 转载于 https www cnblogs com cw828 p 10063200 html
  • 测试原则-阶段-测试用例设计-调试

    测试原则 阶段 测试用例设计 调试 测试原则和方法 测试阶段 测试策略 测试用例的设计 调试 软件度量 测试原则和方法 系统测试 成功的测试 测试原则 软件测试的方法 静态测试 动态测试 测试阶段 单元测试 集成测试 确认测试 系统测试 配
  • 【ETH链游】阿蟹Axie Infinity模拟器运行及多开

    Axie Infinity 众所周知 阿蟹 Axie Infinity 是去年最火的一款GameFi游戏 由越南团队Sky Mavis开发 短短几个月内就红遍东南亚 最大市场在菲律宾 其次是越南 马来西亚 印尼与美国等 去年6月 该游戏单日
  • 个人学习经验: C++ ifndef 作用和用法

    ifndef是if not define的简写 一般的使用场景为 1 头文件中使用 防止头文件被多重调用 2 作为测试使用 省去注释代码的麻烦 3 作为不同角色或者场景的判断使用 头件的中的 ifndef 这是一个很关键的东西 比如你有两个
  • react函数式组件传值之子传父

    在用react进行函数式编程时 父组件可以通过props向子组件传值 那么子组件怎么向父组件传值呢 首先 父组件需要向子组件传递一个函数 然后 子组件通过props获取函数并附上参数 最后 父组件通过函数拿到子组件传递的值 一 具体案例 父
  • gridcontrol选中多行数据进行复制_奇技淫巧:在 ssh 里面把服务器的文本复制到本地电脑...

    点击上方 Python编程时光 选择 加为星标 第一时间关注Python技术干货 2020年 力度最大的购书优惠 千万别错过了 使用 macOS 的同学 应该熟悉一个命令pbcopy 它可以在命令行中把一段内容写入到剪贴板 例如 echo
  • Java安装配置教程,2023年最新版,全部版本看这一篇就够了!!

    JDK新手无脑安装配置教程 JDK下载网址 ps 如果你的JDK版本在官网没有找到 可以通过第三方资源进行下载 下载安装配置教程是通用的 官方链接 gt https www oracle com java technologies java
  • 关于ViewGroup$ViewLocationHolder$mRoot的内存泄漏

    今儿遇到个场景 在Android P API 28 中 在退出了含有RecyclerView的RelativeLayout中 LeakCanary报了这么一个内存泄漏 1 定位问题 1 1 定位源码 在AndroidP中ViewGroup内