总结
最后小编想说:不论以后选择什么方向发展,目前重要的是把Android方面的技术学好,毕竟其实对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!
这里附上我整理的几十套腾讯、字节跳动,京东,小米,头条、阿里、美团等公司19年的Android面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。
由于篇幅有限,这里以图片的形式给大家展示一小部分。
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
技术进阶之路很漫长,一起共勉吧~
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
复制代码
可以看到通过这种方式,如果是可以通过在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
// https://github.com/flutter/flutter/issues/32660 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)),
);
}
void _maybeAddCreationLocationArgument(
Arguments arguments,
FunctionNode function,
Expression creationLocation,
) {
if (_hasNamedArgument(arguments, _creationLocationParameterName)) {
return;
}
if (!_hasNamedParameter(function, _creationLocationParameterName)) {
if (function.requiredParameterCount !=
function.positionalParameters.length) {
return;
}
}
final NamedExpression namedArgument = NamedExpression(_creationLocationParameterName, creationLocation);
namedArgument.parent = arguments;
arguments.named.add(namedArgument);
}
复制代码
- 对于加了
const
修饰的Widget,单独作为一个Transform来处理注入属性,该Transform需要重写visitConstantExpression
方法,通过给InstanceConstant
的filedValue
字段添加一个值达到我们需要的效果。
Text widget = const Text(‘文字’);
Contain(
child:const Text(‘文字’),
);
//Transform示例代码如下:
@override
TreeNode visitConstantExpression(ConstantExpression node) {
node.transformChildren(this);
if (node.constant is InstanceConstant) {
InstanceConstant instanceConstant = node.constant;
Class clsNode = instanceConstant.classReference.node;
if (clsNode is Class && _isSubclassOf(clsNode, _widgetClass)) {
final Name fieldName = Name(
_locationFieldName,
_localCreatedClass.enclosingLibrary,
);
Reference useReference = _localFieldReference;
final Field locationField =
Field(fieldName, isFinal: true, reference: useReference,isConst: true);
useReference.node = locationField;
Constant constant = BoolConstant(true);
instanceConstant.fieldValues
.putIfAbsent(useReference, () => constant);
}
}
return super.visitConstantExpression(node);
}
复制代码
以上代码的实现思路其实并不难,可以对Dart源码中的类似实现多参考参考。通过上述的Transform转换,我们可以完美的解决『多余组件路径』的问题,现在我们得到的路径是实打实的我们自己代码创建的widget路径:
GestureDetector[0]/ Column[0]/Center[0]/Scaffold[0]/MyHomePage[0]/MaterialApp[0]/MyApp[0]
复制代码
学习分享
在当下这个信息共享的时代,很多资源都可以在网络上找到,只取决于你愿不愿意找或是找的方法对不对了
很多朋友不是没有资料,大多都是有几十上百个G,但是杂乱无章,不知道怎么看从哪看起,甚至是看后就忘
如果大家觉得自己在网上找的资料非常杂乱、不成体系的话,我也分享一套给大家,比较系统,我平常自己也会经常研读。
2021最新上万页的大厂面试真题
七大模块学习资料:如NDK模块开发、Android框架体系架构…
只有系统,有方向的学习,才能在段时间内迅速提高自己的技术。
这份体系学习笔记,适应人群:
第一,学习知识比较碎片化,没有合理的学习路线与进阶方向。
第二,开发几年,不知道如何进阶更进一步,比较迷茫。
第三,到了合适的年纪,后续不知道该如何发展,转型管理,还是加强技术研究。如果你有需要,我这里恰好有为什么,不来领取!说不定能改变你现在的状态呢!
由于文章内容比较多,篇幅不允许,部分未展示内容以截图方式展示 。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
也分享一套给大家,比较系统,我平常自己也会经常研读。
2021最新上万页的大厂面试真题
[外链图片转存中…(img-7xsRuAtO-1715365805789)]
七大模块学习资料:如NDK模块开发、Android框架体系架构…
[外链图片转存中…(img-ybSyRu2Q-1715365805789)]
只有系统,有方向的学习,才能在段时间内迅速提高自己的技术。
这份体系学习笔记,适应人群:
第一,学习知识比较碎片化,没有合理的学习路线与进阶方向。
第二,开发几年,不知道如何进阶更进一步,比较迷茫。
第三,到了合适的年纪,后续不知道该如何发展,转型管理,还是加强技术研究。如果你有需要,我这里恰好有为什么,不来领取!说不定能改变你现在的状态呢!
由于文章内容比较多,篇幅不允许,部分未展示内容以截图方式展示 。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!