Flutter无痕埋点

Flutter无痕埋点

因为工做调整的缘由,后面可能将再也不接触flutter开发,本着技术共享不埋没的原则,开源一下flutter无痕埋点的技术方案,此方案完成于半年前,应该在闲鱼的无痕埋点方案开源前,与闲鱼的方案不太同样,你们有什么建议能够普遍留言。另外特别感谢永葵同窗在此技术中的参与和共同努力。app

思考

  • 既然是无痕埋点,Flutter如何实现AOP?像iOS,能够在运行时经过一些方法交换,消息转发来实现切面,可是因为目前Flutter不支持运行时的反射功能,因此第一时间放弃了这个方向。另外因为当时时间和资源上也不是很充足,也没有研究编译期的AOP方案,不过以后闲鱼出品的AspectD确实是个不错的AOP框架。切回正题,因此当时的选择时阅读Flutter的源码,试图寻找官方的API来解决问题。

Flutter如何遍历widget

  • 众所周知,在flutter中有widget树和element树,二者成一一对应的关系,那么想要遍历widget树,其实也就是遍历element树,在阅读源码的过程当中发现framework.dart这个文件下一个visitChildElements(ElementVisitor visitor)的方法,也就是说,经过这个方法,咱们能够遍历指定element下全部的elements,而后经过element拿到他所对应的widget。

页面埋点

导航栏监听

  • 要想作页面埋点,第一个想到的就是经过监听导航栏页面的变更,就能够监听到页面的变化。经过NavigatorObserver确实就能够监听到页面的push和pop,可是官方的这个类提供的方法并不能拿到具体跳转页面的信息(静态页面除外)。那么是否是能够当监听到页面push和pop时,直接遍历element树拿到widget信息呢?实践证实,当监听到push的同时去遍历会报错,由于这个时候页面还正在渲染,flutter的元素正在生成,因此遍历会有问题。因此须要监听页面的渲染完成的时机。

页面渲染监听

  • 如何作页面渲染的监听呢?咱们能够看一下Flutter的启动函数
void runApp(Widget app) {
 WidgetsFlutterBinding.ensureInitialized()
   ..attachRootWidget(app)
   ..scheduleWarmUpFrame();
}
复制代码

而后咱们再观察下WidgetsFlutterBinding这个类,框架

class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding 复制代码

在这个类里作了很是多的绑定,渲染的绑定,手势的绑定等等。 SchedulerBinding这是一个调度器,调度任务的安排。 在SchedulerBinding中有一个控制渲染的方法函数

void handleDrawFrame() {
   assert(_schedulerPhase == SchedulerPhase.midFrameMicrotasks);
   Timeline.finishSync(); // end the "Animate" phase
   try {
     // PERSISTENT FRAME CALLBACKS
     _schedulerPhase = SchedulerPhase.persistentCallbacks;
     for (FrameCallback callback in _persistentCallbacks)
       _invokeFrameCallback(callback, _currentFrameTimeStamp);

     // POST-FRAME CALLBACKS
     _schedulerPhase = SchedulerPhase.postFrameCallbacks;
     final List<FrameCallback> localPostFrameCallbacks =
         List<FrameCallback>.from(_postFrameCallbacks);
     _postFrameCallbacks.clear();
     for (FrameCallback callback in localPostFrameCallbacks)
       _invokeFrameCallback(callback, _currentFrameTimeStamp);
   } finally {
     _schedulerPhase = SchedulerPhase.idle;
     Timeline.finishSync(); // end the Frame
     assert(() {
       if (debugPrintEndFrameBanner)
         debugPrint('▀' * _debugBanner.length);
       _debugBanner = null;
       return true;
     }());
     _currentFrameTimeStamp = null;
   }
 }
复制代码

根据源码咱们能够看到当布局完成后会调用已经注册的回调postFrameCallbacks,官方很友好的开放了添加回调的方法。布局

void addPostFrameCallback(FrameCallback callback) {
   _postFrameCallbacks.add(callback);
 }

复制代码

因此咱们就能够当监听到导航栏路由变化时,而且监听到布局渲染后去遍历element树,拿到咱们想要的widget,去获取widget上面的信息。post

识别页面并获取页面信息

  • 在树的遍历上,咱们须要用从下往上从左往右的方式去遍历,由于元素是从上往下从右往左添加到树上的。咱们只须要找到第一个Scaffold,就能够肯定页面,从它再往上取1-2层,拿到为页面的widget,这中间的一个路径就能够做为咱们页面的标示了。固然你也能够加上Scaffold里页面的title。

点击埋点

全局捕获点击事件

  • 在flutter中存在一个手势竞技场,竞技场中最后获胜的手势能够响应点击事件。那么既然竞技场最后只能响应一个手势,可是又不能hook手势的onTap方法,那咱们要如何实现全局捕获点击事件呢?一样咱们仍然阅读flutter源码,能够看到TapGestureRecognizer中有一个以下方法。
void acceptGesture(int pointer) {
   super.acceptGesture(pointer);
   if (pointer == primaryPointer) {
     _checkDown(pointer);
     _wonArenaForPrimaryPointer = true;
     _checkUp();
   }
 }
 
void rejectGesture(int pointer) {
   super.rejectGesture(pointer);
   if (pointer == primaryPointer) {
     // Another gesture won the arena.
     assert(state != GestureRecognizerState.possible);
     if (_sentTapDown)
       _checkCancel('forced ');
     _reset();
   }
 }
复制代码

上面的方法是手势的拒绝和添加。从源码中咱们能够看出当另外一个手势从竞技场胜出时,不会执行手势的成功回调,而是会执行手势的取消回调。那么咱们是否是能够添加一个全局的手势,而后重写它的拒绝方法,让它内部执行手势的接受方法呢,这样咱们本身添加的全局手势也能执行成功回调了?实践见证确实能够这样作。ui

识别点位标示并获取信息

  • 一样咱们能够在全局手势的回调中拿到当前点位坐标。在回调中一样咱们遍历element树,拿到符合点位坐标而且widget runTimeType 是Ink的最小单元。

最小单元: 蓝色,黄色,绿色都是Ink控件,白色为点击的点,那么绿色即为最小单元。

把Ink到Scroffld的中间路径为做为一个点击位的标示,固然中间能够过滤一些不须要的widget,否则路径会很长。另外能够获取按钮或者子控件的一些其余信息,如title和Image name。spa

遇到的坑

无痕埋点的方案基本上是上面的这些,可是中间咱们仍是一路踩坑,细节上有些须要特殊处理。debug

多子元素控件的处理

  • flutter中widget子组件可能不只有child还有些childrens,所对应的element为SingleChildRenderObjectElement和MultiChildRenderObjectElement,因此在遍历element是要注意区分为哪一种element类型,不一样类型的遍历上略有不一样。如在手势埋点中,在多子元素的组件中,从最后一个开始遍历,保证获取到的第一个命中的点击事件是相应的那个(界面上是最顶层的组件)。一样像ListView,Tab这种组件想要获取正确的路径位置,须要获取当前element在MultiChildRenderObjectElement中的正确位置。

Tab的特殊处理

  • 当Tab下的页面切换时,并不会触发路由的didPush和didPop,因此咱们须要借助点击事件的获取,并分析出是tab切换所触发的,而后遍历获取到页面信息。

附言

  • 对文章有疑问或者有任何技术想要探讨的同窗能够经过邮箱联系我 531889780@qq.com
相关文章
相关标签/搜索