前言

先看下各个平台自动化埋点支持

平台

特点

自动化埋点方案

Android

Java支持编译器静态代理/运行期动态代理

有成熟方案和产品,例如GrowingIO、神策、友盟

iOS

OC提供了强大的运行时和动态性

有成熟方案和产品,例如GrowingIO、神策、友盟

Flutter

Dart反射支持很弱,Flutter禁用了反射机制,考虑从编译期代码插桩实现

无成熟方案和产品

从编译期进行代码插桩,则需要修改编译期的中间件文件。

Dart文件编译会先编译成Dill文件,然后再编译成二进制代码。

如果能在编译器拿到Dill文件,然后进行修改插桩,再进行编译成Binary Code就可以达到AOP埋点的效果

flutter_tool是flutter的编译工具,其并没有提供接口供开发者hook,以及修改编译流程,那么要实现这个步骤,我们就需要修改flutter_tool这个工具。

闲鱼的AspectD就使用了这个思想GitHub - XianyuTech/aspectd: AOP for Flutter(Dart)

基于闲鱼的ApectD来开展后续的工作,这里的 Flutter SDK 完全依赖于原生 SDK,不具有单独运行的能力。

AspectD的使用

首先配置好flutter环境(flutter sdk、dart sdk、fvm、环境变量等),这里使用版本信息如下:

    • Flutter version 2.2.2 at /Users/sheng/GrowIO/flutter• Framework revision d79295af24 (4 months ago), 2021-06-11 08:56:01 -0700• Engine revision 91c9fc8fe0• Dart version 2.13.3• Pub download mirror https://pub.flutter-io.cn• Flutter download mirror https://storage.flutter-io.cn

1. 下拉aspectd仓库

这里我们对aspectd进行了部分修改,以满足我们的无埋点要求。

git clone https://github.com/growingio/aspectd.git

2. 修改build_tool,通过git patch方式

  • git patch

cd path-for-flutter-git-repo
git apply --3way path-for-aspectd-package/0001-aspectd.patch
rm bin/cache/flutter_tools.stamp

path-for-flutter-git-repo表示flutter的路径

path-for-aspectd-package表示aspectd的路径

这里可能会存在git apply错误的情况,可以打开0001-aspectd.patch文件,根据变动自行添加修改。

AspectD通过改写Flutter中的flutter_tools进行修改Dill文件,变动了两个文件:

  • flutter/packages/flutter_tools/lib/src/aspectd.dart 添加

  • flutter/packages/flutter_tools/lib/src/build_system/targets/common.dart 修改

AspectD通过git patch方式,给flutter的分支添加了这些变动。

  • 环境设置

~/.bash_profile 文件中添加

export PUB_HOSTED_URL=https://pub.flutter-io.cn //国内用户需要设置
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn //国内用户需要设置

注:git@github.com: Permission denied 问题,需要你设置ssl证书

然后在aspectd根目录执行 flutter pub get

sheng@chengpengdeMacBook-Pro aspectd % flutter pub get
[KWLM]:pub get
Warning: You are using these overridden dependencies:
! kernel 0.0.0 from git git@github.com:XianyuTech/sdk.git at c9f1a5 in pkg/kernel
! meta 1.3.0 from git git@github.com:XianyuTech/sdk.git at c9f1a5 in pkg/meta
Running "flutter pub get" in aspectd...                            744ms
Running "flutter pub get" in example...                             7.8s

显示我们修改了kernel依赖。

3. 运行example

aspectd/aspectd/aspectd_impl/aspectd/example/ 这3个目录我们都需要进行 flutter pub get

然后进入到aspectd源码目录的example中执行:flutter run --debug --verbose ,也可以直接Android Studio中打开,运行Main

如果/aspectd/lib/src/flutter_frontend_server/下生成了frontend_server.dart.snapshot则表示此次编译 aspectd_impl 成功了。

你也可以不使用flutter run来生成frontend_server.dart.snapshot,使用下面的命令

dart --deterministic --packages=/Users/sheng/GrowIO/aspectd-ex/lib/src/flutter_frontend_server/package_config.json --snapshot=/Users/sheng/GrowIO/aspectd-ex/lib/src/flutter_frontend_server/frontend_server.dart.snapshot --snapshot-kind=kernel /Users/sheng/GrowIO/aspectd-ex/lib/src/flutter_frontend_server/starter.dart

如果frontend_server.dart.snapshot没有生成,运行会显示build完成,但是无法运行。

Launching lib/main.dart on iPhone SE (2nd generation) in debug mode...
lib/main.dart:1
Xcode build done.                                           23.1s
Failed to build iOS app
Error output from Xcode build:
↳** BUILD FAILED **
Xcode's output:
↳/Users/sheng/GrowIO/aspectd/aspectd_impl/.packages does not exist.Did you run "flutter pub get" in this directory?Command PhaseScriptExecution failed with a nonzero exit codenote: Using new build systemnote: Building targets in parallelnote: Planning buildnote: Constructing build descriptionwarning: The iOS Simulator deployment target 'IPHONEOS_DEPLOYMENT_TARGET' is set to 8.0, but the range of supported deployment target versions is 9.0 to 14.0.99. (in target 'Runner' from project 'Runner')
Could not build the application for the simulator.
Error launching application on iPhone SE (2nd generation).
Exited (sigterm)

值得注意的是,

  • aspectd/ 对应调试aspectdtransform代码部分工程,即修改/aspectd/lib/src/ 下的代码是需要该工程的

  • aspectd/aspectd_impl/ 是添加的 Hook 相关的代码部分

  • aspectd/example/ 则是工程demo

每次修改 aspectd hook 相关的代码需要先执行 flutter clean ,再进行编译。

4. hook相关

  • hook写在哪里,怎么hook?

参考aspectd的 README,至此apsectd的集成就告一段落。

自动化埋点

由于Flutter可以依赖于原生SDK,原生SDK包含事件发送逻辑,网络传输逻辑,并且发送 App打开关闭事件App访问事件自定义事件等,那么Flutter部分只需要传递如下事件到原生SDK:

  1. 点击元素事件

  2. 元素内容改变事件

  3. 页面曝光事件

点击事件

对于点击事件,则需要hook点击触发方法,暂时分成两步,在以下时机进行切面

  /// click event aop step 1/// hittest
@Call("package:flutter/src/gestures/hit_test.dart", "HitTestTarget","-handleEvent")/// click event aop step 2/// callback
@Call("package:flutter/src/gestures/recognizer.dart", "GestureRecognizer","-invokeCallback")

关于路径Path的获取,则是通过 Element 中向上遍历父级元素 visitAncestorElements 方法,将一整条元素链存入数组。

相关代码可以去仓库 ​aspectd/growing_aop_impl.dart at master · growingio/aspectd 查看。

路径过滤

从上述方法 中最终获取到的路径 Path 包含多余的系统元素,例如:

MyHomePage/Semantics/Builder/RepaintBoundary/IgnorePointer/AnimatedBuilder/Stack/DecoratedBox/DecoratedBoxTransition/FractionalTranslation/SlideTransition/FractionalTranslation/SlideTransition/CupertinoPageTransition/AnimatedBuilder/RepaintBoundary/Semantics/FocusScope/Actions/PageStorage/Offstage/Semantics/TickerMode/Overlay/Semantics/FocusScope/AbsorbPointer/Listener/HeroControllerScope/Navigator/IconTheme/IconTheme/CupertinoTheme/Theme/AnimatedTheme/Builder/DefaultTextStyle/CustomPaint/Banner/CheckedModeBanner/Title/Directionality/Semantics/Localizations/MediaQuery/Focus/FocusTraversalGroup/Actions/Semantics/Focus/Shortcuts/WidgetsApp/HeroControllerScope/ScrollConfiguration/MaterialApp/MyApp/[root]

我们需要过滤掉系统元素,参考 Flutter Inspector 工具的实现以及 /kernel/lib/transformations/track_widget_constructor_locations.dart /flutter/packages/flutter/lib/src/widgets/widget_inspector.dart 文件代码,其实现逻辑为:

/kernel/lib/transformations/track_widget_constructor_locations.dart 文件在编译期通过一个 transformer 使得所有的 widget 实现了抽象类 _HasCreationLocation_HasCreationLocation 包含了文件位置信息,如果文件是否是用户自己创建,则会记录进Path,但这个功能只会在debug模式下启用,所以我们需要自己实现,那么参考 track_widget_constructor_locations 实现,我们需要通过AspectD插入一个transformer,来完成所有的 widget 实现了抽象类_HasCreationLocation 的操作。

这里提供了自己实现的代码供参考: ​aspectd/track_widget_custom_location.dart at master · growingio/aspectd

Transformer实现

track_widget_constructor_locations 实现介绍

_CustomHasCreationLocation 对应 _HasCreationLocation ,因为我们不能和Inspector一致,同理还有 _creationLocationParameterName 以及 _locationFieldName

抽象类 _CustomHasCreationLocation 其实是外部实现的,示例中写在 growing_impl.dart 文件中,在 _resolveFlutterClasses 方法中会判断路径,来获取该抽象类。

​aspectd/growing_impl.dart at master · growingio/aspectd

​aspectd/track_widget_custom_location.dart at master · growingio/aspectd

再就是 RootUrl 的判断,Flutter Inspector 中通过 /flutter/packages/flutter/lib/src/widgets/widget_inspector.dart 来获取 RootUrl 的,这里我们暂时在 track_widget_constructor_locations 中保存 main.dart 的路径前段,来判断是否是用户的工程创建,然后将这个 RootUrl 保存在了 _CustomLocation 实体中,具体可以在 _constructLocation 查看

ConstructorInvocation _constructLocation(Location location, {String name,ListLiteral parameterLocations,bool showFile: true,
}) {final List<NamedExpression> arguments = <NamedExpression>[new NamedExpression('line', new IntLiteral(location.line)),new NamedExpression('column', new IntLiteral(location.column)),new NamedExpression('rootUrl', new StringLiteral(_rootUrl)),];

然后以此判断是否是自己创建

bool _isLocalElement(Element element) {Widget widget = element.widget;if (widget is _CustomHasCreationLocation) {_CustomHasCreationLocation creationLocation =widget as _CustomHasCreationLocation;if (creationLocation._customLocation.isProjectRoot()) {return true;}}return false;}

最终过滤多余 Element 后,Path 路径如下:

MyApp/MaterialApp/MyHomePage/Scaffold/Center/Column/GestureDetector/Text

元素内容改变事件

对于此类事件,暂时只对常见文本框进行了处理,对文本内容改变的方法进行了切面:

  /// text value changed/// EditableTextState@Execute("package:flutter/src/widgets/editable_text.dart", "EditableTextState","-updateEditingValue")

代码链接:​aspectd/growing_aop_impl.dart at master · growingio/aspectd

除了改变的 Text 内容,还需要 路径Path 以及当前 页面Page 等关键信息, 路径Path 可以参考元素的点击事件处理,页面Page信息则需要我们自己记录堆栈。

页面曝光事件

在阅读Flutter源码过程中,是有一个类似储存页面堆栈的机制的,叫做 RouteEntry ,我们可以依此展开。

这里为了获取上下文信息,又对 buildPage 方法进行了切面。

  /// 1. Page Push - get only RouteEntry@Execute("package:flutter/src/widgets/navigator.dart", "_RouteEntry", "-handlePush")/// 2. Page Pop - get only RouteEntry@Execute("package:flutter/src/widgets/navigator.dart", "_RouteEntry", "-handlePop")/// 3. Page Build/// can get context and widget@Execute("package:flutter/src/material/page.dart","MaterialRouteTransitionMixin", "-buildPage")/// 4. Page Build/// can get context and widget@Execute("package:flutter/src/cupertino/route.dart","CupertinoRouteTransitionMixin", "-buildPage")

具体代码可以查看:​aspectd/growing_aop_impl.dart at master · growingio/aspectd

然后再在对应的方法中,记录页面信息以及页面堆栈,既可以达到我们预想的效果。具体代码参考 ​aspectd/growing_aop_impl.dart at master · growingio/aspectd 中 handlePushhandleBuildPagehandlePop 的处理。

可视化埋点(圈选)

可视化埋点需要遍历页面所有元素,并将可以选择的元素上传,依于之前的操作,我们已经做了页面的存储,则可以通过页面子元素的遍历,遍历页面上所有元素信息。此外,也需要监听页面变动,以选择合适的时机来遍历。

Flutter 每次元素变动,或者刷新会触发 DrawFrame 方法

  /// Draw Frame - 每次变动刷新/// SchedulerBinding:support window.onBeginFrame/window.onDrawFrame call back@Execute("package:flutter/src/scheduler/binding.dart", "SchedulerBinding","-handleDrawFrame")

在此方法中,通过 Element 的 visitChildElements 方法遍历所有子元素,同时过滤系统元素,则可以达到我们想要的效果。

  void webcircleSend() {/// 圈选遍历逻辑if (GrowingAutotracker.getInstance().webCircleRunning) {if (pageList.isEmpty) {GIOLogger.debug("handleDrawFrame webcircle error : no found page entry");return;}GrowingPageEntry entry = pageList.last;entry.context.visitChildElements((element) {traverseElement(element, entry.context as Element, false, 0);});circleElments.forEach((child) {GIOLogger.debug("circleElement : " + child.toString());});Map<String, dynamic> map = <String, dynamic>{};Map<String, dynamic> page = <String, dynamic>{};/// translate entry to mapList<Map> elements = <Map>[];circleElments.forEach((element) {elements.add(element.toMap());});map["elements"] = elements;var element = entry.context as Element;final RenderBox box = element.renderObject as RenderBox;final size = box.size;final offset = box.localToGlobal(Offset.zero);MediaQueryData queryData = MediaQueryData.fromWindow(ui.window);if (queryData.devicePixelRatio > 1) {page["left"] = offset.dx*queryData.devicePixelRatio;page["top"] = offset.dy*queryData.devicePixelRatio;page["width"] = size.width*queryData.devicePixelRatio;page["height"] = size.height*queryData.devicePixelRatio;} else {page["left"] = offset.dx;page["top"] = offset.dy;page["width"] = size.width;page["height"] = size.height;}page["path"] = _getPagePath(entry);page["title"] = entry.titile;page["isIgnored"] = false;/// pagesmap["pages"] = <Map>[page];GrowingAutotracker.getInstance().flutterWebCircleEvent(map);GIOLogger.debug('handleDrawFrame circle ' + map.toString());circleElments.clear();}}void traverseElement(Element element,Element parent, bool isIgnored, int z) {// GIOLogger.debug("reversedObjc " + element.widget.runtimeType.toString());if (_isLocalElement(element)) {String? elementType = null;if (element.widget is IgnorePointer) {/// ignorePointer will ignore all subtree if is ignoringIgnorePointer widget = element.widget as IgnorePointer;if (widget.ignoring) {element.visitChildElements((child) {traverseElement(child,element, true,z++);});return;}}else if (element.widget is RawMaterialButton || element.widget is MaterialButton || element.widget is FloatingActionButton || element.widget is AppBar) {/// because of is local element, Gesture is create by system/// RawMaterialButton is super class of RaisedButton、FlatButton、OutlineButton// [RawMaterialButton,MaterialButton,FloatingActionButton].takeWhile((e) => element.widget is e).isNotEmpty;elementType = "BUTTON";}else if (element.widget is TextFormField || element.widget is TextField) {elementType = "INPUT";}else if (element.widget is ListView || element.widget is CustomScrollView || element.widget is SingleChildScrollView || element.widget is GridView) {elementType = "LIST";}else if (parent.widget is GestureDetector) {/// gesture click enableelementType = "TEXT";}if (elementType != null) {GrowingCircleElement circle = GrowingCircleElement();final RenderBox box = element.renderObject as RenderBox;final size = box.size;final offset = box.localToGlobal(Offset.zero);MediaQueryData mediaQuery = MediaQueryData.fromWindow(ui.window);if (mediaQuery.devicePixelRatio > 1) {circle.rect = Rect.fromLTWH(offset.dx*mediaQuery.devicePixelRatio, offset.dy*mediaQuery.devicePixelRatio, size.width*mediaQuery.devicePixelRatio, size.height*mediaQuery.devicePixelRatio);}else {circle.rect = Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height);}var parser = GrowingElementParser(element, currentPage());var parentParser = GrowingElementParser(parent, currentPage());circle.xpath = parser.xpath;circle.parentXPath = parentParser.xpath;circle.content = parser.content;circle.index = parser.index;circle.page = _getPagePath(currentPage());circle.zLevel = z;circle.isContainer = false;circle.isIgnored = isIgnored;circle.nodeType = elementType;circleElments.add(circle);}element.visitChildElements((child) {traverseElement(child,element, isIgnored,z++);});}else {element.visitChildElements((child) {traverseElement(child,parent, isIgnored,z++);});}}

然后将所有信息传输至原生SDK,由原生SDK进行发送。

结尾

此部分代码仍在开发中,可视化埋点部分在iOS上初步顺利,在Android平台上仍有截图黑屏,对原生SDK代码侵入性较强等问题,同时依赖aspectd的方式,也让用户集成会更加困难,也是一个需要考虑的问题。

Flutter 无埋点SDK实现相关推荐

  1. 最近无埋点技术很是流行,抽空研究了下诸葛IO,talkingData以及百分点这些业内知名公司的无埋点SDK,抽取其中重要的信息供大家参考:

    1.首先什么是无埋点呢,其实所谓无埋点就是开发者无需再对追踪点进行埋码,而是脱离代码,只需面对应用界面圈圈点点即可追加随时生效的事件数据点. 无埋点的好处 其实无埋点并不是完全不用写代码,而是尽可能的 ...

  2. 无埋点数据收集和adb monkey测试屏蔽通知栏

    简单记录百度移动统计android无埋点sdk使用和monkey测试屏蔽通知栏的问题 1.无埋点sdk使用 很简单,下载完sdk后导入到项目中 , 参考sdk文档进行就可以了,个人觉得比友盟还简单,几 ...

  3. 揭秘数极客Android无埋点数据采集原理

    采集数据柯林斯基本分为代码埋点状语从句:无埋点.近年来无埋点的数据采集方案越来越普及,而无埋点的实现方案也有多种,我们今天讨论的问题是数据采集的一种方案,是无需开发人员重复进行采集事件的代码埋点就能达 ...

  4. 微信小程序无埋点数据采集方案

    相信业务团队对这样的场景不会太陌生: 打点需求:每新上一个功能,数据产品便会同步加上打点需求,当数据打点上线后一段时间,数据产品/业务产品便会针对数据的转化率分析和对业务需求的调整: 打点正确性验证: ...

  5. SDK无埋点技术在百分点的探索和实践

    2016-10-11 唐星 移动开发前线 移动开发前线 移动开发前线 微信号 bornmobile 功能介绍 专注于分享移动开发前沿和一线技术. 本文为『移动前线』群在9月21日的分享总结整理而成,转 ...

  6. 深入浅出:移动端(Android 和 iOS)数据采集埋点 SDK

    随着大数据时代的到来,越来越多公司注意到数据带来的价值,开始自建或购买一些第三方的数据平台.从数据流的角度看,平台对于数据的处理,一般有以下几个步骤: 其中,数据采集工作是后面几个步骤的基础,数据采集 ...

  7. python埋点测试_埋点进化论:从埋点到无埋点

    鲁迅先生说:世界上本没有埋点,需要数据的人多了,也就有了埋点. 埋点的诞生 在最初的互联网世界中,并没有埋点的概念.大家并不关心流量从哪里来,用户在网站上做了什么事,一切都是野蛮生长. 随着业务的增长 ...

  8. 无埋点实现监测的真相——革新还是噱头?

    转载:http://www.chinawebanalytics.cn/auto-event-tracking-good-bad-ugly/ 所谓"埋点",是数据采集领域(尤其是用户 ...

  9. 代码埋点、可视化埋点、无埋点几种数据埋点方案的分析报告

    目录 数据采集的核心问题 一.埋点是什么 二.为什么要埋点 三.埋点有哪些方式 四.各埋点方式优劣对比 五.其他 在这篇文章里面,我们会对数据采集的一些基本概念进行阐述,然后,会针对目前市面上新增的一 ...

最新文章

  1. Scrapy-Splash的介绍、安装以及实例
  2. 菜鸟之webservice(一) 服务端搭建
  3. const、static、const staic理解
  4. max7219驱动共阳点阵
  5. step by step approach for building interactive dash app using python: step 1
  6. moxy json介绍_MOXy作为您的JAX-RS JSON提供程序–客户端
  7. DB2数据库中DB2字符串类型
  8. 【Python】【数据库】
  9. oracle 批量给字段加注释,Oracle给表和字段添加注释
  10. paramiko模块实现堡垒机的思路
  11. docker commit 制作镜像
  12. VC知识库搜索ADO
  13. CAD导出.eps格式图
  14. win10解决部分应用字体模糊的问题
  15. 各种开发语言项目环境国内(中国国内加速镜像)配置教程和部分实践经验,包括github.com, nodejs,npm,nvm, yarn, java, maven, gradle, python, m
  16. html div鼠标选中状态,CSS鼠标移动div时如何避免选中div中的文字
  17. java通过itextpdf实现pdf文件加水印
  18. 未来科技蒲公英大飞_大烟草的下跌告诉我们关于大科技的未来
  19. linux下ssh、scp无密钥登陆方法
  20. 上汽集团 java_【上汽集团工资】研发工程师待遇-看准网

热门文章

  1. ArcEngine 鹰眼功能C#实现
  2. c#中文件路径出现非法字符怎么办?解决也容易
  3. 【互联网寒冬】经历裁员,拿20W被迫去大厂
  4. 微信小程序发布详细步骤
  5. 【开发必备】快来收藏!涵盖日常开发中所需要的60多个正则验证!!
  6. 量化交易 米筐 因子的打分对比(因子的对比与挑选)
  7. 移动群智感知应用学习
  8. 解决运行roscore时出现报错问题
  9. 功能测试用例需要详细到什么程度,完全测试程序是可能的么
  10. tekton TriggerBinding资源