注1:本文为之前一些片段思考的汇总成文,一方面是作者近期无精力重新梳理文章(主要原因),另一方面也希望把最原始的思考路径展示给大家,方便一起交流学习。
注2:本文展示的是通用思路,但其中一些细节是从iOS开发的角度思考的,经过长期的验证我们可以肯定的是Android方面同样适用。
上文 小桔棱镜-专注移动端操作行为的利器 中,我们系统的介绍了小桔棱镜的整体功能,接下来我们会针对一些核心问题进行专题讨论,欢迎大家拍砖交流~
唯一标识这个字眼更多的是在大家探讨iOS端UI自动化测试的问题时出现,相比而言同样会涉及到此概念的自动埋点场景却出现的不多,市面上也没有看到过比较成熟的应用方案,本文我们尝试探讨一次,希望可以抛砖引玉。
根据我的理解这部分的难点一是唯一的标识元素,二是在后续千变万化的版本迭代中,唯一标识的生成公式中的变量变化引起唯一标识发生变化,导致埋点信息错乱。终极难点是对这两个难点的平衡,即可以准确实现对元素的唯一标识,同时能保证这个唯一标识的稳定性,这样才能真正实现自动埋点的意义。
熟悉移动端开发的同学通常可以大致例举出一些生成唯一标识的策略,从视图层面的响应链信息、布局信息,到代码层面的变量名、方法名等,可以利用的变量有很多,那怎样的策略是最适用于自动埋点场景的呢?
在讨论方案前,我们首先思考一下理想的唯一标识元素的策略应该满足的基本原则:
- 唯一,尽可能保证生成的标识唯一。
- 稳定,尽可能降低非主观修改引起的标识变化。
- 易察觉,非主观修改引起的标识变化不可避免,那就在变化出现时尽可能让用户感知到,减少带来的负面影响。
- 无侵入,在满足前三条的基础上,对业务代码无侵入是一个很重要的加分项。
其中着重解释一下原则3,它看起来像是在追求完美方案道路上的妥协,其实可能使我们更接近一个好的方案。既然无法避免唯一标识异常变化的发生,那就设计一种方案在异常变化发生时,让用户也能够明显感知到,从而减少这个异常带来的负面影响。也是基于这个原则,我选择了一个合适的角度开始了下面的思考:
有一点哲学的味道,埋点名称往往取决于我们对目标元素的定义。我们通常如何定义一个元素呢?可以试着说出我们看到的界面上的每一个按钮,比如首页的刷新按钮、地址选择页的搜索按钮、结束页面的关闭按钮,我们对元素的描述就代表了我们对它的定义,这个描述在我们的认知中肯定是唯一的,因为我们能区分出不同的元素。
具体分析一下,比如首页的刷新按钮,”首页“代表的是元素所处的空间信息,也就是它所处的页面层级。”刷新按钮“代表元素的功能信息,是因为它的样式或者被点击以后的实际效果才会被叫做”刷新按钮“。那么可以大概总结出我们对一个元素的认知方式:元素 = 它的空间信息 + 功能信息(它的样式或者实际功能)。
我们试着模仿人们对于元素的认知方式,尝试把它转化为更具体的iOS代码,当然这个是很简单片面的一种模仿,逼真的模拟牵扯到了人工智能而不是这么简单的几行代码,我们的出发点是期望生成的唯一标识能够尽可能的贴近用户对于元素的认知,这样唯一标识的异常变化就容易被察觉和接受。
APP在某一瞬间的页面结构相当于一个树,树中的叶子节点就代表了界面上的各个元素,一个元素的响应链就是从自己到根节点的这个链条。我们依次记录响应链上的所有节点的类名就大致描述出目标元素的空间位置,但仔细斟酌后发现有两个问题:
- 这个空间描述可能不唯一,也就是不满足原则1
- 在后续版本迭代中元素的空间描述可能会经常变化,也就是不满足原则2
先来分析问题1,在通常情况下问题1的可能性非常小,因为我们在业务开发中类似于使用UIViewController或者UIView肯定会继承重写,产生的新类的类名自然就是唯一的,但还是存在复用的问题:
举个例子就像我们乘客端APP中的快车和专车,这两个业务线复用了同一类ViewController,依据上述方法就无法保证唯一,针对类似情况,合理的解法是给一些核心节点(本例中就是这个被复用的ViewController)手动设置ID来保证唯一,这个解法的成本不大可以接受。
再来分析问题2,日常开发中随着我们的版本迭代,经常会有加一层view减一层view的操作,也就导致这个空间描述信息很不稳定。针对这种情况我们可以近一步优化,只取响应链上的UIViewController类型的节点,相当于只关心目标元素所在的“大框架”,而不在乎它具体的页面层级信息,这个也符合人们对于元素空间位置的认知,同时“大框架”变化的概率会小很多。
上文问题2的解法中提到了只关注“大框架”,它其实又引出了一个问题,就是类似于UIAlertView这样的弹窗是直接加到UIWindow上的,也就是其响应链上根本不存在UIViewController,针对这种情况,我们只能去关注它具体的页面层级信息而非“大框架”了。
上文我们说到人们对于一个元素的功能认知首先来自于元素的样式,再有就是元素被操作后的实际效果。那我们用代码还原一下这个思路,比如一个按钮,它能被人们感知到的最直接的样式依次为:
- 按钮名称,比如“确认”、“取消”等。
- 按钮图片,很多按钮没有名称,取而代之的是一个很形象的小icon。
这两点基本可以描述一个按钮的功能了,首先满足了原则2,它们变化的概率都很小,尤其是按钮图片,我们在实际开发中如果要给一个按钮换图片,做法也是保证图片名称不变只换图片本身,这样对于我们的策略来说相当于没有变化。其次它们也很好的满足了原则3,也就是如果这两点真的变化了,用户是可以明显感知到的,此时对应的埋点信息的改变也就更容易被察觉和接受了,这也是我们从人们的认知角度出发思考的初衷。
当然也有特例,有些按钮在代码层面获取不到具有代表性的名称和图片,这时就可以关注它被操作后的实际效果了,反映到代码上就是它实际触发的方法,再具体到iOS上也就是target和selector了。这种描述方式相对来说不贴近人们的认知,虽然变化的可能性也不大,但是一旦变化用户很难察觉到,我们把它当作兜底策略吧。
还有特例,比如常见的UITableView和UICollectionView中的Cell,同样很难获取到理想的名称和图片,并且点击它们触发的方法也不具有唯一性。针对这种情况,可以选择使用 控件名称(即UITableView或者UICollectionView的类名)+ Cell名称(即Cell的类名)+ Cell的section(即indexPath.section)+ Cell的row(即indexPath.row)组合起来生成一个唯一的名称。
基于上述思想,我们进行总结归类:
areaInfo = "空间描述";
if 元素的响应链上有UIViewController类型的节点
for 循环所有的UIViewController类型的节点leaf
if 节点leaf的类型被复用
leafName = "手动设置的ID";
else
leafName = "节点类名";
areaInfo = areaInfo + leafName;
else
for 循环所有的节点leaf
areaInfo = areaInfo + leafName;
name = "功能描述"
switch 控件类型
case UIControl:
name = "优先titleName,其次imageName,最后target+selector";
case UITapGestureRecognizer:
name = "target+selector";
case UITableViewCell or UICollectionViewCell:
name = "控件类名+Cell类名+Cell的section+Cell的row";
元素的唯一标识 = areaInfo + name;
上文阐述的方案,基本可以满足本文开头罗列的三个原则了,但细想之后发现依然有漏洞,更不用说如果融入日常生产环境后可能暴露的问题了。本文以外我还有过一些思路:
- 比如考虑每次点击触发的网络请求,因为网络请求能很好的代表业务信息,也就便于实现唯一标识。
- 比如考虑把元素在屏幕上的位置(右上、左上、右下、左下等)也融入到唯一标识中,符合人们的认知同时也可以增强唯一性。
- 再比如考虑每次点击引起的界面变化。
有些思路有点异想天开,有些思路经过斟酌后选择了放弃,留下的就是本文的内容。
那针对可能存在的漏洞我们能做点什么呢?我尝试换个角度去解决,可以在APP中内置工具(基于已有的小桔棱镜工具),在Debug版的运行时中动态检查存在的唯一标识冲突的问题并提示给用户,便于这些隐藏的小概率问题的发现和解决。
以上就是我对于移动端唯一标识元素策略的思考,限于已有知识和经验可能有很多不完善的地方,也希望大家能够广泛讨论并建议,感谢阅读。
分隔线
上文中提到:比如考虑把元素在屏幕上的位置(右上、左上、右下、左下等)也融入到唯一标识中,符合人们的认知同时也可以增强唯一性
这一点其实可以考虑,只要响应链里面没有UIScrollView或者拖动手势之类的,相对于屏幕的位置大概率就不会改变了,不过依然有直接操作frame的可能。
元素大小不适合考虑在内,有一种方法是对大小进行较大力度的四舍五入,这样把后期可能的微调也兼容了,把iOS和安卓两端可能的微小差异也抹平了。 但是有个致命的问题是,当面对相对布局时,元素大小就不是固定的了。
Xcode提供的UITest框架是允许用户录制操作流程的,详见我之前的一篇博文。
在研究元素唯一标识的时候,我特别想知道Apple在录制操作时是如何定义每一个元素的,经过测试发现,在对于UIButton的定义上,我们的思路跟Apple的不谋而合。 而在UIButton以外的情况下,Apple就显得很笨拙了,几乎识别不出来。
如果元素的响应链上有UIScrollView类型的superview:
- 如果是UIScrollView,取cell的frame。
- 如果是UITableView,取cell的indexPath。
- 如果是UICollectionView,取cell的indexPath。
这个思路目前是为了完美实现操作回放设计的,但是太区分列表中的item,不利于操作检测或者稳定性?
又一道分隔线
最初在设计这套方案的时候,就是为了实现自动埋点
、操作回放
、操作检测
这些技术构想,我们称之为设计驱动迭代。
而在真正实践的过程中,又遇到了很多很多问题,我们结合问题和需求场景,再次反复推敲验证,最终产出了系统性的操作行为标识指令方案,可以用一个很直观的公式展示:
操作行为标识 = 动作信息(vm) + 响应链信息(vp) + 列表信息(vl) + 区位信息(vq) + 参考信息(vr) + 功能信息(vf)
公式中的每个因子都是在反复思考实践后确定的,详细思路可以关注我们的开源代码。
这个过程我们就称为迭代反馈设计吧。
这套方案已经比较好的支撑了前面介绍的棱镜全平台的能力建设,可以说是经受住了生产环境的考验。
但我们还有未解的问题:
- 低概率的标识指令变化问题如何解决?
- iOS与Android两端的标识指令如何打通?
这些问题我们都在尝试解决,也都有一些还算可靠的思路了。
所以最后发个招聘帖吧,欢迎感兴趣的同学跟我们一起创造,感谢阅读!