当前位置: 首页 > news >正文

flutter 专题四十七 Flutter 应用启动流程分析

众所周知,任何应用程序的启动都是从main()函数开始的,Flutter也不例外,main.dart文件的main函数开始的,代码如下。

void main() => runApp(MyApp());

main函数则调用的是runApp函数,源码如下。

void runApp(Widget app) {WidgetsFlutterBinding.ensureInitialized()..scheduleAttachRootWidget(app)..scheduleWarmUpFrame();
}

上面代码中,用到了一个级联运算符(…),代表的含义是WidgetsFlutterBinding.ensureInitialized()生成的对象再调用scheduleAttachRootWidget和scheduleWarmUpFrame两个方法。先简单介绍下这三行代码的作用。

  1. 生成一个Flutter Engine(C++代码)和Flutter Framework(Dart代码)的中间桥接对象;
  2. 根据app生成一个渲染树;
  3. 绘制热身帧, 将渲染树生成的Layer图层通过Flutter Engine渲染到Flutter View上。

下面,我们分别对WidgetsFlutterBinding、scheduleAttachRootWidget和scheduleWarmUpFrame来分析Flutter的启动流程。

一、 WidgetsFlutterBinding

点击打开WidgetsFlutterBinding类,该类的代码如下所示。

class WidgetsFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {static WidgetsBinding ensureInitialized() {if (WidgetsBinding.instance == null)//构造方法调用WidgetsFlutterBinding();//返回对象WidgetsBindingreturn WidgetsBinding.instance!;}
}

可以看到,WidgetsFlutterBinding继承自BindingBase,混入了GestureBinding,SchedulerBinding,ServicesBinding,PaintingBinding,SemanticsBinding,RendererBinding和WidgetsBinding7个mixin。

ensureInitialized方法就是获取WidgetsBinding.instance单例的过程。由于mixin没有构造方法,所以WidgetsFlutterBinding()实际调用的是父类BindingBase的构造方法,代码如下。

BindingBase() {// 调用initInstancesinitInstances();
}

WidgetsFlutterBinding混入的7个mixin都重写了initInstances()方法,所以他们各自的initInstances()都会被调用,调用的逻辑如下图所示。
在这里插入图片描述
通过使用mixin设计,实现了高内聚低耦合和模块职责单一,并且通过mixin依赖,实现了initInstances()方法调用的串行按执行顺序。

1.1 FlutterView

FlutterView是Flutter Engine给Flutter Framework开放的用户界面和事件的接口,可以把Flutter Framework理解为围绕FlutterView的一个处理框架。所以其重要性不言而喻。上面WidgetsFlutterBinding混入的多个mixin主要就是处理window对象(即FlutterView对象的)的回调事件和提交渲染内容,所以理解FlutterView是非常有必要的。事实上,window对象是BindingBase的一个变量。

<!-- BindingBase -->
ui.SingletonFlutterWindow get window => ui.window;

ui.window是PlatformDispatcher.instance中windowId为0的主window,如下所示。

<!-- window.dart -->
final SingletonFlutterWindow window = SingletonFlutterWindow._(0, PlatformDispatcher.instance);

SingletonFlutterWindow的继承关系如下。

abstract class FlutterView {}
class FlutterWindow extends FlutterView {}
class SingletonFlutterWindow extends FlutterWindow {}

FlutterView是一个抽象类,完整的代码如下。

abstract class FlutterView {PlatformDispatcher get platformDispatcher;ViewConfiguration get viewConfiguration;double get devicePixelRatio => viewConfiguration.devicePixelRatio;Rect get physicalGeometry => viewConfiguration.geometry;Size get physicalSize => viewConfiguration.geometry.size; WindowPadding get viewInsets => viewConfiguration.viewInsets;WindowPadding get viewPadding => viewConfiguration.viewPadding;WindowPadding get systemGestureInsets => viewConfiguration.systemGestureInsets;WindowPadding get padding => viewConfiguration.padding;void render(Scene scene) => _render(scene, this);void _render(Scene scene, FlutterView view) native 'PlatformConfiguration_render';
}

FlutterView有几个重要的属性和方法。

  1. PlatformDispatcher是FlutterView的核心,FlutterView是对它的一层封装,是真正向Flutter Engine发送消息和得到回调的类;
  2. ViewConfiguration是Platform View的一些信息的描述,其中主要包括几个信息。
  • devicePixelRatio:物理像素和虚拟像素的比值。这个和手机有关,譬如iPhone手机可能是2或者3,Android手机就有可能是个小数,譬如3.5等。
  • geometry:Flutter渲染的View在Native platform中的位置和大小。
  • viewInsets:各个边显示的内容和能显示内容的边距大小;譬如:没有键盘的时候viewInsets.bottom为0,当有键盘的时候键盘挡住了一些区域,键盘底下无法显示内容,所以viewInsets.bottom就变成了键盘的高度。
  • padding:系统UI的显示区域如状态栏,这部分区域最好不要显示内容,否则有可能被覆盖了。譬如,很多iPhone顶部的刘海区域,padding.top就是其高度。
  • viewPadding:viewInsets和padding的和。
FlutterWindow

FlutterWindow没有什么功能,只是封装了一个构造方法,接下来我们来看看SingletonFlutterWindow的一些重要代码。

onMetricsChanged
evicePixelRatio, physicalSize, padding和viewInsets等的变化会触发的回调onMetricsChanged()函数。

VoidCallback? get onMetricsChanged => platformDispatcher.onMetricsChanged;
set onMetricsChanged(VoidCallback? callback) {platformDispatcher.onMetricsChanged = callback;
}

手机设置的地区(如中国大陆),以及设置的地区更改后收到的回调onLocaleChanged,如下所示。

Locale get locale => platformDispatcher.locale;VoidCallback? get onLocaleChanged => platformDispatcher.onLocaleChanged;
set onLocaleChanged(VoidCallback? callback) {platformDispatcher.onLocaleChanged = callback;
}  

文字缩放倍率变化后会回调onTextScaleFactorChanged。

  VoidCallback? get onTextScaleFactorChanged => platformDispatcher.onTextScaleFactorChanged;set onTextScaleFactorChanged(VoidCallback? callback) {platformDispatcher.onTextScaleFactorChanged = callback;}

除了上面的这几个外,其他的调用都会回调某个函数。而FlutterView对象window本质上是对PlatformDispatcher的封装,从PlatformDispatcher获取一些界面相关信息,获取从Flutter Engine 发送来的事件,然后触发和转发相应的回调方法。

1.2 BindingBase

BindingBase是一个抽象类,源码如下。

abstract class BindingBase {BindingBase() {// 初始化initInstances();}// 单例windowui.SingletonFlutterWindow get window => ui.window;
}

BindingBase主要有两个作用:

  • 构造函数调用initInstances方法,其实是为了依次调用7个mixin的initInstances方法。
  • 提供了一个window单例。
1.3 RendererBinding

RendererBinding的功能主要和渲染树相关,打开RendererBinding的源码,首先来看initInstances()初始化方法。

void initInstances() {super.initInstances();_instance = this;// 1_pipelineOwner = PipelineOwner(onNeedVisualUpdate: ensureVisualUpdate,onSemanticsOwnerCreated: _handleSemanticsOwnerCreated,onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed,);// 2window..onMetricsChanged = handleMetricsChanged..onTextScaleFactorChanged = handleTextScaleFactorChanged..onPlatformBrightnessChanged = handlePlatformBrightnessChanged..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged..onSemanticsAction = _handleSemanticsAction;// 3initRenderView();_handleSemanticsEnabledChanged();// 4addPersistentFrameCallback(_handlePersistentFrameCallback);// 5initMouseTracker();
}

在上面的代码中,我们来依次分析下都干了啥。

  1. 生成了一个PipelineOwner对象。它的主要作用是收集需要更新的RenderObjects,然后借助RendererBinding进行UI刷新。
  2. 处理window对象的onMetricsChanged、onTextScaleFactorChanged等回调方法。
  3. initRenderView生成了一个RenderView对象renderView,然后将renderView设置为_pipelineOwner的根节点rootNode。
  4. addPersistentFrameCallback调用的是SchedulerBinding的方法,PersistentFrameCallback主要执行的是Widget的build / layout / paint等一系列操作。
<!-- SchedulerBinding.dart -->
void addPersistentFrameCallback(FrameCallback callback) {_persistentCallbacks.add(callback);
}

5,生成一个MouseTracker对象,处理hitTestResult或者PointerAddedEvent和PointerRemovedEvent事件。

@visibleForTestingvoid initMouseTracker([MouseTracker tracker]) {_mouseTracker?.dispose();_mouseTracker = tracker ?? MouseTracker(pointerRouter, renderView.hitTestMouseTrackers);}
1.4 SemanticsBinding

Semantics主要就是描述应用程序中的UI信息,而在iOS和Android主要是用于读屏使用,帮助有视力障碍的人使用,SemanticsBinding的源码如下。

mixin SemanticsBinding on BindingBase {void initInstances() {super.initInstances();_instance = this;_accessibilityFeatures = window.accessibilityFeatures;}
}
1.5 PaintingBinding

PaintingBinding是一个处理图片缓存的mixin,下面我们来看看PaintingBinding的主要代码,首先看一下initInstances()初始化方法。

mixin PaintingBinding on BindingBase, ServicesBinding {@overridevoid initInstances() {super.initInstances();_instance = this;_imageCache = createImageCache();shaderWarmUp?.execute();
}
  • _imageCache是图片缓存的类,最大能存1000张图片,最大内存是100MB;
  • shaderWarmUp?.execute()是一个异步方法,初始化了一个默认的着色器,避免需要着色器的时候再初始化出现掉帧现象。

同时,createImageCache()方法内部还调用了handleMemoryPressure(),如下所示。

void handleMemoryPressure() {super.handleMemoryPressure();imageCache?.clear();}

之所以这么处理,是因为图片存储非常耗内存,所以当App内存警告时需要清除掉缓存。

1.6 ServicesBinding

ServicesBinding的主要功能是接收MethodChannel和SystemChannels传递过来的消息,下面来看看ServicesBinding的主要代码。首先,还是看看initInstances()的代码。

void initInstances() {super.initInstances();_instance = this;// 1_defaultBinaryMessenger = createBinaryMessenger();// 2_restorationManager = createRestorationManager();// 3window.onPlatformMessage = defaultBinaryMessenger.handlePlatformMessage;// 4initLicenses();// 5SystemChannels.system.setMessageHandler(handleSystemMessage);
}

下面,分别来看一下每一行代码的作用。
1,createBinaryMessenger()创建了一个MethodChannel;
2,createRestorationManager()创建了一个RestorationManager用于恢复界面数据的功能;
3,通过第一步创建的_defaultBinaryMessenger实现和Plugin插件的通信
4,initLicenses是给一些文件加上Licenses说明;
5,发送SystemChannels传递过来消息;

1.7 SchedulerBinding

SchedulerBinding主要处理任务调度,Flutter的调度分为idle、transientCallbacks、midFrameMicrotasks、persistentCallbacks和postFrameCallbacks几个阶段。

  • idle:此阶段没有绘制帧任务处理,主要处理Task,Microtask,Timer回调,用户输入和手势,以及其他一些任务。
  • transientCallbacks:这个阶段主要处理动画状态的计算和更新。
  • midFrameMicrotasks:这个阶段处理transientCallbacks阶段触发的Microtasks。
  • persistentCallbacks:这个阶段主要处理build/layout/paint等操作。
  • postFrameCallbacks:这个阶段主要在处理下一帧之前,做一些清理工作或者准备工作。

接下来,我们看一下handleAppLifecycleStateChanged()方法。

AppLifecycleState? get lifecycleState => _lifecycleState;
void handleAppLifecycleStateChanged(AppLifecycleState state) {assert(state != null);_lifecycleState = state;switch (state) {case AppLifecycleState.resumed:case AppLifecycleState.inactive:_setFramesEnabledState(true);break;case AppLifecycleState.paused:case AppLifecycleState.detached:_setFramesEnabledState(false);break;}
}void _setFramesEnabledState(bool enabled) {if (_framesEnabled == enabled)return;_framesEnabled = enabled;if (enabled)scheduleFrame();
}

上面代码的主要作用是监听生命周期变化,生命周期的状态改变设置_framesEnabled的值,如果_framesEnabled为false停止刷新界面;如果_framesEnabled为true调用scheduleFrame向Native Platform请求刷新视图的请求。接下来,再看一下scheduleFrame()方法。

void scheduleFrame() {if (_hasScheduledFrame || !framesEnabled)return;// 1  ensureFrameCallbacksRegistered();// 2window.scheduleFrame();_hasScheduledFrame = true;}void ensureFrameCallbacksRegistered() {window.onBeginFrame ??= _handleBeginFrame;window.onDrawFrame ??= _handleDrawFrame;
}

其中,ensureFrameCallbacksRegistered()是先确保向window注册了onBeginFrame和 onDrawFrame两个重要回调函数。而window.scheduleFrame()是向Native platform发起一个刷新视图的请求;发送这个请求后,Native platform会在合适的时间调用onBegineFrame和onDrawFrame这两个函数, 这两个回调会完成刷新视图所需的操作,比如更新widgets、动画、和完成渲染等。这些都完成后再调用window.scheduleFrame(),一直循环下去,直到程序退出前台或者程序退出。

接下来看一下handleBeginFrame()方法的源码。

void handleBeginFrame(Duration? rawTimeStamp) {_hasScheduledFrame = false;try {_schedulerPhase = SchedulerPhase.transientCallbacks;final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;_transientCallbacks = <int, _FrameCallbackEntry>{};callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {if (!_removedIds.contains(id))_invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp!, callbackEntry.debugStack);});_removedIds.clear();} finally {_schedulerPhase = SchedulerPhase.midFrameMicrotasks;}
}

handleBeginFrame的功能是执行_transientCallbacks中的所有函数。向transientCallbacks中添加回调主要是Ticker.scheduleTick方法,是动画框架的一部分。

接着,再看一下handleDrawFrame()方法的代码。

void handleDrawFrame() {try {// 1_schedulerPhase = SchedulerPhase.persistentCallbacks;for (final FrameCallback callback in _persistentCallbacks)_invokeFrameCallback(callback, _currentFrameTimeStamp!);// 2_schedulerPhase = SchedulerPhase.postFrameCallbacks;final List<FrameCallback> localPostFrameCallbacks =List<FrameCallback>.from(_postFrameCallbacks);_postFrameCallbacks.clear();for (final FrameCallback callback in localPostFrameCallbacks)_invokeFrameCallback(callback, _currentFrameTimeStamp!);} finally {_schedulerPhase = SchedulerPhase.idle;_currentFrameTimeStamp = null;}
}final List<FrameCallback> _persistentCallbacks = <FrameCallback>[];
final List<FrameCallback> _postFrameCallbacks = <FrameCallback>[];  

handleDrawFrame中执行了两种回调函数,persistentCallbacks和 postFrameCallbacks中所有的回调函数。

1.8GestureBinding

GestureBinding主要处理用户的各种手势事件,初始化方法如下。

mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, HitTestTarget {void initInstances() {super.initInstances();_instance = this;window.onPointerDataPacket = _handlePointerDataPacket;}
}

GestureBinding用_handlePointerDataPacket来处理window的onPointerDataPacket方法,_handlePointerDataPacket的源码如下。

void _handlePointerDataPacket(ui.PointerDataPacket packet) {_pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio));if (!locked)_flushPointerEventQueue();
}void _flushPointerEventQueue() {while (_pendingPointerEvents.isNotEmpty)handlePointerEvent(_pendingPointerEvents.removeFirst());
}void handlePointerEvent(PointerEvent event) {_handlePointerEventImmediately(event);
}void _handlePointerEventImmediately(PointerEvent event) {HitTestResult? hitTestResult;if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) {// 1hitTestResult = HitTestResult();// 2hitTest(hitTestResult, event.position);// 3if (event is PointerDownEvent) {_hitTests[event.pointer] = hitTestResult;}} else if (event is PointerUpEvent || event is PointerCancelEvent) {// 4hitTestResult = _hitTests.remove(event.pointer);} else if (event.down) {hitTestResult = _hitTests[event.pointer];}if (hitTestResult != null ||event is PointerAddedEvent ||event is PointerRemovedEvent) {// 5    dispatchEvent(event, hitTestResult);}
}

_handlePointerDataPacket通过一系列的方法调用,最后调用_handlePointerEventImmediately方法。当event是PointerDownEvent或者PointerHoverEvent时,新建一个HitTestResult对象,它有一个path属性,用来记录事件传递所经过的的节点。在这里插入代码片HitTestResult把GestureBinding也加在了path中,相关代码如下。

void hitTest(HitTestResult result, Offset position) {result.add(HitTestEntry(this));
}

如果event是PointerDownEvent,将这个event加入到_hitTests中, 为了在event.down-即移动的时候也能获取到它。

final Map<int, HitTestResult> _hitTests = <int, HitTestResult>{};

当event是PointerUpEvent或者PointerCancelEvent时,将这个event从_hitTests中移除。最后,调用dispatchEvent(event, hitTestResult)方法。如果您有印象,RendererBinding中我们提到过dispatchEvent方法。

<!-- rendererBinding.dart -->
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {_mouseTracker!.updateWithEvent(event,() => hitTestResult ?? renderView.hitTestMouseTrackers(event.position));super.dispatchEvent(event, hitTestResult);
}    

其中,最重要的调用逻辑renderView.hitTestMouseTrackers(event.position)),会从renderview一直遍历它的child,将沿途的Widget加入到path中,涉及的代码如下。

<!-- view.dart -->
HitTestResult hitTestMouseTrackers(Offset position) {final BoxHitTestResult result = BoxHitTestResult();hitTest(result, position: position);return result;
}bool hitTest(HitTestResult result, { required Offset position }) {if (child != null)child!.hitTest(BoxHitTestResult.wrap(result), position: position);result.add(HitTestEntry(this));return true;
}<!-- box.dart -->
bool hitTest(BoxHitTestResult result, { required Offset position }) {if (_size!.contains(position)) {if (hitTestChildren(result, position: position) || hitTestSelf(position)) {result.add(BoxHitTestEntry(this, position));return true;}}return false;}

当遍历完renderView的所有widget后,将hitTestResult返回给GestureBinding的dispatchEvent方法,然后遍历path数组,逐个调用handleEvent方法处理相关的事件,这和原生的触摸事件的处理流程是一致的。

<!-- gestureBinding.dart -->
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {for (final HitTestEntry entry in hitTestResult.path) {entry.target.handleEvent(event.transformed(entry.transform), entry);}
}void handleEvent(PointerEvent event, HitTestEntry entry) {pointerRouter.route(event);if (event is PointerDownEvent) {gestureArena.close(event.pointer);} else if (event is PointerUpEvent) {gestureArena.sweep(event.pointer);} else if (event is PointerSignalEvent) {pointerSignalResolver.resolve(event);}
}
1.9 WidgetsBinding

WidgetsBinding主要处理widget tree的一些逻辑,初始化方法如下。

void initInstances() {super.initInstances();_instance = this;// 1_buildOwner = BuildOwner();buildOwner!.onBuildScheduled = _handleBuildScheduled;// 2window.onLocaleChanged = handleLocaleChanged;window.onAccessibilityFeaturesChanged = handleAccessibilityFeaturesChanged;
}

上面代码的主要作用是初始化了一个BuildOwner对象,执行widget tree的build任务,然后再
执行了一些window的回调。至此,第一步WidgetsFlutterBinding.ensureInitialized()所涉及的知识点就介绍完了。

二、scheduleAttachRootWidget

scheduleAttachRootWidget的主要作用就是和根视图进行绑定。首先,来看一下scheduleAttachRootWidget的源码。

void scheduleAttachRootWidget(Widget rootWidget) {Timer.run(() {attachRootWidget(rootWidget);});
}void attachRootWidget(Widget rootWidget) {_readyToProduceFrames = true;_renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(container: renderView,debugShortDescription: '[root]',child: rootWidget,).attachToRenderTree(buildOwner!, renderViewElement as RenderObjectToWidgetElement<RenderBox>?);
}

scheduleAttachRootWidget异步调用了attachRootWidget方法。attachRootWidget中初始化了一个RenderObjectToWidgetAdapter对象,构造函数传入了renderView和rootWidget。renderView就是RendererBinding的initInstances方法中初始化的那个对象,rootWidget则是MyApp(),即我们看到的界面。

从构造函数的参数名我们可以看到,renderView是容器,rootWidget是这个容器的child。也就是说renderView是所有的Widget的根。

class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWidget {RenderObjectToWidgetAdapter({this.child,required this.container,this.debugShortDescription,
}) : super(key: GlobalObjectKey(container));

RenderObjectToWidgetAdapter对象调用attachToRenderTree方法,把构造的工具_buildOwner传进去。attachToRenderTree的源码如下。

RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T>? element ]) {if (element == null) {owner.lockState(() {// 1element = createElement();element!.assignOwner(owner);});owner.buildScope(element!, () {// 2element!.mount(null, null);});// 3SchedulerBinding.instance!.ensureVisualUpdate();} else {element._newWidget = this;element.markNeedsBuild();}return element!;
}

创建了一个RenderObjectElement的子类RenderObjectToWidgetElement,并将构造工具buildOwner引用给了它。然后,element调用mount方法。下面,我们来看一下RenderObjectToWidgetElement的mount方法。

// RenderObjectToWidgetElement
void mount(Element? parent, dynamic newSlot) {super.mount(parent, newSlot);_rebuild();
}// RenderObjectElement
void mount(Element? parent, dynamic newSlot) {super.mount(parent, newSlot);_renderObject = widget.createRenderObject(this);attachRenderObject(newSlot);_dirty = false;
}// Element 
void mount(Element? parent, dynamic newSlot) {_parent = parent;_slot = newSlot;_lifecycleState = _ElementLifecycle.active;_depth = _parent != null ? _parent!.depth + 1 : 1;if (parent != null)_owner = parent.owner;final Key? key = widget.key;if (key is GlobalKey) {key._register(this);}_updateInheritance();}

RenderObjectToWidgetElement的mount方法先调用Element的mount方法。主要的作用就是设置_parent,_slot,_owner,_depth等的值;

然后,调用RenderObjectElement的mount方法。创建了一个renderObject,其实就是renderView。然后把这个renderObject挂载到RenderObject Tree上,之前的RenderObject Tree没有内容,所以renderView就是根节点。

接下来,再看一下RenderObjectToWidgetElement的_rebuild方法。

void _rebuild() {try {_child = updateChild(_child, widget.child, _rootChildSlot);} catch (exception, stack) {}
}

_rebuild的功能就是Build子Widget,代码如下。

Element? updateChild(Element? child, Widget? newWidget, dynamic newSlot) {final Element newChild;if (child != null) {if (hasSameSuperclass && child.widget == newWidget) {if (child.slot != newSlot)updateSlotForChild(child, newSlot);newChild = child;} else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {if (child.slot != newSlot)updateSlotForChild(child, newSlot);child.update(newWidget);newChild = child;} else {deactivateChild(child);newChild = inflateWidget(newWidget, newSlot);}} else {// 创建ElementnewChild = inflateWidget(newWidget, newSlot);}return newChild;}

updateChild中如果child为null,newWidget不为null, 则会调用newChild = inflateWidget(newWidget, newSlot);

Element inflateWidget(Widget newWidget, dynamic newSlot) {final Key? key = newWidget.key;final Element newChild = newWidget.createElement();newChild.mount(this, newSlot);return newChild;}

inflateWidget先创建一个Element,然后这个Element调用mount方法。事实上,inflateWidget的主要作用就是使用buildOwner对 Widget 树—renderview->MyApp->MaterialApp… 一直Build下去,直到遍历完成。

三、scheduleWarmUpFrame

scheduleWarmUpFrame是SchedulerBinding的一个方法,如下所示。

void scheduleWarmUpFrame() {Timer.run(() {handleBeginFrame(null);});Timer.run(() {handleDrawFrame();if (hadScheduledFrame)scheduleFrame();});lockEvents(() async {await endOfFrame;});
}

scheduleWarmUpFrame就是调用handleBeginFrame和handleDrawFrame方法绘制一帧呈递给GPU去显示。这里需要说明的是,scheduleWarmUpFrame是立即去绘制的,没有等待Vsyn的通知,因为启动的显示要越快越好。后面的lockEvents也是为了等待预约帧绘制完成后再去执行其他的任务。

相关文章:

flutter 专题四十七 Flutter 应用启动流程分析

众所周知&#xff0c;任何应用程序的启动都是从main()函数开始的&#xff0c;Flutter也不例外&#xff0c;main.dart文件的main函数开始的&#xff0c;代码如下。 void main() > runApp(MyApp());main函数则调用的是runApp函数&#xff0c;源码如下。 void runApp(Widget …...

proxmox通过更多的方式创建虚拟机

概述 作为一名资深运维工程师&#xff0c;我们经常需要在 Proxmox 虚拟化平台上创建和管理虚拟机。本文将介绍三种不同的方式在 Proxmox 上创建 Ubuntu 虚拟机&#xff1a; 通过 Proxmox 命令创建虚拟机通过 Shell 脚本自动化创建虚拟机使用 Proxmox API 创建虚拟机 每种方式…...

阿里云 ubuntu22.04 中国区节点安装 Docker

下面是一份在 Ubuntu 22.04 (Jammy) 上&#xff0c;通过阿里云镜像源来安装并配置 Docker 的详细步骤示例&#xff0c;可在中国区阿里云节点使用&#xff1a; 一、卸载旧版本 (如已安装) 如果系统中已经安装了旧版 Docker (可能是 docker、docker-engine、docker.io、containe…...

Java 中 LinkedList 的底层源码

在 Java 的集合框架中&#xff0c;LinkedList是一个独特且常用的成员。它基于双向链表实现&#xff0c;与数组结构的集合类如ArrayList有着显著差异。深入探究LinkedList的底层源码&#xff0c;有助于我们更好地理解其工作原理和性能特点&#xff0c;以便在实际开发中做出更合适…...

【物联网】ARM核常用指令(详解):数据传送、计算、位运算、比较、跳转、内存访问、CPSR/SPSR

文章目录 指令格式&#xff08;重点&#xff09;1. 立即数2. 寄存器位移 一、数据传送指令1. MOV指令2. MVN指令3. LDR指令 二、数据计算指令1. ADD指令1. SUB指令1. MUL指令 三、位运算指令1. AND指令2. ORR指令3. EOR指令4. BIC指令 四、比较指令五、跳转指令1. B/BL指令2. l…...

【LeetCode】5. 贪心算法:买卖股票时机

太久没更了&#xff0c;抽空学习下。 看一道简单题。 class Solution:def maxProfit(self, prices: List[int]) -> int:cost -1profit 0for i in prices:if cost -1:cost icontinueprofit_ i - costif profit_ > profit:profit profit_if cost > i:cost iret…...

【玩转 Postman 接口测试与开发2_017】第13章:在 Postman 中实现契约测试(Contract Testing)与 API 接口验证(下)

《API Testing and Development with Postman》最新第二版封面 文章目录 第十三章 契约测试与 API 接口验证8 导入官方契约测试集合9 契约测试集合的详细配置9.1 env-apiKey 的创建与设置9.2 env-workspaceId 的设置9.3 Mock 服务器及 env-server 的配置9.4 API 测试实例的配置…...

学习threejs,pvr格式图片文件贴图

&#x1f468;‍⚕️ 主页&#xff1a; gis分享者 &#x1f468;‍⚕️ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕️ 收录于专栏&#xff1a;threejs gis工程师 文章目录 一、&#x1f340;前言1.1 ☘️PVR贴图1.2 ☘️THREE.Mesh…...

【人工智能】通用人工智能 AGI

AGI 是 Artificial General Intelligence 的缩写&#xff0c;中文翻译为通用人工智能。与我们常见的**特定人工智能&#xff08;Narrow AI&#xff09;**不同&#xff0c;AGI 是一个更高深、更具野心的目标。 AGI&#xff08;人工通用智能&#xff09;的定义 通用人工智能&am…...

基于PostGIS的省域空间相邻检索实践

目录 前言 一、相关空间检索函数 1、ST_touches函数 2、ST_Intersects函数 3、ST_Relate函数 4、区别于对比 二、空间相邻检索实践 1、省域表相关介绍 2、相关省域相邻查询 3、全国各省份邻居排名 三、总结 前言 在当今数字化时代&#xff0c;地理空间数据的高效管理…...

Java 2024年面试总结(持续更新)

目录 最近趁着金三银四面了五六家公司吧&#xff0c;也整理了一些问题供大家参考一下&#xff08;适合经验三年左右的&#xff09;。 面试问题&#xff08;答案是我自己总结的&#xff0c;不一定正确&#xff09;&#xff1a; 总结&#xff1a; 最近趁着金三银四面了五六家公…...

JDK9新特性

文章目录 新特性&#xff1a;1.模块化系统使用模块化module-info.java&#xff1a;exports&#xff1a;opens&#xff1a;requires&#xff1a;provides&#xff1a;uses&#xff1a; 2.JShell启动Jshell执行计算定义变量定义方法定义类帮助命令查看定义的变量&#xff1a;/var…...

性能测试中的数据库连接池优化

目录 一、配置连接池参数 二、配置连接池数量 三、监控连接池 数据库连接池的意义是让连接复用&#xff0c;通过建立一个数据库连接池&#xff08;缓冲区&#xff09;以及一套连接的使用&#xff0c;分配&#xff0c;管理策略&#xff0c;使得该连接池中的连接可以得到高效&…...

1. 初识spark

背景&#xff1a; 作为一名开发人员&#xff0c;用内存处理数据是每天都在做的事情。内存处理数据最大的优势就是方便&#xff0c;快捷&#xff0c;可以很快得到结果&#xff0c;但是内存总是有瓶颈的&#xff0c;不管你运行代码的机器有多大的内存&#xff0c;总是有更大规模…...

专业学习|一文了解并实操自适应大邻域搜索(讲解代码)

一、自适应大邻域搜索概念介绍 自适应大邻域搜索&#xff08;Adaptive Large Neighborhood Search&#xff0c;ALNS&#xff09;是一种用于解决组合优化问题的元启发式算法。以下是关于它的详细介绍&#xff1a; -自适应大领域搜索的核心思想是&#xff1a;破坏解、修复解、动…...

Redis --- 使用zset处理排行榜和计数问题

在处理计数业务时&#xff0c;我们一般会使用一个数据结构&#xff0c;既是集合又可以保证唯一性&#xff0c;所以我们会选择Redis中的set集合&#xff1a; 业务逻辑&#xff1a; 用户点击点赞按钮&#xff0c;需要再set集合内判断是否已点赞&#xff0c;未点赞则需要将点赞数1…...

排序算法——快速排序

代码仓库&#xff1a; 1037827920/AlgorithmZoo 快速排序 算法步骤 选择基准元素&#xff0c;从数组中选择一个元素作为基准&#xff0c;通常选择方式有&#xff1a; 第一个元素最后一个元素中间元素随机选择 分区操作&#xff0c;将数组元素根据基准分为两部分&#xff0c;…...

有用的sql链接

『SQL』常考面试题&#xff08;2——窗口函数&#xff09;_sql的窗口函数面试题-CSDN博客 史上最强sql计算用户次日留存率详解&#xff08;通用版&#xff09;及相关常用函数 -2020.06.10 - 知乎 (zhihu.com) 1280. 学生们参加各科测试的次数 - 力扣&#xff08;LeetCode&…...

手写MVVM框架-构建虚拟dom树

MVVM的核心之一就是虚拟dom树&#xff0c;我们这一章节就先构建一个虚拟dom树 首先我们需要创建一个VNode的类 // 当前类的位置是src/vnode/index.js export default class VNode{constructor(tag, // 标签名称&#xff08;英文大写&#xff09;ele, // 对应真实节点children,…...

C++单例模式

单例模式是一种设计模式&#xff0c;它保证一个类只有一个对象。因此单例模式要私有化构造函数&#xff0c;禁用拷贝构造以及赋值重载。同时还要提供一个静态成员函数获取单例对象。 单例模式有两种实现方式&#xff1a;饿汉模式和懒汉模式 饿汉模式&#xff1a;创建静态单例…...

SQL入门到精通 理论+实战 -- 在 MySQL 中学习SQL语言

目录 一、环境准备 1、MySQL 8.0 和 Navicat 下载安装 2、准备好的表和数据文件&#xff1a; 二、SQL语言简述 1、数据库基础概念 2、什么是SQL 3、SQL的分类 4、SQL通用语法 三、DDL&#xff08;Data Definition Language&#xff09;&#xff1a;数据定义语言 1、操…...

RabbitMQ 可靠性投递

文章目录 前言一、RabbitMQ自带机制1、生产者发送消息注意1.1、事务&#xff08;Transactions&#xff09;1.2、发布确认&#xff08;Publisher Confirms&#xff09;1.2.1、同步1.2.2、异步 2、消息路由机制2.1、使用备份交换机&#xff08;Alternate Exchanges&#xff09;2.…...

Java常见的技术场景面试题

一、单点登录这块怎么实现的&#xff1f; 单点登录概述 单点登录&#xff1a;Single Sign On&#xff08;简称SSO&#xff09;,只需要登录一次&#xff0c;就可以访问所有信任的应用系统 在以前的时候&#xff0c;一般我们就单系统&#xff0c;所有的功能都在同一个系统上。…...

使用 Postman 进行 API 测试:从入门到精通

使用 Postman 进行 API 测试&#xff1a;从入门到精通 使用 Postman 进行 API 测试&#xff1a;从入门到精通一、什么是 API 测试&#xff1f;二、Postman 简介三、环境搭建四、API 测试流程1. 收集 API 文档2. 发送基本请求示例&#xff1a;发送 GET 请求示例代码&#xff08;…...

用python实现进度条

前言 在Python中&#xff0c;可以使用多种方式实现进度条。以下是几种常见的进度条格式的实现方法&#xff1a; 1. 使用 tqdm 库 tqdm 是一个非常流行的库&#xff0c;可以轻松地在循环中显示进度条。 from tqdm import tqdm import time# 示例&#xff1a;简单的进度条 fo…...

android 自定义通话录音

在 Android 开发中&#xff0c;实现通话录音功能通常涉及到对系统通话的拦截和录音。由于通话录音涉及到用户隐私和安全性&#xff0c;Android 系统对此有严格的限制和要求。在 Android 10&#xff08;API 级别 29&#xff09;及以上版本中&#xff0c;直接访问通话录音功能变得…...

WebSocket——环境搭建与多环境配置

一、前言&#xff1a;为什么要使用多环境配置&#xff1f; 在开发过程中&#xff0c;我们通常会遇到多个不同的环境&#xff0c;比如开发环境&#xff08;Dev&#xff09;、测试环境&#xff08;Test&#xff09;、生产环境&#xff08;Prod&#xff09;等。每个环境的配置和需…...

【自动化办公】批量图片PDF自定义指定多个区域识别重命名,批量识别铁路货物运单区域内容改名,基于WPF和飞桨ocr深度学习模型的解决方案

项目背景介绍 铁路货运企业需要对物流单进行长期存档&#xff0c;以便后续查询和审计。不同的物流单可能包含不同的关键信息&#xff0c;通过自定义指定多个区域进行识别重命名&#xff0c;可以使存档的图片文件名具有统一的规范和明确的含义。比如&#xff0c;将包含货物运单…...

在线教程丨YOLO系列10年更新11个版本,最新模型在目标检测多项任务中达SOTA

YOLO (You Only Look Once) 是计算机视觉领域中最具影响力的实时目标检测算法之一&#xff0c;以其高精度与高效性深受业界青睐&#xff0c;广泛应用于自动驾驶、安防监控、医疗影像等领域。 该模型最早于 2015 年由华盛顿大学研究生 Joseph Redmon 发布&#xff0c;开创了将目…...

c++可变参数详解

目录 引言 库的基本功能 va_start 宏: va_arg 宏 va_end 宏 va_copy 宏 使用 处理可变参数代码 C11可变参数模板 基本概念 sizeof... 运算符 包扩展 引言 在C编程中&#xff0c;处理不确定数量的参数是一个常见的需求。为了支持这种需求&#xff0c;C标准库提供了 &…...

Ubuntu安装VMware17

安装 下载本文的附件&#xff0c;之后执行 sudo chmod x VMware-Workstation-Full-17.5.2-23775571.x86_64.bundle sudo ./VMware-Workstation-Full-17.5.2-23775571.x86_64.bundle安装注意事项&#xff1a; 跳过账户登录的办法&#xff1a;断开网络 可能出现的问题以及解决…...

在Debian 12上安装VNC服务器

不知道什么标题 可以看到这个文章是通过豆包从国外网站copy的&#xff0c;先这样写着好了&#xff0c;具体的我有时间再补充&#xff0c;基本内容都在这里了。 在Debian 12上安装VNC服务器 简介 VNC&#xff08;Virtual Network Computing&#xff0c;虚拟网络计算&#xf…...

设计模式Python版 外观模式

文章目录 前言一、外观模式二、外观模式示例三、抽象外观类四、抽象外观类示例 前言 GOF设计模式分三大类&#xff1a; 创建型模式&#xff1a;关注对象的创建过程&#xff0c;包括单例模式、简单工厂模式、工厂方法模式、抽象工厂模式、原型模式和建造者模式。结构型模式&am…...

Selenium 浏览器操作与使用技巧——详细解析(Java版)

目录 一、浏览器及窗口操作 二、键盘与鼠标操作 三、勾选复选框 四、多层框架/窗口定位 五、操作下拉框 六、上传文件操作 七、处理弹窗与 alert 八、处理动态元素 九、使用 Selenium 进行网站监控 前言 Selenium 是一款非常强大的 Web 自动化测试工具&#xff0c;能够…...

论文解读:《基于TinyML毫米波雷达的座舱检测、定位与分类》

摘要 本文提出了一种实时的座舱检测、定位和分类解决方案&#xff0c;采用毫米波&#xff08;mmWave&#xff09;雷达系统芯片&#xff08;SoC&#xff09;&#xff0c;CapterahCAL60S344-AE&#xff0c;支持微型机器学习&#xff08;TinyML&#xff09;。提出了波束距离-多普勒…...

e2studio开发RA2E1(5)----GPIO输入检测

e2studio开发RA2E1.5--GPIO输入检测 概述视频教学样品申请硬件准备参考程序源码下载新建工程工程模板保存工程路径芯片配置工程模板选择时钟设置GPIO口配置按键口配置按键口&Led配置R_IOPORT_PortRead()函数原型R_IOPORT_PinRead()函数原型代码 概述 本篇文章主要介绍如何…...

数据结构:队列篇

图均为手绘,代码基于vs2022实现 系列文章目录 数据结构初探: 顺序表 数据结构初探:链表之单链表篇 数据结构初探:链表之双向链表篇 链表特别篇:链表经典算法问题 数据结构:栈篇 文章目录 系列文章目录前言一.队列的概念和结构1.1概念一、动态内存管理优势二、操作效率与安全性…...

idea中git的简单使用

提交&#xff0c;推送直接合并 合到哪个分支就到先切到哪个分支...

Java中的object类

1.Object类是什么&#xff1f; &#x1f7ea;Object 是 Java 类库中的一个特殊类&#xff0c;也是所有类的父类(超类),位于类继承层次结构的顶端。也就是说&#xff0c;Java 允许把任何类型的对象赋给 Object 类型的变量。 &#x1f7e6;Java里面除了Object类&#xff0c;所有的…...

html2canvas绘制页面并生成图像 下载

1. 简介 html2canvas是一个开源的JavaScript库&#xff0c;它允许开发者在用户的浏览器中直接将HTML元素渲染为画布&#xff08;Canvas&#xff09;&#xff0c;并生成图像。以下是对html2canvas的详细介绍&#xff1a; 2. 主要功能 html2canvas的主要功能是将网页中的HTML元…...

Certum OV企业型通配符SSL

随着网络攻击手段的不断演变&#xff0c;仅仅依靠HTTP协议已无法满足现代企业对数据安全的需求。SSL证书&#xff0c;特别是经过严格验证的组织验证型SSL证书&#xff0c;成为了保护网站数据传输安全、提升用户信任度的标配。 一、Certum OV企业型通配符SSL概述 Certum&#…...

2024年Web前端最新Java进阶(五十五)-Java Lambda表达式入门_eclipse lambda(1),面试必备

对象篇 模块化编程-自研模块加载器 开源分享&#xff1a;【大厂前端面试题解析核心总结学习笔记真实项目实战最新讲解视频】 Arrays.sort(players, sortByName); // 1.3 也可以采用如下形式: Arrays.sort(players, (String s1, String s2) -> (s1.compareTo(s2))); ??其…...

JVM 四虚拟机栈

虚拟机栈出现的背景 由于跨平台性的设计&#xff0c;Java的指令都是根据栈来设计的。不同平台CPU架构不同&#xff0c;所以不能设计为基于寄存器的。优点是跨平台&#xff0c;指令集小&#xff0c;编译器容易实现&#xff0c;缺点是性能下降&#xff0c;实现同样的功能需要更多…...

V103开发笔记1-20250113

2025-01-13 一、应用方向分析 应用项目&#xff1a; PCBFLY无人机项目&#xff08;包括飞控和手持遥控器&#xff09;&#xff1b; 分析移植项目&#xff0c;应用外设资源包括&#xff1a; GPIO, PWM,USART,GPIO模拟I2C/SPI, ADC,DMA,USB等&#xff1b; 二、移植项目的基本…...

Page Assist - 本地Deepseek模型 Web UI 的安装和使用

Page Assist Page Assist是一个开源的Chrome扩展程序&#xff0c;为本地AI模型提供一个直观的交互界面。通过它可以在任何网页上打开侧边栏或Web UI&#xff0c;与自己的AI模型进行对话&#xff0c;获取智能辅助。这种设计不仅方便了用户随时调用AI的能力&#xff0c;还保护了…...

Cookie及Session---笔记

目录 Cookiecookie简介cookiesession的认证方式tpshop完整登录实现-cookie Sessionsession简介session自动管理cookietpshop完整登录实现-sessioncookie和session的区别获取响应结果指定内容 Cookie cookie简介 工程师针对HTTP协议是无连接无状态特性所设计的一种技术&#x…...

【Block总结】DASI,多维特征融合

论文信息 HCF-Net&#xff08;Hierarchical Context Fusion Network&#xff09;是一种新提出的深度学习模型&#xff0c;专门用于红外小目标检测。该论文于2024年3月16日发布&#xff0c;作者包括Shibiao Xu、ShuChen Zheng等&#xff0c;主要研究机构为北京邮电大学。该模型…...

LabVIEW的智能电源远程监控系统开发

在工业自动化与测试领域&#xff0c;电源设备的精准控制与远程管理是保障系统稳定运行的核心需求。传统电源管理依赖本地手动操作&#xff0c;存在响应滞后、参数调节效率低、无法实时监控等问题。通过集成工业物联网&#xff08;IIoT&#xff09;技术&#xff0c;实现电源设备…...

4.PPT:日月潭景点介绍【18】

目录 NO1、2、3、4​ NO5、6、7、8 ​ ​NO9、10、11、12 ​ 表居中或者水平/垂直居中单元格内容居中或者水平/垂直居中 NO1、2、3、4 新建一个空白演示文稿&#xff0c;命名为“PPT.pptx”&#xff08;“.pptx”为扩展名&#xff09;新建幻灯片 开始→版式“PPT_素材.doc…...

《迪拜AI展:探寻中东人工智能发展的璀璨新篇》

迪拜&#xff1a;AI 浪潮下的闪耀明珠 迪拜&#xff0c;这座位于阿拉伯半岛东部、波斯湾东南岸的城市&#xff0c;犹如一颗璀璨的明珠&#xff0c;在中东地区散发着独特的魅力。它是阿拉伯联合酋长国的第二大城市&#xff0c;也是迪拜酋长国的首府 &#xff0c;凭借优越的地理位…...