写点什么

如何设计高准确率的 Flutter 埋点框架

2019 年 6 月 27 日

如何设计高准确率的Flutter埋点框架

背景


用户行为埋点是用来记录用户在操作时的一系列行为,也是业务做判断的核心数据依据,如果缺失或者不准确将会给业务带来不可恢复的损失。闲鱼将业务代码从 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;



那么我们的问题就是如何算出坑位在屏幕内曝光面积的比例。要算出这个值,需要知道以下几个数值:


  • 容器相对屏幕的偏移量

  • 坑位相对容器的偏移量

  • 坑位的位置和宽高

  • 容器的位置和宽高


其中坑位和容器的宽和高很容易获取和计算,这里就不再累述。


获得容器相对屏幕的偏移量


//监听容器滚动,得到容器的偏移量double _scrollContainerOffset = scrollNotification.metrics.pixels;
复制代码


获得坑位相对屏幕的偏移量


//曝光坑位Widget的contextfinal RenderObject childRenderObject = context.findRenderObject();final RenderAbstractViewport viewport = RenderAbstractViewport.of(childRenderObject);if (viewport == null) {  return;}if (!childRenderObject.attached) {  return;}//曝光坑位在容器内的偏移量final RevealedOffset offsetToRevealTop = viewport.getOffsetToReveal(childRenderObject, 0.0);
复制代码


逻辑判断


if (当前坑位是invisible && 曝光比例 >= 0.5) {  记录当前坑位是visible状态  记录出现时间} else if (当前坑位是visible && 曝光比例 < 0.5) {  记录当前坑位是invisible状态  if (当前时间-出现时间 > 500ms) {    调用曝光埋点接口  }}
复制代码


点击坑位


点击坑位埋点没什么难点,很容易就可以想到下面的方案:



效果


经过多轮迭代和优化,目前线上 Flutter 页面的埋点准确率已经达到 100%,有力地支持了业务的分析和判断。同时这套方案让业务同学在做开发时,对于页面进入/离开、曝光坑位可以做到无感知,即不用关心何时去触发,做到了简单易用和无侵入性。


未来


此外,针对页面进入/离开这个场景,由于闲鱼是基于 Flutter Boost 混合栈的方案,因此我们的解决方案还不够通用。不过未来随着闲鱼上的 Flutter 页面越来越多,我们后续也会去实现基于 Flutter 原生的方案。


本文转载自公众号闲鱼技术(ID:XYtech_Alibaba)


原文链接


https://mp.weixin.qq.com/s/CMYi-f0-6nwZ4ZyV5K_lKA


2019 年 6 月 27 日 08:004385

评论

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

实例学习绘画UML图

张瑞浩

作业1:食堂就餐卡系统设计(UML)

蒜泥精英

第一课 架构师的自我修养

Geek_bobo

食堂就餐卡系统架构设计

~就这样~

【第一周】学习总结——架构方法、软件建模与设计文档

三尾鱼

极客大学架构师训练营

就餐卡设计文档

chengjing

作业2-学习心得

蒜泥精英

编译运行Zookeeper源码

CoderLi

Java zookeeper 程序员 源码分析 后端

架构师训练营第0期-第1周-作业一

极客大学架构师训练营

食堂就餐卡系统设计

八两

就餐卡系统(时间太紧张,阅读了很多,我转载的这篇)

王锟

第一次课作业

lai

什么是架构师?

呆呆栋

架构师训练营 Week 01 学习总结

Kun

极客大学架构师训练营

Lesson 1 架构师如何做架构 心得笔记

edd

编程好习惯 极客大学架构师训练营 架构总结

食堂就餐卡系统设计(第一周)

架构师训练营-食堂就餐卡系统设计

彭灵俊

极客大学架构师训练营

架构师训练营-学习总结

~就这样~

聊聊架构师

Jerry Tse

随笔杂谈 极客大学架构师训练营 作业

作业一:食堂就餐卡系统设计

独孤魂

极客大学架构师训练营

就餐卡系统架构设计

祝好

关于架构师这个角色的感悟

祝好

第一节课的总结

王锟

架构方法学习小结

行下一首歌

极客大学架构师训练营

架构师训练营第01周——UML练习

李伟

极客大学架构师训练营

就餐卡系统设计

平淡人生

极客大学架构师训练营

食堂就餐卡系统设计

Acker飏

极客大学架构师训练营

架构文档编写

金桔🍊

食堂就餐卡系统架构视图

行下一首歌

极客大学架构师训练营

架构师训练营第0期-第1周-作业二

极客大学架构师训练营

什么叫架构师

平淡人生

极客大学架构师训练营

NLP领域的2020年大事记及2021展望

NLP领域的2020年大事记及2021展望

如何设计高准确率的Flutter埋点框架-InfoQ