蚂蚁质量 AnTest 快船 | 作者
一、背景
随着支付宝小程序和 H5 应用的生态开放,很多前端层面的业务自定义弹层不满意监管合规要求的现象日益突出。本文提出一种基于运行时的动态检测算法来解决此类前端弹层的识别问题。
检测范围:支付宝所有小程序或 H5 应用在 Render 层 Webview 出现的前端业务弹层。
二、现有检测方案分析
目前前端领域有部分前端弹层的检测方案。主要可分为两个方向:
- 基于图像算法理论的弹层识别
- 基于前端 Dom 树的弹层识别
对这两种算法进行调研分析,对两者算法之间的优缺点进行了比对分析,如下表所示:
综上分析,基于前端 Dom 结构的检测相比于图像算法有以下两大优势:
1.具备运行时的动态检测能力。
2、检测类型 较广泛,对于正常开发思路设计的弹层都有相应的检测能力
通过实际调研发现目前端领域的弹层检测方案主要是基于 Puppeteer(无头浏览器)检测方案,可理解为模拟器环境启动一个 Chrome 浏览器进行检测。基于真机运行时的弹层检测方案目前较为零散,没有一套相对成熟的算法机制。
因此本文旨在从算法层面出发,实现一套在任何浏览器环境下都可以运行和检测的前端弹层识别算法,在小程序或者 H5 动态运行时自动进行检测,捕捉支付宝生态开放后业务方实现的前端弹层。
三、前端弹层理论
前端弹层定义(业务层面)
要检测弹层,自然首先要定义弹层,弹层一般有以下特性:
- 置于顶层占领屏幕或是屏幕大部分面积,遮住其他内容且难以忽略或绕开,强制用户进行交互(关闭 or 跳转)
- 置于顶层的占据屏幕较小区域,通常为 banner 图片或提醒添加到首页等提示性弹层,不影响用户交互
其中交互包括:接收到 click、touch、滑动、页面隐藏/销毁等事件
## 前端弹层特点
- position 属性设置为 absolute 或 fixed 绝对定位
- 弹层容器中心点 document.elementFromPoint 结果必定为弹层节点的子节点(包括自身),如果不是说明弹层容器已不在屏幕顶部,为内容节点
- 弹层容器与页面视口重叠面积达到视口总面积的一定百分比,过小的节点不考虑
- 弹层容器内部覆盖一定数量和面积的内容元素,如果没有覆盖任何元素说明
- 弹层容器内部子元素的数量和面积大于 0,否则极大概率是一个单纯的 DOM,比如遮罩层,或者 css 纯色背景
# 四、弹层检测系统方案设计
整套检测系统从流程上可分为以下两步:
- 动态监测每次 Dom 变更记录,通过算法挑选出可能为弹层的候选节点集合,记为 Candidates
- 计算和更新候选节点 Candidates 和渲染帧的节点位置信息相交情况,通过算法确认有实际意义的弹层节点
## 检测方式
前端文件注入 js 分析文件,在页面动态运行时进行实时分析
弹层检测系统流程架构图
算法关键点解析
第一步:挑选弹层候选节点
1)监听 Mutation Observer 获得 Dom 变更记录,为什么只取出第一层的 Records?
弹层跳出时机有两种:
- 单独作为一个 Dom 渲染帧出现,此时弹层的最外层容器必为 Records 的第一层,记为第一类弹层
- 和其他内容节点容器在同一帧一起渲染,此时弹层不一定为 Records 的第一层,记为第二类弹层
因此只需遍历 Records 第一层就可以把可能出现的第一类弹层全部检测出来;而第二类弹层需要遍历 Records 所有元素才可捕捉到,而页面渲染包含大量渲染帧,如果将每次渲染帧的所有元素都遍历一遍,那么会导致算法复杂度很高不利于运行时的实时检测,因此第二类弹层检测采用如下算法进行检测
2)第二类弹层筛选算法
上述算法已将可能的第一类弹层全部挑出,在此基础上我们通过视觉坐标定位的方式尽可能的收集出第二类弹层元素。
首先可以明确的一点是业务写的前端弹层基本在以下 6 种形式呈现在页面中:
通过观察发现我们只需要对页面特定区域进行检测即可找出可能存在的第二类弹层。因此方案设计如下:
遍历第一类候选弹层的所有 element,对每个 element 进行如下操作:
- 获取元素相对于视口位置的 width 和 height
- 通过 document.elementsFromPoint 方法挑出如下特定坐标的所有 HTML 元素,加入到数组集合 candidateElement 中
页面初始渲染时,在第一次 Dom 变更记录中 Records 的最外层必定存在一个覆盖整个视口面积的 div 元素,因此这个元素一定存在于第一类弹层的检测结果中。这也自然将手机屏幕中的这些特定位置坐标点加到了我们筛选出的特定坐标中,保证了之前提到的 6 种弹层能全部检测到。为了更全面的搜索其他未知区域的弹层,我们也将其他第一类候选弹层的特征点加入进来,进一步扩大弹层搜索的区域范围。相比遍历每帧的所有元素,此算法只需遍历个位数级别的元素,大大降低算法复杂度且搜索精度极高,适合运行时的动态检测。
3)如何确认弹层候选元素
经过上述算法已将两类弹层候选节点筛选出来,接下来经过以下三个步骤最终确认弹层候选元素。
- 对所有 candidateElement 节点使用 document.elementFromPoint 查找中心位置的最外层节点。若该节点结果为当前 Candidates 节点的子孙,则说明该节点可能为弹层节点,否则说明节点已经不显示在屏幕顶部剔除掉
- 过滤出 candidateElement 与视口 Viewport 重叠面积超过视口面积 10% 的节点
- candidateElement 元素 position 属性设置为 absolute 或 fixed 绝对定位
至此,第一步挑选弹层候选元素结束,记为数组 Candidates,接下来进入第二步确认阶段。
第二步:确认候选节点是否为弹层
算法核心理论
计算和更新 Candidates 中每个元素(记为 Candidate)与渲染帧所含内容节点位置信息相交情况,包括以下四项:
●包含的内容节点数量
●包含的内容节点面积
●非包含的内容节点位置重叠数量
●非包含的内容节点位置重叠总面积
流程可分为三步:
- 首先统计每个 Candidate 和当前最后渲染帧所含内容节点位置信息相交情况。
- 当页面出现新渲染帧,将 Candidate 与新渲染帧的内容节点位置变更进行计算,更新相交情况。
- 当出现用户交互(用户事件、页面隐藏、页面销毁)或者页面 Dom 稳定 15 秒不发生任何变化时停止检测,开始对弹层的视觉可视化进行检测,包括以下几项:
●如果 Candidate 内部含的内容节点数量和面积均为 0,证明该弹层无实际意义的可视内容,直接剔除
●如果 Candidate 底下覆盖的内容节点数量和面积均为 0,证明该弹层底下没有覆盖任何内容,从弹层角度上来说没有任何覆盖效果,直接剔除
●通过 getComputedStyle 判断 Candidate opacity、visibility、display 属性是否为不可见,直接剔除
●如果 Candidate 元素的宽高刚好等于屏幕视口的宽高,证明此弹层大概率是蒙层弹层,此时计算弹层底下覆盖内容面积/弹层元素自身总面积,如果占比低于 10% 我们也直接剔除
到此为止,整个前端弹层的检测流程结束,剩余的 “幸存” 节点作为前端弹层的检测结果上报给后台。
五、弹层检测结果
六、未来方向
特殊场景的检测准确性
- 页面渲染存在业务的自定义全屏 loading,容器被误判为弹层
- 页面在弹层底下覆盖内容为空(部分白屏情况),此时节点的面积一定为 0,此条件下存在误判可能性
↙↙↙阅读原文可查看相关链接,并与作者交流