关注前沿技术,分享热点话题,QCon全球软件开发大会三站同启,重磅回归!立即查看 了解详情

从 0 到 1 搭建技术中台之 iOS 可视化埋点实践

2020 年 9 月 12 日

从 0 到 1 搭建技术中台之 iOS 可视化埋点实践

自去年开始,中台话题的热度不减,很多公司都投入到中台的建设中,从战略制定、组织架构调整、协作方式变动到技术落地实践,每个环节都可能出现各种各样的问题。技术中台最坏的状况是技术能力太差,不能支撑业务的发展,其次是技术脱离业务,不能服务业务的发展。前者是能力问题,后者是意识问题。在本专题中,伴鱼技术团队分享了从 0 到 1 搭建技术中台的过程及心得。

前言

可视化埋点,也称圈选埋点,是建立在全埋点技术基础上的一种数据埋点机制。通过全埋点技术,尽可能地将用户的所有交互行为进行采集上报,然后通过可视化圈选的方式筛选出感兴趣的行为统计数据,为产品运营提供决策支持。可视化埋点具有“全面、便捷、低技术门槛”的特点,能够有效降低研发、运营成本,是对传统代码埋点技术的有力补充。

本文结合伴鱼 iOS 端在圈选埋点技术上的一些实践经验,对圈选埋点方案的设计和实现进行探讨。

总体思路

从数据采集到生成统计报表,一般需要经过三个步骤,如下图所示

main frame

1. 用户行为数据采集:通过全埋点技术采集用户行为事件;

2. 圈选配置匹配规则:由产品或运营人员通过可视化圈选工具,对感兴趣的用户行为事件进行标定,生成事件匹配规则,并上传到服务端;

3. 匹配计算生成报表:数据研发人员根据已配置的事件匹配规则进行数据统计,生成报表

这里采用全埋点的方式采集用户行为数据,会增加 App 端数据流量和服务端数据存储压力。选择该方案的理由参见 4.2 前后端配合方式的选择 。

事件标识定义

全埋点采集用户行为,需要解决的最大问题是:如何精确描述行为事件。通常对页面和页面中的可交互元素分别进行定义。

A. 页面标识

页面标识通常采用 2 种方式来标定:

1. 页面路径:从 Window 的根控制器开始直到页面所在视图控制器的路径。例如

复制代码
UITabBarController-UINavigationController(1)-MyViewController(2)

括号中的数字代表控制器在父控制器中的索引。

2. 页面类名: 直接已控制器的类名作为页面标识。被 Presented 的控制器也适用于该方式。

例外情况

a. 页面所属控制器存在自定义的父控制器

例如:一个控制器包含了若干子控制器,且通过 UIScrollView 分页的方式呈现各子控制器的视图。对于此类控制器,无法通过 hook viewDidAppear: 的方式来记录 PV。

解决办法:通过 UIScrollViewDelegate 的 scrollViewDidScroll: 和 scrollViewDidEndScrollingAnimation: 方法来监听 UIScrollView 的内容偏移事件,根据 contentOffset 计算当前显示的视图属于哪一个控制器,最后手动触发控制器的 viewDidAppear: 方法。

b. 一些页面需要避免被采集

一些用于调试的页面,或经产品确认不参与采集的页面,通过下发 ignore list 的方式来过滤。

B. 元素标识

理论上,页面中所有可交互的元素都应该能够被采集到。但考虑到 App 交互的多样性和现实成本,这里仅讨论支持点击操作的元素。

通常,元素标识由三部分组成

1. 元素在页面视图树中的路径

路径由视图树根节点开始,到该元素节点的父节点为止。例如:假设视图中有一个按钮控件,那么它的路径可以表示成如下形式:

复制代码
UIWindow-UITransitionView-UIDropShadowView-UILayoutContainerView-UITransitionView-
UIViewControllerWrapperView-UILayoutContainerView-UINavigationTransitionView-
UIViewControllerWrapperView-UIView

2. 元素的类型名称 + 索引

以上述按钮为例:它的类型名为 UIButton,索引为其在父视图中的添加顺位。

3. 元素的内容

元素的内容可能是文本、图片、其他包含图片或文字的子元素组合。类似于 UILabel、UIImageView 这样的元素,直接获取其文本信息或图片 URL 即可。对于 UIButton,获取其 currentTitle 文本或 UIControlStateNormal 状态下的图片 URL。文本内容优先于图片内容。

对于图文并存,或包含子元素组合的元素,需要根据元素类型及圈选方式确定,且元素内容标识需要单独生成。在 元素内容 一节中有详细介绍。

上述按钮的完整标识可以表示如下:

复制代码
UIWindow-UITransitionView-UIDropShadowView-UILayoutContainerView-UITransitionView-
UIViewControllerWrapperView-UILayoutContainerView-UINavigationTransitionView-
UIViewControllerWrapperView-UIView-UIButton(0)_[click me]

UIButton 后面小括号中的数字”0”表示其在父视图中的索引,中括号内的 “click me” 来自其 currentTitle 的值。

元素索引的添加时机

建议只从视图控制器所在的视图开始添加元素索引。系统内置的视图,如 UITransitionView 会在运行时修改其子元素的索引,造成元素路径发生变化,因此在进行路径追溯时,到达 UIViewController (注意不含 UITabBarController 和 UINavigationController) 就不再添加索引。

独立元素与可重复元素的路径

独立元素是指在视图中独立绘制的元素,通常与其他元素无关联。对于此类型元素,标识定义为:”路径”“类型 + 索引”[“内容”]。

可重复元素是指在列表中绘制的元素。在 iOS 中只考虑 UITableViewCell 和 UICollectionReusableView。通过元素在父视图中的 indexPath 来确定元素的索引,即 (indexPath.section-indexPath.row),那么可重复元素的路径可以定义为:

复制代码
... UIView-UITableView(0)-UITableViewCell(indexPath.section-indexPath.row)

元素内容

我们将元素内容的分为图片和文本两类。文本类内容可以从控件的 text、title 等属性获取,这里不再赘述。图片内容的获取,有 2 种方式:

通过 imageNamed: 方法设置的图片,通过 description 方法打印其信息,可以得到类似如下结果:

复制代码
<UIImage:0x6000005de2e0 named(main: home_search_icon) {24, 24}>

这里的 “main: home_search_icon” 表示图片名称为 “home_search_icon”,来自 “main bundle”。我们可以截取 “main: home_search_icon” 作为图片内容。

如果通过 description 方法打印的信息如下:

复制代码
<UIImage:0x6000005a1f80 anonymous {75, 75}>

这说明图片是通过其他方式进行设置的,需要通过第二种方式来获取其内容。

  • 通过 SDWebImage 等三方库设置了 UIImageView 的 URL,可以直接在运行时获得其关联属性
复制代码
NSString *imageContent;
SEL sel = NSSelectorFromString(@"sd_imageURL");
if ([self respondsToSelector:sel]) {
NSURL *url = [self performSelector:sel];
if ([url isKindOfClass:[NSURL class]])
{
imageContent = url.absoluteString;
}
}

单一内容、复合内容以及内容标识

如果一个元素只含有一项文本或图片,则称这个元素的内容为单一内容。单一内容本身作为其内容标识。

如果一个元素包含多个文本或图片、或其子元素内也包含文本或图片,则称其内容为复合内容。我们对复合内容进行遍历,遍历结果按键值对保存:

复制代码
{
"UIView-UILabel(0)": "text 1",
"UIView-UIImageView(1)": "main: search_icon",
}

其中,key 对应的是子元素相对路径,作为改内容的内容标识,即从当前元素到子元素的路径,value 对应的是该内容具体的文本或图片内容。

对于具有复合内容的元素,有时会对其中某一项内容进行统计,该内容的内容标识可以参与到事件匹配。

考虑到性能影响,一个元素的内容遍历深度一般不超过 5。

事件匹配规则

我们通过定义事件匹配规则来对事件进行过滤,符合匹配规则的事件被认为是需要进行统计的。匹配规则实质上是对页面标识、元素标识、元素内容定义的一系列正则表达式。将用户行为相关的页面、元素标识、元素内容与事先定义的正则表达式进行匹配,匹配成功则进行统计。

正则表达式符号定义:

为了简化正则表达式的书写,我们将正则表达式中需要精确匹配的字符串进行如下约定:

  • fixedPrefix:表示固定的前缀字符,元素的路径需要精确匹配
  • fixedSuffix:表示固定的后缀字符,元素的索引或其他需要精确匹配的字符
  • fixedStr:表示固定的完整字符,元素的标识或内容需要精确匹配
  • fixedSection:在可重复元素中表示固定的 section,可重复元素的 section 索引需要精确匹配
  • fixedRow:在可重复元素中表示固定的 row,可重复元素的 row 索引需要精确匹配

假设我们要采集一个元素的标识为

复制代码
UIWindow-UITransitionView-UIDropShadowView-UILayoutContainerView-UITransitionView-
UIViewControllerWrapperView-UILayoutContainerView-UINavigationTransitionView-
UIViewControllerWrapperView-UIView-UITableView-UITableViewCell(1-2)_[null]

根据上述约定,我们可以定义如下正则表达式来采集该元素:

复制代码
^UIWindow-UITransitionView-UIDropShadowView-UILayoutContainerView-UITransitionView-
UIViewControllerWrapperView-UILayoutContainerView-UINavigationTransitionView-
UIViewControllerWrapperView-UIView-UITableView-UITableViewCell(1-2)_[[\S|\s]+\]
用 fixedPrefix 表示”UIWindow-UITransitionView-UIDropShadowView-UILayoutContainerView-UITransitionView-
UIViewControllerWrapperView-UILayoutContainerView-UINavigationTransitionView-
UIViewControllerWrapperView-UIView-UITableView-UITableViewCell

用 (fixedSection-fixedRow) 表示 “(1-2)”

用 fixedSuffix 表示 “_”

可以得到简化后的正则表达式

复制代码
^fixedPrefix\(fixedSection-fixedRow\)fixedSuffix\[[\S|\s]+\]$

该规则表示:匹配某个视图列表中 section 为 1,row 为 2 的元素,不关注内容变化。

基于正则表达式的事件匹配规则

  • 页面匹配规则

根据页面标识进行精确匹配即可。

  • 独立元素匹配规则

    • 当前位置
      关注元素的绝对位置,不关注元素内容。如果元素位置发生变化,则不纳入统计。元素标识匹配正则表达式为:^fixedPrefix[[\S|\s]+]$。
    • 当前内容
      关注元素的内容,允许元素在其父视图内的索引发生变化。如果元素内容发生变化,则不纳入统计。单一内容的元素标识匹配正则表达式为:^fixedPrefix(\d*)fixedSuffixfixedSuffixfixedPrefix(\d)[[§|\s]+],且内容标识正则表达式为:^fixedStr$。
  • 可重复元素匹配规则

    • 不关注内容
      • 同类元素
        关注列表中同一 section 内的所有元素。当用户点击任一元素时产生的事件都会纳入统计。元素标识匹配正则表达式为:^fixedPrefix(fixedSection-\d*)fixedSuffix[[\S|\s]+]$。
      • 当前位置
        只关注列表中固定位置的某个元素。只有当用户点击该元素时产生的事件才会纳入统计。元素标识匹配正则表达式为:^fixedPrefix(fixedSection-fixedRow)fixedSuffix$。
    • 关注内容
      • 同类元素
        关注列表中同一 section 内的所有元素,且对指定内容进行聚合统计。元素标识匹配正则表达式与不关注内容的表达式一致:^fixedPrefix(fixedSection-\d*)fixedSuffix[[\S|\s]+]:fixedPrefix[[§|\s]+]
      • 当前位置
        只关注列表中固定位置的某个元素。只有当用户点击该元素时产生的事件才会纳入统计,并且对当前位置元素的指定内容进行统计聚合。元素标识匹配正则表达式为:^fixedPrefix(fixedSection-fixedRow)fixedSuffixfixedPrefix[[§|\s]+] 。该规则适用这样的场景:运营人员想查看列表指定元素的内容对点击率的影响。
      • 当前内容
        只关注列表中固定位置的某个元素,且该元素的某项内容不能发生改变。位置和内容任意一项发生变化,则不纳入统计。元素标识匹配正则表达式为:^fixedPrefix(fixedSection-fixedRow)fixedSuffixfixedPrefix
  • 元素、内容匹配规则表

前后端配合方式的选择

前端匹配

  • 工作方式

    • 圈选配置由服务端统一下发到 App
    • App 根据圈选配置进行匹配采集,将采集到的用户事件上报服务端
    • 服务端进行数据统计处理,生成报表
  • 优点

    • App 只上报被圈选匹配的事件,上报数据量小
    • 服务端只负责圈选配置的下发同步,实现较为简单
  • 缺点

    • 数据统计具有滞后性,依赖圈选配置下发的覆盖程度
    • 无法追溯历史,即无法统计圈选配置生效前发生的事件
    • App 端需要考虑匹配计算对性能的影响

后端匹配

  • 工作方式

    • App 全量采集用户行为事件
    • 服务端根据圈选配置,结合全量采集的事件进行匹配过滤
  • 优点

    • 可以支持实时统计
    • 可追溯历史,即可以统计圈选配置生效前的历史数据
    • App 不做匹配过滤,对性能无影响
  • 缺点

    • App 全量采集的数据量大,需考虑对用户流量的影响
    • 服务端做匹配过滤工作涉及的计算量较大
    • 服务端存储全量采集数据涉及到的存储空间较大

伴鱼的选择

  • 尽可能不影响用户体验。如果匹配规则较多,在端上做匹配过滤势必会对性能有所影响。因此仅在提供了圈选配置功能的 App 上支持前端匹配功能。
  • 实时性、可追溯这两个特性,对于产品和运用来说异常重要,不能妥协。
  • 全埋点采集的数据对于用户流量的影响并不高。根据伴鱼绘本的经验,单个用户平均一天产生的行为数据不超过 5M,相当于上传了一张高清图片。
  • 服务端的存储资源可以定期清理。
  • 服务端的计算资源的问题可以通过弹性扩容的方式解决。

圈选验证

产品或运营完成圈选配置后,需要验证圈选是否生效。App 可以通过集成圈选 SDK 来实现所见即所得的验证方式。如下图所示,符合匹配规则的页面和元素会以不同颜色高亮显示。

元素标识发生变化导致匹配规则失效时如何处理?

无论何种原因导致元素的路径或内容发生变化,最终会使得元素事件无法被事先配置的圈选规则匹配。有 2 种典型场景:

  • 产品需求迭代过程中的页面改版导致元素路径或内容发生了变更。在这种场景下,旧的圈选配置仍然生效,只需在新版本下手工增加新的圈选规则即可。 服务端进行数据统计时,可以按照 App 的版本进行区分。当 App 迭代到一定程度,较早前版本的圈选配置将失效。服务端可以定期对老版本的圈选配置进行清理。

  • App 在运行过程中,因业务条件发生变化导致页面布局或元素内容发生变更。例如,某些页面对于付费用户和非付费用户展示不同的布局效果。这其实和上述场景类似,需要在所有可能的用户场景下分别进行圈选配置操作。

某些元素的父视图层级固定,只是索引会发生变化,例如导航栏右上角的下拉菜单列表,列表中的元素顺序可能会变化,但都限定在菜单容器内。对于这种元素,我们可以在生成圈选配置时,限定元素的文本内容。只要内容不变,仍然能够被匹配命中。

总而言之,如果导致元素的标识变化的场景是可以被枚举的,我们只需枚举所有感兴趣的场景,然后分别进行圈选埋点;如果元素的视图层级固定,仅索引会变,我们可以根据元素内容进行限定,只匹配特定内容的元素;其他情况下建议直接使用代码埋点。

总结

本文尝试阐述这样一种理念:通过全埋点的方式采集用户行为,通过正则匹配的方式构建统计规则,最终为产品决策提供数据支持。圈选埋点技术有效地提高了研发效率,让产品和运营能够更直观便捷地定义指标;但对于复杂的业务场景,代码埋点仍然不可或缺。

参考:

GrowingIO JavaScript SDK 的实现细节和扩展
网易 HubbleData 无埋点 SDK 在 iOS 端的设计与实现

2020 年 9 月 12 日 10:00 1801

评论

发布
暂无评论
发现更多内容

【面试必问】Spring中的事务管理详解

只喝纯牛奶

敏捷教练和Scrum Master - 敏捷转型中的两个重要角色的对比

Bob Jiang

Scrum 敏捷教练 ScrumMaster

JVM系列:通过一个例子分析JIT的汇编代码

简爱W

架构师训练营第九周学习总结

张明森

微碳系:我心中的宇宙

Geek_116789

Go: 并发访问 Map — Part III

陈思敏捷

go golang 并发 map sync

格一格你的情欲念

王进行

30岁的二三事

大唐小生

总结 个人感悟

今天你内卷了吗?

池建强

个人成长 内卷化

JVM详解之:类的加载链接和初始化

程序那些事

Java JVM GC 加载

密码朋克的社会实验(三):比特币发明了什么

腾讯安全云鼎实验室

比特币 区块链 密码学

来了来了!Docker安装及运行原理

程序员的时光

Docker 微服务 Java web

小伙伴想写个 IDEA 插件么?这些 API 了解一下!

liuzhihang

IDEA idea插件 教程 API IntelliJ IDEA

搭乘政策红利“快车” 欧科云链助力区块链人才培养

CECBC区块链专委会

金融行业区块链技术应用有了“安全符”

CECBC区块链专委会

什么是算法的大O表示法

码农神说

算法 时间复杂度 Java算法 大O

这届 Showgirl行不行?AI告诉你谁是ChinaJoy上最漂亮的小姐姐

华为云开发者社区

人工智能 人脸识别 图像识别 展览会论坛会 华为云

数据结构与算法之排序

shirley

排序算法

我收集的 3 个企业经营“失败”案例

泰稳@极客邦科技

这16道Redis最常见面试问题,你能回答上来几个?

火羊哥

java\

新生必备清单:不想成为虚度青春的“小透明”,手机应该怎样选?

脑极体

如何进行需求梳理及埋点方案设计

易观大数据

职场求生攻略答疑篇之 2 —— 无所适从的向上沟通

臧萌

JVM参数手册

Rayjun

JVM GC

“PlusToken”跨国网络传销案告破,涉案400亿元!

CECBC区块链专委会

Java七种排序算法以及实现

狸猫换太子

Java 排序算法 实现

dubbo-go 中使用 sentinel

apache/dubbo-go

golang dubbo sentinel

最牛逼的Java框架,没有之一

我是苞谷

别在网上乱找代码了,找了一段代码突然爆了!!!

导导

java\

机器学习基石第五节 学习笔记

半亩房顶

Machine Learning

重学JavaScript01:就从面向对象说起吧

张理查rootv

从 0 到 1 搭建技术中台之 iOS 可视化埋点实践-InfoQ