背景
用户行为埋点是用来记录用户在操作时的一系列行为, 也是业务做判断的核心数据依据, 如果缺失或者不准确将会给业务带来不可恢复的损失. 闲鱼将业务代码从 Native 迁移到 Flutter 上过程中, 发现原先 Native 体系上的埋点方案无法应用在 Flutter 体系之上. 而如果我们只把业务功能迁移过来就上线, 对业务是极其不负责任的. 因此, 经过不断探索, 我们沉淀了一套 Flutter 上的高准确率的用户行为埋点方案.
用户行为埋点定位
先来讲讲在我们这里是如何定义用户行为埋点的. 在如下用户时间轴上, 用户进入 A 页面后, 看到了按钮 X, 然后点击了这个按钮, 随即打开了新的页面 B.
这个时间轴上有如下 5 个埋点事件发生:
进入 A 页面. A 页面首帧渲染完毕, 并获得了焦点.
曝光坑位 X. 按钮 X 处于手机屏幕内, 且停留一段时间, 让用户可见可触摸.
点击坑位 X. 用户对按钮 X 的内容很感兴趣, 于是点击了它. 按钮 X 响应点击, 然后需要打开一个新页面.
离开 A 页面. A 页面失去焦点.
进入 B 页面. B 页面首帧渲染完毕, 并获得焦点.
在这里, 打埋点最重要的是时机, 即在什么时机下的事件中触发什么埋点, 下面来看看闲鱼在 Flutter 上的实现方案.
实现方案
进入 / 离开页面
在 Native 原生开发中, Android 端是监听 Activity 的 onResume 和 onPause 事件来做为页面的进入和离开事件, 同理 iOS 端是监听 UIViewController 的 viewWillAppear 和 viewDidDisappear 事件来做为页面的进入和离开事件. 同时整个页面栈是由 Android 和 iOS 操作系统来维护.
在 Flutter 中, Android 和 iOS 端分别是用 FlutterActivity 和 FlutterViewController 来做为容器承载 Flutter 的页面, 通过这个容器可以在一个 Native 的页面内来进行 Flutter 页面的切换, 即 Flutter 自己维护了一个 Flutter 页面的页面栈. 这样, 原来我们最熟悉的那套在 Native 原生上的方案在 Flutter 上无法直接运作起来.
针对这个问题, 可能很多人会想到去注册监听 Flutter 的 NavigatorObserver, 这样就知道 Flutter 页面的进栈 (push) 和出栈 (pop) 事件. 但是这会有两个问题:
假设 A,B 两个页面先后进栈 (A enter -> A leave -> B enter). 然后 B 页面返回退出(B leave), 此时 A 页面重新可见, 但是此时是收不到 A 页面 push(A enter) 的事件.
假设在 A 页面弹出一个 Dialog 或者 BottomSheet, 而这两类也会走 push 操作, 但实际上 A 页面并未离开.
好在 Flutter 的页面栈不像 Android Native 的页面栈那么复杂, 所以针对第一个问题, 我们可以维护一个和页面栈匹配的索引列表. 当收到 A 页面的 push 事件时, 往队列里塞入 A 的索引. 当收到 B 页面的 push 事件时, 检测列表内是否有页面, 如有, 则对列表最后一个页面执行离开页面事件, 再对 B 页面执行进入页面事件, 接着往队列里塞 B 的索引. 当收到 B 页面的 pop 事件时, 先对 B 页面执行离开页面事件记录, 再对队列里存在的最后一个索引对应的页面 (假设为 A) 进行判断是否在栈顶(ModalRoute.of(context).isCurrent), 如果是, 则对 A 页面执行进入页面事件.
针对第二个问题, Route 类内有个成员变量 overlayEntries, 可以获取当前 Route 对应的所有图层 OverlayEntry, 在 OverlayEntry 对象中有个成员变量 opaque 可以判断当前这个图层是否全屏覆盖, 从而可以排除 Dialog 和 BottomSheet 这种类型. 再结合问题 1, 还需要在上述方案中加上对 push 进来的新页面来做判断是否为一个有效页面. 如果是有效页面, 才对索引列表中前一个页面做离开页面事件, 且将有效页面加到索引列表中. 如果不是有效页面, 则不操作索引列表.
以上并不是闲鱼的方案, 只是笔者给出的一个建议. 因为闲鱼 App 在一开始落地 Flutter 框架时, 就没有使用 Flutter 原生的页面栈管理方案, 而是采用了 Native+Flutter 混合开发的方案. 具体可参考前面的一篇文章 《已开源 | 码上用它开始 Flutter 混合开发 --FlutterBoost》 . 因此接下来也是基于此来阐述闲鱼的方案.
闲鱼的方案如下(以 Android 为例, iOS 同理):
注: 首次打开指的是基于混合栈新打开一个页面, 非首次打开指的是通过回退页面的方式, 在后台的页面再次到前台可见.
看到这个方案可能会有人问, 为什么这么绕, 为什么不全部交给 Native 侧去直接管理呢? 交给 Native 侧去直接管理这样做针对非首次打开这个场景是合适的, 但是对首次打开这个场景却是不合适的. 但是在首次打开这个场景下, onResume 时 Flutter 页面尚未初始化, 此时还不知道页面信息, 因此也就不知道进入了什么页面, 所以需要在 Flutter 页面初始化 (init) 时再回过来调 Native 侧的进入页面埋点接口. 而为了避免开发人员去关注是否为首次打开 Flutter 页面, 因此我们统一在 Flutter 侧来直接触发进入 / 离开页面事件.
曝光坑位
先讲下曝光坑位在我们这里的定义, 我们认为图片和文本是有曝光意义的, 其他用户看不见的是没有曝光意义的, 在此之上, 当一个坑位同时满足以下两点时才会被认为是一次有效曝光:
坑位在屏幕可见区域中的面积大于等于坑位整体面积的一半.
坑位在屏幕可见区域中停留超过 500ms.
基于此定义, 我们可以很快得出如下图所示的场景, 在一个可以滚动的页面上有 A,B,C,D 共 4 个坑位. 其中:
坑位 A 已经滑出了屏幕可见区域, 即 invisible;
坑位 B 即将向上从屏幕中可见区域滑出, 即 visible->invisible;
坑位 C 还在屏幕中央可视区域内, 即 visible;
坑位 D 即将滑入屏幕中可见区域, invisible->visible;
那么我们的问题就是如何算出坑位在屏幕内曝光面积的比例. 要算出这个值, 需要知道以下几个数值:
容器相对屏幕的偏移量
坑位相对容器的偏移量
坑位的位置和宽高
容器的位置和宽高
其中坑位和容器的宽和高很容易获取和计算, 这里就不再累述.
获得容器相对屏幕的偏移量
获得坑位相对屏幕的偏移量
逻辑判断
点击坑位
点击坑位埋点没什么难点, 很容易就可以想到下面的方案:
效果
经过多轮迭代和优化, 目前线上 Flutter 页面的埋点准确率已经达到 100%, 有力地支持了业务的分析和判断. 同时这套方案让业务同学在做开发时, 对于页面进入 / 离开, 曝光坑位可以做到无感知, 即不用关心何时去触发, 做到了简单易用和无侵入性.
未来
此外, 针对页面进入 / 离开这个场景, 由于闲鱼是基于 Flutter Boost 混合栈的方案, 因此我们的解决方案还不够通用. 不过未来随着闲鱼上的 Flutter 页面越来越多, 我们后续也会去实现基于 Flutter 原生的方案.
来源: http://www.tuicool.com/articles/ZJBRriy