工程实践 | 在 Flutter 中实现一个精准的滑动埋点

发表于 3年以前  | 总阅读数:3210 次

前言

今天的这篇文章要介绍的内容,是我们经常会用到的一个场景:埋点。为了对行为特征的数据进行量化分析、优化产品, 我们常常需要在特定的时机上报数据埋点, 而曝光埋点则是其中的一个高频使用场景。

滑动埋点的痛

在 Flutter 中,我们通常会在 initState 这个生命周期上报曝光埋点,这在一般的使用场景下当然是没有问题的。然而在滑动场景下这个解决方案就不 work 了,我们来看看。

很明显,我们把本来没有展示的 widget 也给打印出来了。如果这样做,埋点上报不准确,将会给业务带来不可恢复的损失。

ScrollView 加载机制

为什么会出现这种情况呢?在查阅了源码之后,我们发现所有的 ScrollView 都是在一个可视区域 Viewport 当中进行绘制,为了让滑动更加流畅,通常 ScrollView 都会在可视区域之外加载一部分,也就是 cacheExtent。落入该缓存区域的项目即使在屏幕上尚不可见,也会进行布局。这时候 initState 就被执行了。ListView 作为 ScrollView 的子类同样也使用了这个机制。

那么很自然我们能够想到一个最简单的解决方案:把预加载机制给禁用掉不就可以了嘛:

ListView.builder(
  cacheExtent: 0,
  itemCount: 40,
  itemBuilder: (context, index) {
    return Item(index: index);
  },
),

好了,本文到此结束,你学会了吗。

新的问题

开个玩笑,相信大家很容易就能够联想到,这样做大概率会产生性能问题。在我们真实业务中,会考虑到支持的最差的设备性能,以及业务的复杂性,肯定不是这样简单的取消掉预加载就能够解决的。

在做测试的时候,会发现如果去掉缓存机制,平均帧率会下降 5-10 帧左右, 还是在比较好的一加手机上的测试结果,这当然是不能接受的。(更何况本身在 1.x 版本 的 Flutter 下 ListView 性能就有一些问题)

所以我们想要的是一套 Flutter 上的高准确率的用户行为埋点方案,而且不要影响到 ScrollView 的性能。

破局

想清楚了需求,就有了一半的思路。在我们查阅了业界现有的资料后,发现闲鱼技术已经分享了一个比较好的解题思路:揭秘!一个高准确率的 Flutter 埋点框架如何设计[1]。奈何这个方案也没有开源的计划,那就只有自己来写一个吧。这个问题应该如何解呢?

在前面我们提到过,每一个 ScrollView 都会有一个自己的 Viewport 来决定自己的绘制范围,这个 Viewport 最后会生成一个 RenderObjectElement,这样就可以单独渲染这个区域,把影响返回控制到最小。那么问题现在就变成了我们想要计算一个 Item 什么时候进入到 Viewport 中。

一个复杂的问题需要把它抽象成更简单的问题然后逐步求解。我们不妨先把 Item 看成一个点,看看要计算一个 Item 是否在 Viewport 内需要哪些信息。

很容易能够想到和滑动的偏移量 (Scroll Offset),以及 Viewport 在滑动方向上的长度 (Viewport Length), 还有 item 自身的信息,也就是当前 item 距离滑动起始点的距离 (Exposure Offset) 相关。

想象一下滑动的样子,一个 Item 从 ViewPort 的右边滑入,进入 ViewPort,被用户看到,然后再从 ViewPort 的左边划出,这一系列过程。我们可以把这个过程抽象为下面的四个状态:

  • Item 在 ViewPort 右侧不可视范围内:(Scroll Offset + ViewPort Length < Exposure Offset)
  • Item 进入 ViewPort 右侧:(Scroll Offset + ViewPort Length > Exposure Offset)
  • Item 在 ViewPort 中
  • Item 在 ViewPort 左侧不可视范围内:(Exposure Offset < Scroll Offset)

对于从左边划入右边则是这几个状态:

  • Item 在 ViewPort 左侧不可视范围内:(Exposure Offset < Scroll Offset)
  • Item 进入 ViewPort 左侧:(Exposure Offset > Scroll Offset)
  • Item 在 ViewPort 中
  • Item 在 ViewPort 右侧不可视范围内:(Scroll Offset + ViewPort Length < Exposure Offset)

通过观察可以发现,Item 从左边划入和从右边划入它的判断时机是不一样的,所以我们需要区分两种滑动情况。

下面我们把 Item 自身的宽度 (Item Width)也带上,再使用上面得出的结论来进行计算。

我们这里暂时认为 Item 完全划入 ViewPort 才算一次曝光。

  • Item 在 ViewPort 右侧不可视范围内:(Scroll Offset + ViewPort Length < Exposure Offset)
  • Item 进入 ViewPort 右侧:(Scroll Offset + ViewPort Length > Exposure Offset)
  • Item 在 ViewPort 中
  • Item 在 ViewPort 左侧不可视范围内:(Exposure Offset + Item Width < Scroll Offset)

对于从左边划入右边则是这几个状态:

  • Item 在 ViewPort 左侧不可视范围内:(Exposure Offset + Item Width < Scroll Offset)
  • Item 进入 ViewPort 左侧:(Exposure Offset + Item Width > Scroll Offset)
  • Item 在 ViewPort 中
  • Item 在 ViewPort 右侧不可视范围内:(Scroll Offset + ViewPort Length < Exposure Offset)

如何获取这些信息

知道了解法之后,接下来就只需要寻找这些拼图的碎片就行了。

Item 大小信息

这块比较简单,我们都知道可以通过 Widget 的 BuildContext 拿到它所对应的 RenderObject,通过它去拿当前 Item 的长度和宽度。

// 这里命名为曝光坑位的大小,对于不同滑动方向,我们需要用不同方向的长度。
final exposurePitSize = (context.findRenderObject() as RenderBox).size;

这里的 context 是我们想要判断是否曝光的 Item 的 context,如果你对这个概念还不太清楚,可以去看看这篇 深入理解 BuildContext[2]。

注意:不是每个 Widget 都会创建一个 RenderObject,只有 RenderObjectWidget 才会创建 RenderObjectListView 会默认帮每一个 Item 添加一个 RepaintBoundary,这个 Widget 是一个 SingleChildRenderObjectWidget,所以每一个 Item 其实都会有一个它所对应的 RenderObject

// SliverChildListDelegate 的 build 方法
if (addRepaintBoundaries) child = RepaintBoundary(child: child);

ViewPort 大小信息

我们在进行曝光判断的时候,肯定是在每一个 Item 中进行的,而 ViewPort 则是存在于 ListView 这一层级,所以我们需要从祖先的节点中找到它,幸运的是,Flutter 已经为我们提供了这个方法。

static RenderAbstractViewport? of(RenderObject? object) {
  while (object != null) {
    if (object is RenderAbstractViewport)
      return object;
    object = object.parent as RenderObject?;
  }
  return null;
}

我们刚刚已经拿到了 Item 对应的渲染对象,RenderAbstractViewport.of 可以通过这个 RenderObject 向上寻找祖先节点,直到发现离它最近一个节点的 RenderAbstractViewport 就能拿到我们想要的 ViewPort 信息了。

Size? getViewPortSize(BuildContext context) {
  final RenderObject? box = context.findRenderObject();
  final RenderAbstractViewport? viewport = RenderAbstractViewport.of(box);
  assert(() {
    if (viewport != null) {
      debugPrint('Please make sure you have a `ScrollView` in ancestor');
      return false;
    }
    return true;
  });
  final Size? size = viewport?.paintBounds.size;
  return size;
}

Item 相对 ViewPort 的滑动起始点的距离

RenderAbstractViewport 的另一个方法 getOffsetToReveal,中,我们可以获得当前的 RenderObject 相对于这个 ViewPort 滑动的起始位置。

double getExposureOffset(BuildContext context) {
  final RenderObject? box = context.findRenderObject();
  final RenderAbstractViewport? viewport = RenderAbstractViewport.of(box);

  if (viewport == null || box == null || !box.attached) {
    return 0.0;
  }

  // box 为当前 Item 的 RenderObject
  // alignment 为 0 的时候获得距离起点的相对偏移量
  // 为 1 的时候获得距离终点的相对偏移量。
  final RevealedOffset offsetRevealToTop =
      viewport.getOffsetToReveal(box, 0.0, rect: Rect.zero);
  return offsetRevealToTop.offset;
}

滑动距离

要获得滑动距离通常有两种方式:

  • 通过 ScrollController 获得。
  • 利用 Scrollable Widget 的 Notification 机制。

每次编写代码的时候都必须得写 ScrollController 看上去有些麻烦,所以我们选择了Notification 这种方式。(它也更加通用)

Scroll Notification

Scrollable Widget 将会向其其祖先通知有关滚动变化信息,而这些信息能够使用 NotificationListener 来捕获到。目前有下面几种 Notification:

  • ScrollStartNotification:滚动开始时发起 Notification
  • ScrollUpdateNotification:滚动进行时不断发起 Notification。(频率很高)
  • ScrollEndNotification:滚动结束时发起 Notification
  • UserScrollNotification:当用户改变滚动方向时,发起通知。(通常在不同方向的 ScrollView 互相嵌套时会出现)

我们这里使用 NotificationListener 来获取 滑动的信息。

Widget buildNotificationWidget(BuildContext context, Widget child) {
  return NotificationListener<ScrollNotification>(
    onNotification: (scrollNotification) {
      // 这里就能获取到滚动信息
    },
    child: ScrollView,
  );
}

解决信息共享问题

看到这里,似乎我们要的拼图都凑齐了,但是总感觉哪里不对劲?

如果你敏锐的话,想必已经发现我们现在这样的设计根本没法在一个地方拿到全部信息。

数据获取位置不一致

Scroll Notification 仅会向祖先节点发起 Notification 通知,也就是说,我们在 Item 层级是拿不到的!

如果我们想要在 Item 中进行埋点曝光判定,就必须要获取到更高的祖先节点中的 scrollNotification

当然解法肯定有很多,共享状态的方法在状态管理中是一个常见的 Case,但是为了滑动埋点曝光就引入一个状态管理库似乎有些得不偿失,所以还不如使用 Flutter 最原始的 Inherit 机制来实现数据的共享。

什么是 Inherit 机制

要理解 Inherit 机制,首先你需要了解 Flutter 的三棵树, 这个网上的解释文章已经有很多了,我就不再赘述, 感兴趣的可以看看 迷鹿[3]的这篇 Widget、Element、Render 是如何形成树结构?[4]。

简单来说,Inherit 机制是一种能够在 Flutter 中自顶向下共享数据的方式,我们知道 Flutter 是通过树形结构来构建视图的,而其中的 InheritedWidget 则是能够让它的数据能够被所有子节点中的 Widget 访问到。

它的原理也是很简单,每个 Element 都持有了一个叫做 Map<Type, InheritedElement>? _inheritedWidgetsMap 的引用,当我们的 Element 在挂载到 Element Tree 的时候 (执行 mount 操作的时候会调用 _updateInheritance),将会把 parent 中保存的 _InheritedWidget 引用自己也给留一份。

void _updateInheritance() {
  assert(_lifecycleState == _ElementLifecycle.active);
  _inheritedWidgets = _parent?._inheritedWidgets;
}

InheritedWidget 创建的 Element 则会在 mount 的时候把自己给塞到这个 map 当中,这样就完成了自顶向下的数据共享了。

@override
void _updateInheritance() {
  assert(_lifecycleState == _ElementLifecycle.active);
  final Map<Type, InheritedElement>? incomingWidgets = _parent?._inheritedWidgets;
  if (incomingWidgets != null)
    _inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
  else
    _inheritedWidgets = HashMap<Type, InheritedElement>();
  _inheritedWidgets![widget.runtimeType] = this;
}

基于此,我们就可以完成对于滑动埋点曝光的计算了,可喜可贺。

拿来吧你

像我们这样有经验的开发者,看到这样好的文章,第一时间那一定是想要 自己实践一下。

直接拿来吧你

所以为了各位宝贵的 (滑水/唠嗑/带娃/...) 时间,这款滑动埋点方案已经登陆了 Pub 仓库[5],各位可以放心食用了。

目前已经支持的有:

  • 懒曝光模式:仅当滚动结束时再曝光;
  • 曝光比例:可以控制 Item 展现多大的范围算是一次曝光;
  • 追踪 Item 何时离开可视范围:可以获取到曝光时长;
  • 支持所有 ScrollView:包括 ListViewGridViewCustomScrollView 等等。

这个项目我会一直维护下去 (毕竟自己也要用), 如果你想了解该项目的最新进展, 可以关注该项目的 GitHub[6], 或者有需要增加的功能需求,也欢迎通过邮箱与我联系:

  • Pub 地址 https://pub.flutter-io.cn/packages/flutter_exposure
  • Github 地址 https://github.com/Vadaski/flutter_exposure
  • 邮箱 xinlei966@gmail.com

写在最后

这个解决方案其实是在去年公司里就用到了,一直没有来得及开源。在这里也感谢 闲鱼技术[7] 提供的宝贵思路, 最近凑了一些零零碎碎的时间把它给完成了,把趁着国庆第一天写完了这篇文章, 希望大家能通过我的分享有一点点收获~

文内链接

[1]揭秘!一个高准确率的 Flutter 埋点框架如何设计: https://juejin.cn/post/6844903864479514631#comment

[2]深入理解 BuildContext: https://juejin.cn/post/6844903777565147150

[3]迷鹿: https://juejin.cn/user/4309694831660711

[4]Widget、Element、Render 是如何形成树结构?: https://juejin.cn/post/6921493845330886670

[5]Pub 仓库: https://pub.flutter-io.cn/packages/flutter_exposure

[6]GitHub: https://github.com/Vadaski/flutter_exposure

[7]闲鱼技术: https://juejin.cn/post/6955304605190357005

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/Q0HLJdqeRcYg6GKQQWq4CQ

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
如何有效定位Flutter内存问题? 4年以前  |  16302次阅读
鸿蒙系统起飞!Flutter 完全适配指南 2年以前  |  12697次阅读
Flutter的手势GestureDetector分析详解 5年以前  |  12411次阅读
Flutter插件详解及其发布插件 5年以前  |  12139次阅读
 目录