能监控页面的曝光时机还不够,有时我们不仅仅需要的是知道进入了哪个页面,还需要知道在某个页面停留了多长的时间,并且应用在前后台的切换也要计算进去。同样的,在Flutter中存在监听页面的生命周期的接口WidgetsBindingObserver:
abstract class WidgetsBindingObserver { //省略部分代码 /// Called when the system puts the app in the background or returns /// the app to the foreground. /// /// An example of implementing this method is provided in the class-level /// documentation for the [WidgetsBindingObserver] class. /// /// This method exposes notifications from [SystemChannels.lifecycle]. void didChangeAppLifecycleState(AppLifecycleState state) { } } 复制代码
其中AppLifecycleState是个枚举类,包含四种状态:
enum AppLifecycleState { resumed, inactive, paused, detached, } 复制代码
该接口通过以上四种状态,我们可以知道在某个页面停留的时长是多久。
以上是采集页面pv、uv、页面路径的基本思路,具体的代码不多做介绍,逻辑参考原生的实现即可。后面我着重介绍用户行为操作,点击行为埋点数据的采集实现。
3. Flutter组件ID的规则
对于组件的ID来说,它的规则要比页面的定义更加复杂。首先,Flutter的组件本身并没有一个id的概念,虽然Flutter的每个Widget都可以通过一个唯一key去标识,但是在创建Widget的时候除非有特殊的需求(比如复用等),我们一般不会去传入一个key,所以需要换个思路:根据视图树。
[图片上传中...(image-c1c490-1605191361082-11)]
每个页面的组件都是根据其父子、兄弟关系构建出视图树绘制在页面上。从我们观测的组件的本身开始,在这个视图树上逐级向上遍历搜索,直到根节点,找到这个组件在这个树上的位置信息等特征信息,这样就能得到一个组件在视图树上的 一个组件路径,也就是说,我们可以根据这个路径,在视图树中定位到这个组件(图片引用自极客时间-Flutter专栏):
widget、Element、RenerObject关系
[图片上传中...(image-dfdb0a-1605191361082-10)]
三棵树 Flutter中,存在这么三棵树(为了便于理解我们抽象RenderObject也为一个树),当我们点击了某个Widget的时候,我们期望的结果是可以通过这个Widget获取它在视图树上的位置,可惜的是Flutter中的Widget并没有一个类似"parent"和"child"属性可以供我们去获取,也没有提供接口让我们去获取,其实这也比较好理解,因为Widget本身就只是一个配置信息,这点在Widget源码中注释也有体现:"Describes the configuration for an [Element]."
再从Element树入手,通过对Element源码的阅读,Element实现了BuildContext,而BuildContext它定义了一系列的接口去获取父子element与指定的RenderObject、指定类型的Widget、指定的State等等:
abstract class BuildContext { ... ///搜索Element父节点 void visitAncestorElements(bool visitor(Element element)); ///搜索Element子节点 void visitChildElements(bool visitor(Element element));
T findAncestorWidgetOfExactType(); T findAncestorStateOfType(); T findAncestorRenderObjectOfType(); ...还有其他的省略... }
复制代码
Element实现了具体的搜索方法:
void visitAncestorElements(bool visitor(Element element)) { assert(_debugCheckStateIsActiveForAncestorLookup()); Element ancestor = _parent; while (ancestor != null && visitor(ancestor)) ancestor = ancestor._parent; } 复制代码
而根据Element,是可以通过element.widget获取与之对应的Widget的,根据Widget也就得到了具体的路径。
而如果选择从RenderObejct入手,它内部定义了获取父亲节点与子节点的方法:
abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget { ///获取树上的父节点 AbstractNode? get parent => _parent;
... //遍历搜索子节点 void visitChildren(RenderObjectVisitor visitor) { } ... } 复制代码
RenderObject在源码中看似没有定义接口去直接获取对应的Element的,更加无法直接去获取对应的Widget,但是注意到它有一个debugCreator属性:
/// The object responsible for creating this render object. /// Used in debug messages. Object? debugCreator;///表示这个render obejct表示负责创建此render object的对象,也就这个render object被谁持有 复制代码
虽然是个Object类型的,但是源码中对应的就是DebugCreator类:
/// A wrapper class for the [Element] that is the creator of a [RenderObject]. /// /// Attaching a [DebugCreator] attach the [RenderObject] will lead to better error /// message. class DebugCreator { /// Create a [DebugCreator] instance with input [Element]. DebugCreator(this.element);
/// The creator of the [RenderObject]. final Element element;
@override String toString() => element.debugGetCreatorChain(12); } 复制代码
在Element的子类RenderObjectElement的mount和update方法中对这个属性进行了创建:
@override void mount(Element parent, dynamic newSlot) { super.mount(parent, newSlot); //省略部分代码... _renderObject = widget.createRenderObject(this); //省略部分代码... assert(() { //复制debugCreator属性方法(assert部分会在Release的时候删除) _debugUpdateRenderObjectOwner(); return true; }()); //省略部分代码... }
@override void update(covariant RenderObjectWidget newWidget) { super.update(newWidget); ... assert(() { //复制debugCreator属性方法(assert部分会在Release的时候删除) _debugUpdateRenderObjectOwner(); return true; }()); ... }
void _debugUpdateRenderObjectOwner() { assert(() { //将当前Element传入到DebugCreator中保存。RenderObjectElement继承Element _renderObject.debugCreator = DebugCreator(this); return true; }()); }
复制代码
可以看到通过这种方式,如果是可以通过在RenderObject中的debugCreator属性被赋值,那么是可以通过这个属性获取到对应的Element的,也就可以获取到Widget。但是通过代码也看到这个属性赋值定义在assert中,Release下不会走这部分,所以这一块要做修改。
所以,如果能在点击的时候能直接或间接获取到Element,根据上面路径的规则生成,对于上图中的GestureDetector,它的路径为:
Contain[0]/Column[0]/Contain[1]/GestureDetector[0];
同时,为了防止不同页面中可能存在的路径相同情况,给这个路径加上当前页面的标识,所以path最后的规则为:
[ 页面ID:组件路径 ]。
4. Flutter中事件与手势分析
为了更好的理解Flutter中的手势事件,下面简要的做一个分析:
Flutter中指针事件表示用户交互的原始触摸数据,例如PointerDownEvent、PointerUpEvent、PointerCancelEvent等等,当手指触摸屏幕的时候,发生触摸事件,Flutter会确定触发的位置上有哪些组件,并将触摸事件交给最内层的组件去响应,事件会从最内层的组件开始,沿着组件树向根节点向上一级级冒泡分发。
通过对一个简单的GestureDetector组件的点击回调的debug观测,得到如下图的一个调用结构:
[图片上传中...(image-c56bb-1605191361080-9)]
上图中,_rootRunUnary以下为引擎自己实现的调用,会将收集到的事件传递到GestureBinding._handlePointerDataPacket中:
mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, HitTestTarget { @override void initInstances() { super.initInstances(); _instance = this; ///binding初始化的时候设置了回调方法,接受引擎传来的事件数据 window.onPointerDataPacket = _handlePointerDataPacket;///onPointerDataPacket就是一个function } .... } 复制代码
GestureBinding._flushPointerEventQueue方法就是对队列中的事件依次取出并进行处理:
final Queue _pendingPointerEvents = Queue(); void _flushPointerEventQueue() { assert(!locked);
if (resamplingEnabled) { _resampler.addOrDispatchAll(_pendingPointerEvents); _resampler.sample(samplingOffset); return; }
// Stop resampler if resampling is not enabled. This is a no-op if // resampling was never enabled. _resampler.stop();
while (_pendingPointerEvents.isNotEmpty) _handlePointerEvent(_pendingPointerEvents.removeFirst()); } 复制代码
所以,真正开始处理PointerEvent应该是从GestureBinding的_handlePointerEvent方法开始:
void _handlePointerEvent(PointerEvent event) { assert(!locked); HitTestResult? hitTestResult; if (event is PointerDownEvent || event is PointerSignalEvent) { assert(!_hitTests.containsKey(event.pointer)); hitTestResult = HitTestResult();///1.创建一个HitTestResult对象 hitTest(hitTestResult, event.position);///2.命中测试,实际先调用到RendererBinding的hitTest方法 if (event is PointerDownEvent) { _hitTests[event.pointer] = hitTestResult;///如果是PointerDownEvent,创建事件标识id与hitTestResult的映射 } ... } else if (event is PointerUpEvent || event is PointerCancelEvent) { hitTestResult = _hitTests.remove(event.pointer);///事件序列结束后移除 } else if (event.down) { ///其他是事件重用Down事件避免每次都要去命中测试(比如:PointerMoveEvents) hitTestResult = _hitTests[event.pointer]; } ... if (hitTestResult != null || event is PointerHoverEvent || event is PointerAddedEvent || event is PointerRemovedEvent) { assert(event.position != null); dispatchEvent(event, hitTestResult);///分发事件 } } 复制代码
对代码中的几点注释说明:
-
如果是
PointerDownEvent或者是PointerSignalEvent,直接创建一个HitTestResult对象,该对象内部有一个_path字段(集合); -
调用
hitTest方法进行命中测试,而该方法就是将自身作为参数创建HitTestEntry,然后将HitTestEntry对象添加到HitTestResult的_path中。HitTestEntry中只有一个HitTestTarget字段。实际也就是将这个创建的HitTestEntry添加到HitTestResult的_path字段中,当做事件分发冒泡排序中的一个路径节点。
///先RendererBinding的hitTest方法,方法定义如下: void hitTest(HitTestResult result, Offset position) { assert(renderView != null); assert(result != null); assert(position != null); renderView.hitTest(result, position: position); super.hitTest(result, position); } 复制代码
内部调用主要就是两步:
- 调用
RenderView的hitTest方法(从根节点RenderView开始命中测试):
bool hitTest(HitTestResult result, { required Offset position }) {
if (child != null)
///内部会先对child进行命中测试
child!.hitTest(BoxHitTestResult.wrap(result), position: position);
result.add(HitTestEntry(this));///将自己添加到_path字段,作为一个事件分发的路径节点
return true;
}
///child是RenderBox类型对象,hitTest方法在RenderBox中实现:
bool hitTest(HitTestResult result, { @required Offset position }) {
///...去掉assert部分
///这里就是判断点击的区域置是否在size范围,是否在当前这个RenderObject节点上
if (_size.contains(position)) {
///在当前节点,如果child与自己的hitTest命中测试有一个是返回true,就加入到HitTestResult中
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
复制代码
- 调用父类的
hitTest方法,也就是GestureBinding的hitTest方法:
@override // from HitTestable void hitTest(HitTestResult result, Offset position) { result.add(HitTestEntry(this)); } 复制代码
经过一系列的hitTest后,通过一下判断:
if (hitTestResult != null || event is PointerHoverEvent || event is PointerAddedEvent || event is PointerRemovedEvent) { assert(event.position != null); dispatchEvent(event, hitTestResult); } 复制代码
调用到GestureBinding的dispatchEvent方法:
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) { ... for (final HitTestEntry entry in hitTestResult.path) { try { entry.target.handleEvent(event.transformed(entry.transform), entry); } catch (exception, stack) { .... )); } } } 复制代码
该方法就是遍历_path中的每个HitTestEntry,取出target进行事件的分发,而HitTestTarget除了几个Binding,其具体都是由RenderObject实现的,所以也就是对每个RenderObject节点进行事件分发,也就是我们说的“事件冒泡”,冒泡的第一个节点是最小child节点(最内部的组件),最后一个是GestureBinding。
值得注意的是,Flutter中并没有机制去取消或者去停止事件进一步的分发,我们只能在
hitTestBehavior中去调整组件在命中测试期内应该如何表现,而且只有通过命中测试的组件才能触发事件。
所以,_handlePointerEvent方法主要就是不断通过hitTest方法计算出所需的HitTestResult,然后再通过dispatchEvent对事件进行分发。
以上是简单的对Flutter的事件分发进行一个分析,具体到我们组件层面的使用,Flutter内部还做了较多的处理,在Flutter中,具备手势点击事件的组件的实现,可直接使用的组件层面主要分为以下(也可以其它纬度分类):
- 直接使用Listener组件监听事件
- 其他基于对手势识别器
GestureRecoginzer的实现:
- 使用
GestureDetector组件 - 使用
FloatButton、InkWell...等结构为:xx--xx->GestureDecector->Listener这种依托于GestureDecector->Listener的组件 - 类似
Switch,内部也是基于GestureRecoginzer实现的组件
针对第二点,在遇到多个手势冲突的时候,为了确定最终响应的手势,还得经过一个"手势竞技场"的过程,也就是在上图中recognizer手势识别器以上部分的调用结构,在"手势竞技场"中胜利的才能最终将事件响应组件层面。
以上为手势事件的一个大概的流程分析,了解了其原理与基本流程,能更好的帮助我们去完成自动埋点功能的实现。如果对Flutter手势事件原理还有不清楚的可以去查阅其它资料或者留言交流。
5.AOP
通过上面的描述,首先我们肯定是可以在响应的单击、双击、长按回调函数通过直接调用SDK埋点代码来获得我们的数据,那么如何才能实现这一步的自动化呢?
AOP:在指定的切点插入指定的代码,将所有的代码插桩逻辑几种在一个SDK内处理,可以最大程度的不侵入我们的业务。
目前阿里闲鱼开源的一款面向Flutter设计的AOP框架:Aspectd,具体的使用不多做介绍,看github地址即可。
通过上述手势事件的分析,选择以下两个切入点(当然也有其它的切入方式):
HitTestTarget的handleEvent(PointerEvent event,HitTestEntry entry)方法;GestureRecognizer的invokeCallback<T>(String name,RecognizerCallback<T> callback,{String debugReport})方法;
其代码大致如下所示:
@Call("package:flutter/src/gestures/hit_test.dart", "HitTestTarget", "-handleEvent") @pragma("vm:entry-point") dynamic hookHitTestTargetHandleEvent(PointCut pointCut) { dynamic target = pointCut.target; PointerEvent pointerEvent = pointCut.positionalParams[0]; HitTestEntry entry = pointCut.positionalParams[1]; curPointerCode = pointerEvent.pointer; if (target is RenderObject) { if (curPointerCode > prePointerCode) { clearClickRenderMapData(); } if (!clickRenderMap.containsKey(curPointerCode)) { clickRenderMap[curPointerCode] = target; } } prePointerCode = curPointerCode; target.handleEvent(pointerEvent, entry); }
@Call("package:flutter/src/gestures/recognizer.dart", "GestureRecognizer", "-invokeCallback") @pragma("vm:entry-point") dynamic hookinvokeCallback(PointCut pointcut) { var result = pointcut.proceed(); if (curPointerCode > preHitPointer) { String argumentName = pointcut.positionalParams[0];
if (argumentName == 'onTap' || argumentName == 'onTapDown' || argumentName == 'onDoubleTap') { RenderObject clickRender = clickRenderMap[curPointerCode]; if (clickRender != null) { DebugCreator creator = clickRender.debugCreator; Element element = creator.element; //通过element获取路径 String elementPath = getElementPath(element); ///丰富采集时间 richJsonInfo(element, argumentName, elementPath); } preHitPointer = curPointerCode; } }
return result; } 复制代码
大体的实现思路如下:
- 通过Map记录事件唯一的
pointer标识符与响应的RenderObject的映射关系,只记录_path中的第一个,也就是命中测试的最小child,且记录下当前事件序列的pointer(pointer在一个事件序列中是唯一的值,每发生一次手势事件,它会自增1); - 在
GestureRecognizer的invokeCallback<T>(String name,RecognizerCallback<T> callback,{String debugReport})方法中,通过上面记录的的pointer,在Map中取出RenderObject,取debugCreator属性得到Element,再得到对应的widget;
在上述第2步中,其实存在一个问题,就是RenderObject的debugCreator字段,这个字段表示负责创建此render object的对象,源码中创建过程写在aessert中,所以其实只能在debug模式下获取到,它在源码中实际创建位置在RenderObjectElement的mount,在update执行更新的时候同样也会更新:
@override void mount(Element parent, dynamic newSlot) { super.mount(parent, newSlot); //省略部分代码... _renderObject = widget.createRenderObject(this); //省略部分代码... assert(() { //assert部分会在Release的时候删除 _debugUpdateRenderObjectOwner(); return true; }()); //省略部分代码... }
void _debugUpdateRenderObjectOwner() { assert(() { //将当前Element传入到DebugCreator中保存。RenderObjectElement继承Element _renderObject.debugCreator = DebugCreator(this); return true; }()); } 复制代码
为了让我们在AOP的时候,在Release模式下也能获取到这个数据,所以我们要特殊处理。既然在源码中它只能在debug下创建,我们就创造条件让它在Release下也创建。
@Execute('package:flutter/src/widgets/framework.dart','Element','-mount') @pragma('vm:entry-point') static dynamic hookElementMount(PointCut pointCut){ dynamic obj = pointCut.proceed; Element element = pointCut.target; if(kReleaseMode||kProfileMode){ //release和profile模式创建这个属性 element.renderObject.debugCreator = DebugCreator(element); } }
@Execute('package:flutter/src/widgets/framework.dart','Element','-update') @pragma('vm:entry-point') static dynamic hookElementUpdate(PointCut pointCut){ dynamic obj = pointCut.proceed; Element element = pointCut.target; if(kReleaseMode||kProfileMode){ //release和profile模式创建这个属性 element.renderObject.debugCreator = DebugCreator(element); } } 复制代码
对debugCreator字段处理完成后,我们就可以根据RenderObject获取对应的Element,获取到Element也就可以去计算组件的path id了。
通过以上操作,在实际中,我们对一个GestureDetector进行点击测试后,得到如下结果:
GestureDetector[0]/Column[0]/Contain[0]/BodyBuilder[0]/MediaQuery[0]/LayoutId[0]/CustomMultiChildLayout[0]/AnimatedBuilder[0]/DefaultTextStyle[0]/AnimatedDefaultTextStyle[0]/_InkFeatures[0]/NotificationListener[0]/PhysicalModel[0]/AnimatedPhysicalModel[0]/Material[0]/PrimaryScrollController[0]/_ScaffoldScope[0]/Scaffold[0]/MyHomePage[0].../MyApp[0] 复制代码
经过对比发现,这似乎确实是我们代码中创建的组件的路径没错,但是好像中间多了很多奇怪的组件路径,这似乎不是我们自己创建的,这里还是存在一些问题要优化。
6.关于组件ID的优化
- 组件路径ID过长:
组件的路径ID很长。因为Flutter布局嵌套包装的特点,如果一直向上搜索父亲节点,会一直搜索到MyApp这里,中间还会包含很多系统内部创建的组件。
- 不同平台特性:(==去掉这点,无需优化,因为平台特性只会出现在系统内部节点,自己编写的除非有特别的判断,否则不会出现差异性==)
在不同的平台,为了保持某些平台的特性风格,可能会出现路径中某个节点不一致的情况(比如在IOS平台的路径可能会出现一个侧滑的节点,其他平台没有)。例如以"Cupertino"、"Material"开头的这种组件,要选择屏蔽掉差异。
- 动态插入Widget不稳定
根据上面定义的规则,在页面元素不发生变动的情况下,基本上是能保证"稳定性"与"唯一性",但是如果页面元素发生动态变化,或者在不同的版本之间UI进行了改版,此时我们定义的规则就会变的不够稳定,也可能不再唯一,比如下图所示:
[图片上传中...(image-85fa0d-1605191361072-8)]
在插入一个Widget后,我们的GestureDetector的路径变成了Contain[0]/Column[0]/Contain[2]/GestureDetector[0],与之前相比发生了变化,这点优化比较简单:将同级兄弟节点的位置,变成相同类型的组件的位置。优化后的组件路径为:Contain[0]/Column[0]/Contain[1]/GestureDetector[0]。这样在插入一个非同类型的Widget后,其路径依旧不变,但如果插入的是同类型的还是会发生改变,所以这个是属于相对的稳定。
那么剩下的问题如何优化呢?
7.Dart元编程解决遗留问题
问题1:我们实际获取到的路径并不是我们在代码中创建的组件路径,比如:
//我们自己代码创建一个Contain @override Widget build(BuildContext context){ return Contain( child:Text('text'), ); } //实际上Contain的内部build函数,会做层层的包装,其他组件也是类似情况 @override Widget build(BuildContext context) { Widget current = child; if (child == null && (constraints == null || !constraints.isTight)) { current = LimitedBox( maxWidth: 0.0, maxHeight: 0.0, child: ConstrainedBox(constraints: const BoxConstraints.expand()), ); } ...省略部分代码 if (alignment != null) current = Align(alignment: alignment, child: current); ...省略部分代码 return current; } 复制代码
因为这个情况,会导致出现三个情况:
- 我们在用上述方式获取组件路径的时候,中间会夹杂很多我们并不那么关心的组件路径,即使这些确实是在路径上的的组件,我们实际上只想要关注我们创建的那部分,关键是如何去除"多余组件路径"。
- 系统组件有时内部为了在一些情况下支持各个平台特性,还会出现使用各自不同的组件,这种差异需要屏蔽。
- 因为Flutter独特的嵌套方式,每个组件在搜索父节点时最终会搜索到main中,实际其实我们只需要以当前页面为划分即可。
如何解决呢?注意到当我们使用Flutter自带的工具Flutter Inspector观测我们创建的页面时,出现的是我们想要的组件展示情况:
[图片上传中...(image-54a276-1605191361071-7)]
[图片上传中...(image-4ea4a5-1605191361071-6)]
通过图中可以看到,widgets的展示形式完整的表示了我们自己页面代码中创建widget的结构,那么这个是如何实现的呢?
实际上,这个是通过一个WidgetInspectorService的服务来实现的,一个被GUI工具用来与WidgetInspector交互的服务。在Foundation/Binding.dart中通过initServiceExtensions注册,而且只有在debug环境下才会注册这个拓展服务。
通过对官方开源的dev-tools源码的分析,其应用层面的关键方法如下:
// Returns if an object is user created. //返回该对象是否自己创建的(这里我们针对的是widget) bool _isLocalCreationLocation(Object object) { final _Location location = _getCreationLocation(object); if (location == null) return false; return WidgetInspectorService.instance._isLocalCreationLocation(location); }
/// Creation locations are only available for debug mode builds when
/// the --track-widget-creation flag is passed to flutter_tool. Dart 2.0 is
/// required as injecting creation locations requires a
/// Dart Kernel Transformer.
///
/// Currently creation locations are only available for [Widget] and [Element].
_Location _getCreationLocation(Object object) {
final Object candidate = object is Element ? object.widget : object;
return candidate is _HasCreationLocation ? candidate._location : null;
}
bool _isLocalCreationLocation(_Location location) { if (location == null || location.file == null) { return false; } final String file = Uri.parse(location.file).path;
// By default check whether the creation location was within package:flutter. if (_pubRootDirectories == null) { // TODO(chunhtai): Make it more robust once // github.com/flutter/flu… is fixed. return !file.contains('packages/flutter/'); } for (final String directory in _pubRootDirectories) { if (file.startsWith(directory)) { return true; } } return false; } 复制代码
方法中出现的两个关键类_Location与_HasCreationLocation,是在编译期通过Dart Kernel Transformer实现的,与Android中的ASM实现Transform类似,Dart在编译期间也是有一个个的Transform来实现一些特定的操作的,这部分可以在Dart的源码中找到。
而widget_inspctor的这个功能,就是在debug模式的编译期间,通过一个特定的Transform,让所有的Widget 实现了抽象类_HasCreationLocation,同时改造了Widget的构造器函数,添加一个命名参数(_Location类型),通过AST,给_Location属性赋值,实现transform的转换。
但是,这个功能是只能在debug模式下开启的,我们要达到这个效果,只能自己实现一个Transform,支持在非debug模式下也能使用。而且,我们可以直接利用aspectd的已有功能,稍微改造一下,添加一个自己的Transform,而且不需要添加widget创建的行列等复杂的信息,只需要能够区分widget是开发者自己项目创建的即可,也就是只需要一个标识即可。
同样的在实现的过程中也有几点要注意:
- 对于创建widget的时候,如果加了
const修饰,比如下面示例,是需要单独作为一个Transform来处理的。
Text widget = const Text('文字'); Contain( child:const Text('文字'), ); 复制代码
-
在debug下可以用
TreeNode的Location字段做区分,但是在release下这个字段是null,不能按照这个区分出自己项目创建的widget。 -
如果使用Aspectd的话,自己添加的改造Transform要添加在Aspectd内部实现的几个Transform之前。因为Aspectd提供的比如call api,在用在构造函数的时候,会将方法调用处替换掉,我们如果在这个后面注入会无效。所以转换的顺序应该是修改普通构造在最前面,其次是处理常量声明表达式,最后是Aspectd自己的转换。
参考源码的track_widget_constructor_locations.dart的实现,Transform实现的关键代码如下:
- 自己定义的一个类,让widget实现这个类,注意该类定义的时候需要我们在
main方法中直接或者间接的使用到,对应的_resolveFlutterClasse方法也要修改。
void _resloveFlutterClasses(Iterable libraries){ for(Library library in libraries){ final Uri importUri = library.importUri; if(importUri != null && importUri.scheme == 'package'){ //自己定义类的完整路径,比如是:example/local_widget_track_class.dart if(importUri.path = 'example/local_widget_track_class.dart'){ for(Class cls in library.classes){ //定义的类名,比如是:LocalWidgetLocation if(cls.name = 'LocalWidgetLocation'){ _localWidgetLocation = cls; } } }else if(importUri.path == 'flutter/src/widgets/framework.dart'|| ....){ ... } } } } 复制代码
- 继承
Transformer主要需要实现visitStaticInvocation、visitConstructorInvocation方法:
@override StaticInvocation visitStaticInvocation(StaticInvocation node) { node.transformChildren(this); final Procedure target = node.target; if (!target.isFactory) { return node; } final Class constructedClass = target.enclosingClass; if (!_isSubclassOfWidget(constructedClass)) { return node; }
_addLocationArgument(node, target.function, constructedClass); return node; }
@override ConstructorInvocation visitConstructorInvocation(ConstructorInvocation node) { node.transformChildren(this); final Constructor constructor = node.target; final Class constructedClass = constructor.enclosingClass; if(_isSubclassOfWidget(constructedClass)){ _addLocationArgument(node, constructor.function, constructedClass); return node; }
void _addLocationArgument(InvocationExpression node, FunctionNode function, Class constructedClass) { _maybeAddCreationLocationArgument( node.arguments, function, ConstantExpression(BoolConstant(true)), ); }
写在最后
由于本文罗列的知识点是根据我自身总结出来的,并且由于本人水平有限,无法全部提及,欢迎大神们能补充~
将来我会对上面的知识点一个一个深入学习,也希望有童鞋跟我一起学习,一起进阶。
提升架构认知不是一蹴而就的,它离不开刻意学习和思考。
**这里,笔者分享一份从架构哲学的层面来剖析的视频及资料分享给大家,**梳理了多年的架构经验,筹备近1个月最新录制的,相信这份视频能给你带来不一样的启发、收获。
领取方式:点击这里获取免费架构视频资料
最近还在整理并复习一些Android基础知识点,有问题希望大家够指出,谢谢。
希望读到这的您能转发分享和关注一下我,以后还会更新技术干货,谢谢您的支持!
转发+点赞+关注,第一时间获取最新知识点
Android架构师之路很漫长,一起共勉吧!