webf/Kraken 调研

一、webf/Kraken 介绍

Kraken

Kraken 是一款采用基于 Flutter 而实现的自绘渲染引擎。它使用 W3C 标准的 HTML,CSS,JavaScript,并支持通过 JavaScript 实现对画面的实时交互。

image.png

bridge层

JavaScript Engine

Kraken 的 JavaScript Engine 为 QuickJS,支持 ECMA 2020 标准的绝大部分功能。QuickJS 不光支持解析 JavaScript 代码,同时还支持离线将 JavaScript 转换为 ByteCode 格式,再直接解析 ByteCode 运行,这样可以减去页面加载过程中 Parse 阶段的时间(AOT )。

https://gitee.com/Program-in-Chinese/QuickJS#https://gitee.com/link?target=https%3A%2F%2Fbellard.org%2Fquickjs%2F

QuickJS:https://ming1016.github.io/2021/02/21/deeply-analyse-quickjs/

QuickJS实现小程序demo:https://www.jianshu.com/p/d0a8af99411f

QuickJS iOS demo:https://github.com/quickjs-zh/QuickJS-iOS

https://github.com/quickjs-ios/quickjs-ios

https://blog.zorro.im/posts/2021-12-21-quickjs-swift-bindings.html

V8、JSCore、Hermes、QuickJS,hybrid开发JS引擎怎么选:https://jishuin.proginn.com/p/763bfbd3c4e7

React Native JSI Challenge:https://medium.com/@christian.falch/https-medium-com-christian-falch-react-native-jsi-challenge-1201a69c8fbf

Native FFI(Foreign Function Interface) Binding

通过 QuickJS Binding,Kraken 使用 C/C++ 实现了 DOM 标准内的绝大部分功能,包括 Node,EventTarget,Element,选择器等。

JIT与AOT的区别

提前编译是相对于即时编译的概念,提前编译能带来的最大好处是Java虚拟机加载这些已经预编译成二进制库之后就能够直接调用,而无须再等待即时编译器在运行时将其编译成二进制机器码。理论上,提前编译可以滅少即时编译带来的预热时间,减少Java应用长期给人带来的“第一次运行慢"的不良体验,可以放心地进行很多全程序的分析行为,可以使用时间压力更大的优化措施。但是提前编译的坏处也很明显,它破坏了Java"—次编写,到处运行"的承诺,必须为每个不同的硬件、操作系统去编译对应的发行包;也显著降低了Java链接过程的动态性,必须要求加载的代码在编译期就是全部已知的,而不能在运行期才确定,否则就只能舍弃掉己经提前编译好的版本,退回到原来的即时编译执行状态。

WKWebView内置的JSCore支持JIT,但JSCore framework不支持JIT

[图片上传中...(image-bc414a-1697532918095-0)]

原文:https://jishuin.proginn.com/p/763bfbd3c4e7

HTML 解析

用户所输入的 HTML 文本也会在 bridge 层进行解析。bridge 中内置一个 HTML Parser,用于解析用户编写的 HTML 字符串。将 HTML 字符串解析后,会生成一组 DOM 节点的调用,进而创建出 DOM 对象。

Kraken的缺点是不支持 css 样式。

个人理解:

flutter页面对应WebF对象。

WebF通过bundle读取到页面(html,css,js)。

dart将读取到的页面通过FFI bridge交给c++进行翻译:

c++部分主要包括quickJS,html parser。

翻译的结果通过FFI交给flutter进行渲染。

二、flutter基础

flutter中的三棵树

flutter包含三棵树形结构:

  • Widget Tree
  • Element Tree
  • RenderObject Tree

Widget不直接参与绘制,仅仅存储绘制需要的信息,如果直接渲染widget是非常耗费性能的,因为widget经常发生变化,就会buildflutter其实渲染的是render树,但是不是所有的widget都继承renderObjectWidget,比如 Container就继承StatelessWidget。只有继承renderObjectWidget 才能创建renderObject对象,才能被渲染引擎直接渲染。

createRenderObject返回的是renderObject,也就是参与渲染的对象。这个方法来自于RenderObjectWidget类。

@protected
@factory
RenderObject createRenderObject(BuildContext context);

渲染过程:当Widget的对象继承renderObject的时候,子类实现父类的方法createRenderObject创建renderObject对象 加入render树中,进行独立渲染。

在Flutter中,只要是build()方法被调用了,就会创建一堆Widget,这个是OK的,上面说过Widget仅仅是配置文件,所以即使频繁地 创建/重建 它们,是不会带来界面的刷新的。

如果你要在Flutter里创建一个动画,一般来说,需要在指定的Widget之上套上一层XXXTransition,然后动画开始后用animationController来不断地让Widget刷新重建,但是整个页面不会因此重新刷新。

createElement方法来自于Widget类,只要是Widget都会创建一个element,因此widget树和element树是一一对应的。

abstract class Widget extends DiagnosticableTree {
  /// Initializes [key] for subclasses.
  const Widget({ this.key });
  @protected
  @factory
  Element createElement();

Element类的inflateWidget方法如下,flutter通过mount方法把创建的element添加到element树中。

image.png

RenderObjectElement的mount方法:


image.png

执行顺序:首先创建widget,之后调用createElement方法创建element,之后调用mount方法加入element树,如果widget继承renderObjectWidget(或者说是Element是RenderObjectElement),就会在mount中创建renderObject之后加入render树

StatelessElement继承ComponentElement

StatefulElement继承ComponentElement

ComponentElement继承Element

StatelessElement和StatefulElement的mount方法最终都会调用build方法,获取到外部build方法传入的widget。

StatefulElement相比StatelessElement多了一步 _state = widget.createState(),在创建element的同时创建了state,并保存为state。之后调用不同的是通过statebuild方法,其他的都是和StatelessElement一样。

总结:每一个Widget创建都会创建一个Element对象,通过调用createElement方法 把创建的Element加入Element树中,之后会调用mount方法,mount方法会判断,如果widget继承renderObjectWidget(或者说是Element是RenderObjectElement),就会在mount中创建renderObject之后加入render树。对于StatelessElementStatefulElement都是继承ComponentElement,通过调用build方法,把自己传递出去StatefulElement多了一个createState

可以看到都是BuildContext抽象类型,在这里可以理解成BuildContext就是Element。

abstract class Element extends DiagnosticableTree implements BuildContext


image.png

流程:程序员写Widget -> Widget形成Element -> Element构建 RenderObject -> RenderObject描述Canvas绘制 -> 交给FlutterEngine 光栅化Rasterize

我们知道,Widget是immutable的,也就是说一旦有了变化,Widget是反复重建的,你在代码里写的Widget的构造函数会被重新执行。那对应的,肯定得有一个东西来消化掉这些变化中的不变,来做cache。
Widget的每一帧对应了一个State,一个指定的state就能精确描述一个Widget该怎么展示。当State发生变化,则Widget重建,但Element只会updates。

image.png

以下是通过使用的例子:

一个 Widget 绑定一个GlobalKey,通过 GlobalKey来获取对应的Element(BuildContext),然后通过Element来获取对应的RenderObject,从而获取这个Widget 在手机 屏幕上对应的位置与大小 信息,

`///第一步 创建 GlobalKey GlobalKey globalKey = GlobalKey(); ///第二步在 对应的Widget 引用 ,如这里的TextText('张三',key:globalKey); ///第三步 通过 globalKey 来获取 对应的Element(BuildContext) BuildContext stackContext = globalKey.currentContext; ///第四步 获取对应的 RenderObject ///RenderObject是抽象的,它的一些基础信息封装在子类RenderBox中 RenderBox renderBox = stackContext.findRenderObject(); ///然后通过 RenderBox 来获取对应的Text在手机屏幕上显示的位置 与大小 信息 ///相对于全局的位置 Offset offset = renderBox.localToGlobal(Offset.zero); ///获取指定的Widget的大小 信息 Size size = renderBox.paintBounds.size;

参考资料:
https://juejin.cn/post/7035607966543773703 https://zhuanlan.zhihu.com/p/103398958?utm_id=0 https://developer.huawei.com/consumer/cn/forum/topic/0202327746887030154https://zhuanlan.zhihu.com/p/369286610 https://developer.huawei.com/consumer/cn/forum/topic/0202327746887030154https://zhuanlan.zhihu.com/p/369286610`

方案设计

采用方案3


image.png

四、WebF文件解析

widget文件夹:存储所有自定义widget,element

webf.dart:webf主要widget,element

WebF:statefulWidget,作为wiget的组合类,在WeFState配置组合,element用父类默认的statefulElement,没有renderObject

WebFState:State类,build返回包装后的WebFContext作为渲染widget

WebFContext:代理widget,child为WebFRootRenderObjectWidget。element为WebFContextInheritElement,没有renderObject
WebFContextInheritElement:WebFContext对应的element

WebFRootRenderObjectWidget:是RenderObjectWidget,element为_WebFRenderObjectElement,renderObject为WebFController.view.getRootRenderObject()

_WebFRenderObjectElement:WebFRootRenderObjectWidget的element

WebFController:WebFRootRenderObjectWidget对应的renderObject管理器

module文件夹,module_manager.dart文件
abstract class BaseModule
class ModuleManager {
final int contextId;
final WebFController controller;
final Map<String, BaseModule> _moduleMap = {};
}
BaseModule的子类包括
LocationModule
ClipBoardModule
NavigatorModule
MethodChannelModule
可以看出不同module代表不同功能,由ModuleManager管理,且ModuleManager与WebFController是1-1关系。

换言之,一个webF对象的不同功能由ModuleManager管理,每个功能对应一个module,其中MethodChannelModule是通信功能。


image.png

image.png

class WebFController {
static final SplayTreeMap<int, WebFController?> _controllerMap = SplayTreeMap();
SplayTreeMap是一种自平衡二叉搜索树,它具有可以可以快速访问最近被访问的元素。它能在O(log n)内完成插入、查找和删除操作。
WebFViewController _view;
WebFModuleController _module;
WebFBundle? _entrypoint;
WebFController的重要成员变量如上,并与它们表现为组合关系,WebFController作为整体,与它们拥有共同的生命周期。

class WebFViewController implements WidgetsBindingObserver, ElementsBindingObserver {
late RenderViewportBox viewport;
late Document document;
late Window window;
WebFViewController负责页面相关操作,其中RenderViewportBox是WebFRootRenderObjectWidget对应的RenderObject。

WebFModuleController前文说到,是功能模块管理器。

image.png

dynamic invokeModule(Pointer<Void> callbackContext, int contextId, String moduleName, String method, params,
DartAsyncModuleCallback callback) {
WebFController controller = WebFController.getControllerOfJSContextId(contextId)!;
WebFViewController currentView = controller.view;
dynamic result;
Future<dynamic> invokeModuleCallback({String? error, data}) {
Completer<dynamic> completer = Completer();

      Pointer<NativeValue> dataPtr = malloc.allocate(sizeOf<NativeValue>());
      toNativeValue(dataPtr, data);
      callbackResult = callback(callbackContext, contextId, nullptr, dataPtr);
      malloc.free(dataPtr);
    }

    completer.complete(fromNativeValue(callbackResult));
  });
  return completer.future;
}
result = controller.module.moduleManager.invokeModule(
    moduleName, method, params, invokeModuleCallback);

return result;
}
关键代码如上。点击js页面的按钮触发回调。


image.png

可以看到,原始数据结构是从c++返回的Pointer<NativeString>或Pointer<NativeValue> ffi指针结构。
通过nativeStringToString和fromNativeValue将其转换为dart对应结构。


image.png

// Dispatch the event to the binding side.
void _dispatchEventToNative(Event event) {
Pointer<NativeBindingObject>? pointer = event.currentTarget?.pointer;
DartInvokeBindingMethodsFromDart f = pointer.ref.invokeBindingMethodFromDart.asFunction();

f(pointer, returnValue, method, dispatchEventArguments.length, allocatedNativeArguments);
可以看到f方法和和_invokeModule的参数对不上。

f调用的是c++ 进而调用quickJS注入的回调方法,quickJS内部回调内有通信代码,再由c++调用_invokeModule。

这块就涉及到FFI。

五、WebF 源码解读

WebF widget 包含bundle,build实现如下:
class WebF extends StatefulWidget {
@override
WebFState createState() => WebFState();
}
class WebFState extends State<WebF> with RouteAware {
@override
Widget build(BuildContext context) {
if (!_flutterScreenIsReady) {
return SizedBox(width: 0, height: 0);
}

return RepaintBoundary(
child: WebFContext(
child: WebFRootRenderObjectWidget(
widget,
onCustomElementAttached: onCustomElementWidgetAdd,
onCustomElementDetached: onCustomElementWidgetRemove,
children: customElementWidgets.toList(),
),
),
);
}

class WebFContext extends InheritedWidget
abstract class InheritedWidget extends ProxyWidget
abstract class ProxyWidget extends Widget
ProxyWidget 作为一个抽象的代理 Widget并没有实质性的作用,只是在父类和子类需要传递信息时使用;主要有 InheritedWidget 和 ParentDataWidget两类。

class WebFContext extends InheritedWidget {
WebFContext({required super.child});

@override
bool updateShouldNotify(covariant InheritedWidget oldWidget) {
return false;
}

@override
InheritedElement createElement() {
return WebFContextInheritElement(this);
}
}
WebFContext 该widget内,createElement返回了对应的element,WebFContextInheritElement。
WebFRootRenderObjectWidget webF root渲染节点Widget,继承关系如下(不是WebF开头的都是flutter系统类):
WebFRootRenderObjectWidget extends MultiChildRenderObjectWidget 支持多child的渲染节点Widget
MultiChildRenderObjectWidget extends RenderObjectWidget 渲染节点Widget
RenderObjectWidget extends Widget
主要包括以下方法,都是RenderObject的基本方法:

RenderObject createRenderObject 创建渲染节点
void updateRenderObject 更新
_WebFRenderObjectElement createElement 创建渲染节点元素
WebFRootRenderObjectWidget是渲染WebF界面的widget:

@override
RenderObject createRenderObject(BuildContext context) {
  if (kProfileMode) {
    PerformanceTiming.instance().mark(PERF_CONTROLLER_INIT_START);
  }

  double viewportWidth = _webfWidget.viewportWidth ?? window.physicalSize.width / window.devicePixelRatio;
  double viewportHeight = _webfWidget.viewportHeight ?? window.physicalSize.height / window.devicePixelRatio;

  WebFController controller = WebFController(shortHash(_webfWidget.hashCode), viewportWidth, viewportHeight,
      background: _webfWidget.background,
      showPerformanceOverlay: Platform.environment[ENABLE_PERFORMANCE_OVERLAY] != null,
      entrypoint: _webfWidget.bundle,
      // Execute entrypoint when mount manually.
      autoExecuteEntrypoint: false,
      onLoad: _webfWidget.onLoad,
      onLoadError: _webfWidget.onLoadError,
      onJSError: _webfWidget.onJSError,
      methodChannel: _webfWidget.javaScriptChannel,
      gestureListener: _webfWidget.gestureListener,
      navigationDelegate: _webfWidget.navigationDelegate,
      devToolsService: _webfWidget.devToolsService,
      httpClientInterceptor: _webfWidget.httpClientInterceptor,
      onCustomElementAttached: onCustomElementAttached,
      onCustomElementDetached: onCustomElementDetached,
      initialCookies: _webfWidget.initialCookies,
      uriParser: _webfWidget.uriParser);

  if (kProfileMode) {
    PerformanceTiming.instance().mark(PERF_CONTROLLER_INIT_END);
  }

  (context as _WebFRenderObjectElement).controller = controller;

  OnControllerCreated? onControllerCreated = _webfWidget.onControllerCreated;
  if (onControllerCreated != null) {
    onControllerCreated(controller);
  }

  return controller.view.getRootRenderObject();
}
@override
_WebFRenderObjectElement createElement() {
  return _WebFRenderObjectElement(this);
}

createRenderObject返回的是该widget tree节点对应的renderObject节点,可以看到在方法内创建了WebFController对象,并return它的属性view.getRootRenderObject()

因此可以看出WebFController是负责管理渲染内容的控制器。

bundle传递给了WebFController的entrypoint属性。

entrypoint: _webfWidget.bundle,


image.png

第三行方法内执行
newChild.mount(this, newSlot);


image.png

会将WebFRenderObjectElement加入到element tree中,其中

newChild为需要添加的WebFRenderObjectElement

image.png

_WebFRenderObjectElement 在mount时,会多执行executeEntrypoint方法。
@override
void mount(Element? parent, Object? newSlot) async {
super.mount(parent, newSlot);
assert(parent is WebFContextInheritElement);
assert(controller != null);
(parent as WebFContextInheritElement).controller = controller;
await controller!.executeEntrypoint(animationController: widget._webfWidget.animationController);
}
executeEntrypoint内会先解析resolve bundle(entrypoint),然后再执行evaluate。
WebFBundle提供三种快捷方式创建,字节码(DataBundle,包括bytecode等,通过属性contentType区分),
contentType:
final ContentType _cssContentType = ContentType('text', 'css', charset: UTF_8);
// MIME types suits JavaScript: https://mathiasbynens.be/demo/javascript-mime-type
final ContentType javascriptContentType = ContentType('text', 'javascript', charset: UTF_8);
final ContentType htmlContentType = ContentType('text', 'html', charset: UTF_8);
final ContentType _javascriptApplicationContentType = ContentType('application', 'javascript', charset: UTF_8);
final ContentType _xJavascriptContentType = ContentType('application', 'x-javascript', charset: UTF_8);
final ContentType webfBc1ContentType = ContentType('application', 'vnd.webf.bc1');

url(
NetworkBundle
AssetsBundle
FileBundle。。。),
string(DataBundle)。
它有很多子类,比如
FileBundle
AssetsBundle
NetworkBundle
DataBundle等。
Future<void> executeEntrypoint(
{bool shouldResolve = true, bool shouldEvaluate = true, AnimationController? animationController}) async {
if (_entrypoint != null && shouldResolve) {
await _resolveEntrypoint();
if (_entrypoint!.isResolved && shouldEvaluate) {
_evaluateEntrypoint(animationController: animationController);
} else {
throw FlutterError('Unable to resolve $_entrypoint');
}
} else {
throw FlutterError('Entrypoint is empty.');
}
}
可以看到在解析bundle时,有进行性能统计的打点,和我们sdk内的性能统计类似。
Future<void> _resolveEntrypoint() async {
assert(!_view._disposed, 'WebF have already disposed');

if (kProfileMode) {
PerformanceTiming.instance().mark(PERF_JS_BUNDLE_LOAD_START);
}

WebFBundle? bundleToLoad = _entrypoint;
if (bundleToLoad == null) {
// Do nothing if bundle is null.
return;
}

// Resolve the bundle, including network download or other fetching ways.
try {
await bundleToLoad.resolve(view.contextId);
} catch (e, stack) {
if (onLoadError != null) {
onLoadError!(FlutterError(e.toString()), stack);
}
// Not to dismiss this error.
rethrow;
}

if (kProfileMode) {
PerformanceTiming.instance().mark(PERF_JS_BUNDLE_LOAD_END);
}
}
WebFBundle的resolve如下,其子类进行了重写。
@mustCallSuper
Future<void> resolve(int? contextId) async {
if (isResolved) return;

// Source is input by user, do not trust it's a valid URL.
_uri = Uri.tryParse(url);
if (contextId != null && _uri != null) {
WebFController? controller = WebFController.getControllerOfJSContextId(contextId);
if (controller != null) {
_uri = controller.uriParser!.resolve(Uri.parse(controller.url), _uri!);
}
}
}
NetworkBundle的解析如下:
请求url并将结果以二进制的形式存储到data中。
@override
Future<void> resolve(int? contextId) async {
super.resolve(contextId);
final HttpClientRequest request = await _sharedHttpClient.getUrl(_uri!);

// Prepare request headers.
request.headers.set('Accept', _acceptHeader());
additionalHttpHeaders?.forEach(request.headers.set);
if (contextId != null) {
WebFHttpOverrides.setContextHeader(request.headers, contextId);
}

final HttpClientResponse response = await request.close();
if (response.statusCode != HttpStatus.ok)
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Unable to load asset: url'), IntProperty('HTTP status code', response.statusCode), ]); final Uint8List bytes = await consolidateHttpClientResponseBytes(response); data = bytes.buffer.asUint8List(); contentType = response.headers.contentType ?? ContentType.binary; } 以下是evaluate的关键代码: 会根据数据的类型, javascript, bytecode, html, text(处理成javascript) 进行解析。 Uint8List data = entrypoint.data!; if (entrypoint.isJavascript) { // Prefer sync decode in loading entrypoint. evaluateScripts(contextId, await resolveStringFromData(data, preferSync: true), url: url); } else if (entrypoint.isBytecode) { evaluateQuickjsByteCode(contextId, data); } else if (entrypoint.isHTML) { parseHTML(contextId, await resolveStringFromData(data)); } else if (entrypoint.contentType.primaryType == 'text') { // Fallback treating text content as JavaScript. try { evaluateScripts(contextId, await resolveStringFromData(data, preferSync: true), url: url); } catch (error) { print('Fallback to execute JavaScript content ofurl');
rethrow;
}
} else {
// The resource type can not be evaluated.
throw FlutterError('Can't evaluate content of $url');
}
evaluateScripts evaluateQuickjsByteCode parseHTML 三个方法的实现都在webf的/bridge/to_native.dart文件里。这里的native指C++。
最终调用如下:
final DartParseHTML _parseHTML =
WebFDynamicLibrary.ref.lookup<NativeFunction<NativeParseHTML>>('parseHTML').asFunction();
final DartEvaluateScripts _evaluateScripts =
WebFDynamicLibrary.ref.lookup<NativeFunction<NativeEvaluateScripts>>('evaluateScripts').asFunction();
final DartEvaluateQuickjsByteCode _evaluateQuickjsByteCode = WebFDynamicLibrary.ref
.lookup<NativeFunction<NativeEvaluateQuickjsByteCode>>('evaluateQuickjsByteCode')
.asFunction();
WebFDynamicLibrary的ref属性 get方法如下:
ref是DynamicLibrary类型
static DynamicLibrary get ref {
DynamicLibrary? nativeDynamicLibrary = _ref;
_ref = nativeDynamicLibrary ??= DynamicLibrary.open(join(_dynamicLibraryPath, _nativeDynamicLibraryName));
return nativeDynamicLibrary;
}
WebFDynamicLibrar对lib进行了懒加载处理,在不同平台有不同名称,再iOS名称为webf_bridge

// The kraken library name.
static String libName = 'libwebf';

static String get _nativeDynamicLibraryName {
if (Platform.isMacOS) {
return 'libName.dylib'; } else if (Platform.isIOS) { return 'webf_bridge.framework/webf_bridge'; } else if (Platform.isWindows) { return 'libName.dll';
} else if (Platform.isAndroid || Platform.isLinux) {
return '$libName.so';
} else {
throw UnimplementedError('Not supported platform.');
}
}
DynamicLibrary是flutter sdk的类型,
https://api.flutter.dev/flutter/dart-ffi/DynamicLibrary-class.html
https://codelabs.developers.google.com/codelabs/flutter-ffigen#0
A dynamically loaded native library.

动态加载本地库。

A dynamically loaded library is a mapping from symbols to memory addresses. These memory addresses can be accessed through lookup.

DynamicLibrary的代码在flutter sdk的lib/ffi/dynamic_library.dart中。


image.png

可以看出flutter sdk已经包含ffi功能,而不是由webf实现。
动态库的符号表映射到了内存地址中,这些地址可以通过loopup方法访问。

因此,可以看出parseHTML,evaluateScripts,evaluateQuickjsByteCode的实现都在c/c++中实现,dart只是组装好参数进行调用。

FFI介绍

如在 Flutter 中调用 C/C++ 代码可以通过 method channel 的方式来调用 JNI 间接调用 C/C++ 代码 ,那么就会有较长的调用链 Flutter -> Java JNI -> C/C++, 假如我们要传递一个参数,这个参数将会在 Java & C++ 各拷贝一次,如果是大对象容易造成内存抖动,且效率较低。

FFI (Foreign function interface) 代表 外部功能接口, 类似功能的其他术语包括本地接口和语言绑定。这个叫法延续在 Rust、Python、Dart 等语言中,而 Java 将其 FFI 称为 JNI(Java 本机接口)。

在 Flutter 2.2 中,Dart 2.13 扩展了对原生互操作性的支持,支持在 FFI 中使用数组和封装结构体

使用 FFI 后,首次加载缩略图速度提升 2% ~ 16%,在涉及大量图片传输场景下数据提升明显,数据传输耗时占比较高,FFI 替换 Channel 后传输耗时降低。

开眼快创Flutter实践:http://w4lle.com/2021/04/12/kidea-flutter/

https://toutiao.io/posts/x9nh0ii/preview

处理复杂的对象通常使用结构体,如何传递 C++ 结构体与 Dart 交互?

  static Message createMessage() {
    final DynamicLibrary _opencvLib =    Platform.isAndroid ? DynamicLibrary.open("libnative-lib.so") : DynamicLibrary.process(); 
    final Message Function(Pointer<Utf8> msg, int phone) createMessage =    _opencvLib.lookup<NativeFunction<Message Function(Pointer<Utf8> msg, Uint32 phone)>>("createMessage").asFunction();
    final msg = 'Dart'.toNativeUtf8();
    final phone = 1000;
    final result = createMessage(msg, phone);
    print('result msg = ${result.msg.toDartString()} phone = ${result.phone}');
    return result;
  }

在demo 的createMessage loopup中,返回值为Message,在webf中是void,说明webf不是通过同步的方式返回。
typedef NativeEvaluateQuickjsByteCode = Void Function(Pointer<Void>, Pointer<Uint8> bytes, Int32 byteLen);
typedef DartEvaluateQuickjsByteCode = void Function(Pointer<Void>, Pointer<Uint8> bytes, int byteLen);
typedef NativeEvaluateScripts = Void Function(
Pointer<Void>, Pointer<NativeString> code, Pointer<Utf8> url, Int32 startLine);
typedef DartEvaluateScripts = void Function(
Pointer<Void>, Pointer<NativeString> code, Pointer<Utf8> url, int startLine);

可以看到在bridge/to_native.dart文件(dart调用c++的文件)中,除了翻译js,翻译字节码,翻译html还有其它通信 api。
一共包含14个dart调用c++的api。
1.获取webf信息,参数为空,返回值为NativeWebFInfo结构体,包含app名称,版本,修订版本,系统名。

这些可能是webf c++ lib的版本信息。

typedef NativeGetWebFInfo = Pointer<NativeWebFInfo> Function();
typedef DartGetWebFInfo = Pointer<NativeWebFInfo> Function();
final DartGetWebFInfo _getWebFInfo =
WebFDynamicLibrary.ref.lookup<NativeFunction<NativeGetWebFInfo>>('getWebFInfo').asFunction();

class NativeWebFInfo extends Struct {
external Pointer<Utf8> app_name;
external Pointer<Utf8> app_version;
external Pointer<Utf8> app_revision;
external Pointer<Utf8> system_name;
}
2.调用模块事件。

这些可能是触发js中注册的回调handler。比如点击按钮的handler。

// Register invokeEventListener
typedef NativeInvokeEventListener = Pointer<NativeValue> Function(
Pointer<Void>, Pointer<NativeString>, Pointer<Utf8> eventType, Pointer<Void> nativeEvent, Pointer<NativeValue>);
typedef DartInvokeEventListener = Pointer<NativeValue> Function(
Pointer<Void>, Pointer<NativeString>, Pointer<Utf8> eventType, Pointer<Void> nativeEvent, Pointer<NativeValue>);
final DartInvokeEventListener _invokeModuleEvent =
WebFDynamicLibrary.ref.lookup<NativeFunction<NativeInvokeEventListener>>('invokeModuleEvent').asFunction();
3.创建屏幕。

不确定,可能是初始化显示区域。

// Register createScreen
typedef NativeCreateScreen = Pointer<Void> Function(Double, Double);
typedef DartCreateScreen = Pointer<Void> Function(double, double);
final DartCreateScreen _createScreen =
WebFDynamicLibrary.ref.lookup<NativeFunction<NativeCreateScreen>>('createScreen').asFunction();

  1. 翻译(执行)js脚本,返回值为空。

执行注入quickjs的js脚本。

// Register evaluateScripts
typedef NativeEvaluateScripts = Void Function(
Pointer<Void>, Pointer<NativeString> code, Pointer<Utf8> url, Int32 startLine);
typedef DartEvaluateScripts = void Function(
Pointer<Void>, Pointer<NativeString> code, Pointer<Utf8> url, int startLine);
final DartEvaluateScripts _evaluateScripts =
WebFDynamicLibrary.ref.lookup<NativeFunction<NativeEvaluateScripts>>('evaluateScripts').asFunction();

5.翻译html,返回值为空。
执行注入quickjs的html。
// Register parseHTML
typedef NativeParseHTML = Void Function(Pointer<Void>, Pointer<Utf8> code, Int32 length);
typedef DartParseHTML = void Function(Pointer<Void>, Pointer<Utf8> code, int length);
final DartParseHTML _parseHTML =
WebFDynamicLibrary.ref.lookup<NativeFunction<NativeParseHTML>>('parseHTML').asFunction();
6.翻译字节码,返回值为空。
typedef NativeEvaluateQuickjsByteCode = Void Function(Pointer<Void>, Pointer<Uint8> bytes, Int32 byteLen);
typedef DartEvaluateQuickjsByteCode = void Function(Pointer<Void>, Pointer<Uint8> bytes, int byteLen);

final DartEvaluateQuickjsByteCode _evaluateQuickjsByteCode = WebFDynamicLibrary.ref
.lookup<NativeFunction<NativeEvaluateQuickjsByteCode>>('evaluateQuickjsByteCode')
.asFunction();

7.初始化js引擎(应该是quickJS初始化),返回值为空。
初始化quickjs配置。

// Register initJsEngine typedef NativeInitDartContext = Void Function(Pointer<Uint64> dartMethods, Int32 methodsLength);
typedef DartInitDartContext = void Function(Pointer<Uint64> dartMethods, int methodsLength);

final DartInitDartContext _initDartContext =
WebFDynamicLibrary.ref.lookup<NativeFunction<NativeInitDartContext>>('initDartContext').asFunction();
8.页面销毁。

typedef NativeDisposePage = Void Function(Pointer<Void> page);
typedef DartDisposePage = void Function(Pointer<Void> page);

final DartDisposePage _disposePage =
WebFDynamicLibrary.ref.lookup<NativeFunction<NativeDisposePage>>('disposePage').asFunction();

9.页面创建。

typedef NativeAllocateNewPage = Pointer<Void> Function(Int32);
typedef DartAllocateNewPage = Pointer<Void> Function(int);

final DartAllocateNewPage _allocateNewPage =
WebFDynamicLibrary.ref.lookup<NativeFunction<NativeAllocateNewPage>>('allocateNewPage').asFunction();

10.通过字节码注册插件。

Kraken 插件的注册,比如新的HTML 标签。https://openkraken.com/plugins/plugin/introduction

typedef NativeRegisterPluginByteCode = Void Function(Pointer<Uint8> bytes, Int32 length, Pointer<Utf8> pluginName);
typedef DartRegisterPluginByteCode = void Function(Pointer<Uint8> bytes, int length, Pointer<Utf8> pluginName);

final DartRegisterPluginByteCode _registerPluginByteCode =
WebFDynamicLibrary.ref.lookup<NativeFunction<NativeRegisterPluginByteCode>>('registerPluginByteCode').asFunction();

11.性能统计开关。

typedef NativeProfileModeEnabled = Int32 Function();
typedef DartProfileModeEnabled = int Function();

final DartProfileModeEnabled _profileModeEnabled =
WebFDynamicLibrary.ref.lookup<NativeFunction<NativeProfileModeEnabled>>('profileModeEnabled').asFunction();
12.获取UI指令。

typedef NativeGetUICommandItems = Pointer<Uint64> Function(Pointer<Void>);
typedef DartGetUICommandItems = Pointer<Uint64> Function(Pointer<Void>);

final DartGetUICommandItems _getUICommandItems =
WebFDynamicLibrary.ref.lookup<NativeFunction<NativeGetUICommandItems>>('getUICommandItems').asFunction();
UI指令如下:
enum UICommandType {
createElement, 创建element
createTextNode, 创建textNode
createComment, 创建Comment
createDocument, 创建Document
createWindow, 创建window
disposeBindingObject, 销毁绑定对象
addEvent, 增加事件
removeNode, 移除node节点
insertAdjacentNode, 插入相邻node节点
setStyle, 设置style
setAttribute, 设置attribute
removeAttribute, 移除attribute
cloneNode, 克隆node
removeEvent, 移除event
createDocumentFragment, 创建DocumentFragment
// perf optimize 性能优化
createSVGElement, 创建svg element
createElementNS, 创建元素NS
}

13.获取UI指令大小。
typedef NativeGetUICommandItemSize = Int64 Function(Pointer<Void>);
typedef DartGetUICommandItemSize = int Function(Pointer<Void>);

final DartGetUICommandItemSize _getUICommandItemSize =
WebFDynamicLibrary.ref.lookup<NativeFunction<NativeGetUICommandItemSize>>('getUICommandItemSize').asFunction();
14.清除UI指令。
typedef NativeClearUICommandItems = Void Function(Pointer<Void>);
typedef DartClearUICommandItems = void Function(Pointer<Void>);

final DartClearUICommandItems _clearUICommandItems =
WebFDynamicLibrary.ref.lookup<NativeFunction<NativeClearUICommandItems>>('clearUICommandItems').asFunction();
webf中和通信相关的dart代码都在bridge文件夹里。


image.png

native_types.dart记录通信相关结构体类型。
native_value.dart记录相关值类型。
to_native.dart前文已经提到,是dart通过ffi调用c++ lib。
dynamic_library.dart前文已经提到,WebFDynamicLibrary类实现,封装了DynamicLibrary。
binding.dart
bridge.dart
from_native.dart 接收来自C++ lib的回调


image.png

c/c++的实现如上图:
bindings/qjs是quickjs的封装
cmake编译相关
core

foundation
include
polyfill
scripts
test测试相关
third_party第三方包括quickjs源码

以下是一个flutterView点击按钮触发webf dart通信回调的流程:


image.png

最早触发的webf方法是,手势分发器的onClick回调。

GestureDispatcher类中,_gestureRecognizers存储所有的手势处理。
final Map<String, GestureRecognizer> _gestureRecognizers = <String, GestureRecognizer>{};
其中,Tap手势的topUp交给_onClick回调。

_gestureRecognizers[EVENT_CLICK] = TapGestureRecognizer()..onTapUp = _onClick;
与点击相关的手势TapGestureRecognizer存储在_gestureRecognizers里。
GestureDispatcher() {
// Tap Recognizer
_gestureRecognizers[] = TapGestureRecognizer()..onTapUp = _onClick;
// DoubleTap Recognizer
_gestureRecognizers[EVENT_DOUBLE_CLICK] = DoubleTapGestureRecognizer()..onDoubleTapDown = _onDoubleClick;
// Swipe Recognizer
重新运行看下在构造页面时,GestureDispatcher的构造函数的调用情况。
GestureDispatcher gestureDispatcher = GestureDispatcher();
可以看到gestureDispatcher作为webController的属性,在页面初始化时就创建了。

image.png

那这个TapGestureRecognizer是怎么和flutterView上的点击绑定的呢?

查看flutter源码中的调用堆栈。
可以看到后面一段是TapGestureRecognizer的内部处理,直到最后执行webf配置的回调_onClick。
前面一段是路由处理,主要是找到该事件PointerEvent对应的处理路由PointerRoute,并执行该route,该route。
这个过程都在PointerRouter内完成。


image.png

PointerEvent记录了该事件类型及相关参数(比如点击位置等);

PointerRoute则是该事件对应的处理回调,可以看到就是TapGestureRecognizer的基类的handleEvent:方法。

image.png

注释看出PointerRoute是接收PointerEvent的回调。

/// A callback that receives a [PointerEvent]
typedef PointerRoute = void Function(PointerEvent event);
在回调_onClick中调用_handleMouseEvent,传入相对位置和全局位置。

void _onClick(TapUpDetails details) {
_handleMouseEvent(EVENT_CLICK, localPosition: details.localPosition, globalPosition: details.globalPosition);
}


image.png

创建对应的MouseEvent并交给target进行分发。

Event event = MouseEvent(type,
clientX: clientX,
clientY: clientY,
offsetX: localPosition.dx,
offsetY: localPosition.dy,
view: (_target as Node).ownerDocument.defaultView);

_target?.dispatchEvent(event);

[图片上传失败...(image-d45531-1697533863190)]

最终会调用_dispatchEventInDOM。

前文说了GestureDispatcher是webfController的属性,那么target在什么时候赋值?

重新运行发现初始化页面时并未赋值,在点击时进入断点赋值:


image.png

RenderViewportBox extends RenderBox

当事件类型是event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent会进行新的命中测试,命中测试相关请看,得到命中测试列表后,开始调用dispatchEvent进行事件分发。

这块的前置工作应该是找到处理这个事件的第一响应者。应该类似iOS的事件响应链和传递链。

即找到的第一响应者为RenderViewportBox,并触发它的handleEvent方法进行处理。
// Add pointer to gesture dispatcher.
controller.gestureDispatcher.handlePointerEvent(event);
然后将事件交给webfcontroller的手势分发器处理。

void handlePointerEvent(PointerEvent event) {
if (!(event is PointerDownEvent ||
event is PointerUpEvent ||
event is PointerMoveEvent ||
event is PointerCancelEvent)) {
// Only basic Point events are handled, other event does nothing and returns directly such as hover and scroll.
return;
}

// Stores the current TouchPoint to trigger the corresponding event.
TouchPoint touchPoint = _toTouchPoint(event);

_addPoint(touchPoint);

if (event is PointerDownEvent) {
_gatherEventsInPath();

// Clear timer to prevent accidental clear target.
_stopClearTargetTimer();

// The current eventTarget state needs to be stored for use in the callback of GestureRecognizer.
_target = _eventPath.isNotEmpty ? _eventPath.first : null;
if (_target != null) {
  _bindEventTargetWithTouchPoint(touchPoint, _target!);
}

}
target为_eventPath第一个元素,查找_eventPath的赋值。

image.png

class RenderBoxModel extends RenderBox


image.png

image.png

renderBox.getEventTarget = getEventTarget;
RenderBoxModel的target为this。
this定义为:
mixin ElementEventMixin on ElementBase
重新运行,在初始化页面时候getEventTarget进行了赋值。


image.png

image.png

image.png

可以看到在初始化页面,构建页面元素进行attach时,进行了SpanElement(target)和renderBoxModel的绑定。
回到最初,gesture_dispatcher的这个方法。

void setEventPath(EventTarget target) {
_eventPath = target.eventPath;
}
EventTarget的方法:
List<EventTarget> get eventPath {
List<EventTarget> path = [];
EventTarget? current = this;
while (current != null) {
path.add(current);
current = current.parentEventTarget;
}
return path;
}
可以认为EventTarget.eventPath.first就是EventTarget。
debug证明了这点。


image.png

并将target存到gesture_dispatcher的map中。
void _bindEventTargetWithTouchPoint(TouchPoint touchPoint, EventTarget eventTarget) {
_pointTargets[touchPoint.id] = eventTarget;
}
final Map<int, EventTarget> _pointTargets = {};
回到点击按钮的触发回调链路。
Event event = MouseEvent(type,
clientX: clientX,
clientY: clientY,
offsetX: localPosition.dx,
offsetY: localPosition.dy,
view: (_target as Node).ownerDocument.defaultView);
_target?.dispatchEvent(event);
image.png

将封装后的MouseEvent交给target分发。

void dispatchEvent(Event event) {
if (_disposed) return;

event.target = this;
_dispatchEventInDOM(event);
}
this是SpanElement。
下面代码大概意思就是找click类型的EventHandler列表,找到则执行。
同时判断是否需要继续传递,如果需要传递给parent节点(parentEventTarget)。这里类似iOS。
// Refs: https://github.com/WebKit/WebKit/blob/main/Source/WebCore/dom/EventDispatcher.cpp#L85
void _dispatchEventInDOM(Event event) {
// TODO: Invoke capturing event listeners in the reverse order.

String eventType = event.type;
List<EventHandler>? existHandler = _eventHandlers[eventType];
if (existHandler != null) {
// Modify currentTarget before the handler call, otherwise currentTarget may be modified by the previous handler.
event.currentTarget = this;
// To avoid concurrent exception while prev handler modify the original handler list, causing list iteration
// with error, copy the handlers here.
try {
for (EventHandler handler in [...existHandler]) {
handler(event);
}
} catch (e, stack) {
print('e\nstack');
}
event.currentTarget = null;
}

// Invoke bubbling event listeners.
if (event.bubbles && !event.propagationStopped) {
parentEventTarget?._dispatchEventInDOM(event);
}
}
依次传递到:SpanElement,ButtonElement,DivElement,BodyElement,HTMLElement,都是null,直到Document找到了handler并执行。
可以看到执行的过程调用了C++。这里做的应该是通过c++执行注入到quickJS的方法。
// Dispatch the event to the binding side.
void _dispatchEventToNative(Event event) {
Pointer<NativeBindingObject>? pointer = event.currentTarget?.pointer;
int? contextId = event.target?.contextId;
if (contextId != null && pointer != null && pointer.ref.invokeBindingMethodFromDart != nullptr) {
BindingObject bindingObject = BindingBridge.getBindingObject(pointer);
// Call methods implements at C++ side.
DartInvokeBindingMethodsFromDart f = pointer.ref.invokeBindingMethodFromDart.asFunction();

Pointer<Void> rawEvent = event.toRaw().cast<Void>();
List<dynamic> dispatchEventArguments = [event.type, rawEvent];

if (isEnabledLog) {
  print('dispatch event to native side: target: ${event.target} arguments: $dispatchEventArguments');
}

Pointer<NativeValue> method = malloc.allocate(sizeOf<NativeValue>());
toNativeValue(method, 'dispatchEvent');
Pointer<NativeValue> allocatedNativeArguments = makeNativeValueArguments(bindingObject, dispatchEventArguments);

Pointer<NativeValue> returnValue = malloc.allocate(sizeOf<NativeValue>());
f(pointer, returnValue, method, dispatchEventArguments.length, allocatedNativeArguments);
Pointer<EventDispatchResult> dispatchResult = fromNativeValue(returnValue).cast<EventDispatchResult>();
event.cancelable = dispatchResult.ref.canceled;
event.propagationStopped = dispatchResult.ref.propagationStopped;

从from_native接收到C++的回调,触发_invokeModule方法。


image.png

最后通过webFMethodChannel将结果通过webf接口发送给dart。
Future<dynamic> _invokeMethodFromJavaScript(WebFController? controller, String method, List args) {
WebFMethodChannel? webFMethodChannel = controller?.methodChannel;
if (webFMethodChannel != null) {
return webFMethodChannel.invokeMethodFromJavaScript(method, args);

WebF 是如何高性能实现 Flutter + Web 融合的-董天成.pdf 解读

image.png

这里的canvas支持使用的是高性能的skia渲染引擎。

flutter的底层渲染采用的是skia,webF内部是基于flutter,并借助flutter实现整个渲染(skyline也是)。

image.png

绘制的每个图像通过建立一个fluter renderObject tree保存,每个图像通过layout进行布局,绘制通过canvas api(skia实现)。

可以理解为iOS里面的view tree,view和subviews的树形结构。

这种flutter引擎的一部分,内部都已经实现。

下图为flutter渲染流程:

image.png

GPU发送VSync到cpu 主线程(下面有介绍过程,flutter应该是通过displaylink捕获),执行dart代码,更新三棵树,renderObject Tree交给CPU的gpu渲染线程进行组合排版,Throttle(节流器)我理解是当合成完毕后,通知主线程可以进行下一次的树更新,否则过滤掉下次更新。然后交给skia执行渲染,skia调度openGL或者Vulkan(metal)进行图像绘制。

参与UI的构建和显示涉及到两个线程分别是界面线程(UI Thread)和光栅线程(GPU Thread),

UI线程做构建流水线工作(开发者编写的代码), 光栅线程做UI绘制工作(图形库 Skia 在此线程上运行)。

GPU每隔一定的时间发出一个Vsync信号这个时间由屏幕的刷新率决定,以60HZ的刷新率为例那么它的时间间隔就是1000/60 = 16.7 ms一次。
UI线程收到Vsync信号后就会做UI的构建工作(需要在16.7ms内完成否则出现丢帧),然后发送到光栅线程GPU线程。
GPU Thread收到UI Thread发来的UI数据后就会通过Skia去上屏渲染。
这里说的UI线程指的应该是CPU的主线程。

而GPU线程指的应该调度GPU的CPU线程,即通过Skia进行渲染(最终调用GPU光栅化)的线程,是CPU上的一个常驻线程。(这个线程通过与GPU通信,并不是gpu上开的子线程,GPU负责执行opengl指令并显示到缓冲区或屏幕,没有子线程的概念,这里容易理解错)

以上方式根据Vsync来触发渲染,避免了多余的绘制。

这个流程和iOS UIKit自身的绘制机制如出一辙,flutter以自己的方式实现了和iOS相同的绘制工作。

image.png

image.png

iOS 的显示系统是由 VSync 信号驱动的,VSync 信号由硬件时钟生成,每秒钟发出 60 次(这个值取决设备硬件)。iOS 图形服务接收到 VSync 信号后,会通过 IPC(进程间通信CFMachPort) 通知到 App 内。App 的 Runloop 在启动后会注册对应的 CFRunLoopSource (source1)通过 mach_port 接收传过来的时钟信号通知,随后 Source 的回调会驱动整个 App 的动画与显示。

渲染管线流程如下:从Vsync到输出layer Tree,Dart处理的cpu主线程阶段。

image.png
Animate:

flutter中实现动画需要state支持SingleTickerProviderStateMixin,而SingleTickerProviderStateMixin内部用到了Ticker,Ticker就是类似iOS中的displaylink。

flutter中动画原理就是ticker接收到ticker回调(或者说由Vsync触发),会每一帧去调用setState(),让组件变成dirty状态。下一帧时组件将会执行 build 函数。

Tick animations to change widget state,即ticker唤起动画,通过setState改变widget的状态为dirty。

Build:

当组件状态发生变化时(setState),下一帧时组件将会执行 build 函数进行重建。

Rebuild widgets to account for changes。通过重建组件的方式来解释变化。

Layout:

更新RenderObject的尺寸和位置,前文说了,Widget会重新创建,但RenderObject只会更新。

Paint:

记录要展示列表,用于合成layers

Flutter中的三棵树

Flutter的开发更像是面向Widget编程,Widget内部又封装了Element以及RenderObject 那么我们先从Flutter中的三棵树说起:

Widget 组件树
Element 逻辑树
RenderObject 渲染树


image.png

从上图可以看出Widget和Element的数量是一一对应的,而RenderObject不是。查看framework.dart源码后可以发现只有RenderObjectWidget的派生类才会有RenderObject,其他的Widget都不具备渲染能力。

image.png
  1. 如果我们的Widget是StatelessWidget,那么当他的内容被创建出来之后,就不能再改变了。相反StatefulWidget就可以。
  2. 无状态Widget,就是说一旦这个Widget创建完成,状态就不允许再变动。有状态Widget,就是说当前Widget创建完成之后,还可以对当前Widget做更改,可以通过setState函数来刷新当前Widget来达到有状态。
  3. StatelessWidget是一个不需要状态更改的widget,它没有要管理的内部状态。StatefulWidget是可变状态的widget。
    StatelessWidget和StatefulWidget的区别在可变的State。


    image.png

class MaterialApp extends StatefulWidget
State<MaterialApp> createState() => _MaterialAppState();
SingleChildRenderObjectWidget extends RenderObjectWidget

image.png

1.创建widget树
2.调用runApp(rootWidget),将rootWidget传给rootElement,做为rootElement的子节点,生成Element树,由Element树生成Render树

Widget:存放渲染内容、视图布局信息,widget的属性最好都是immutable(如何更新数据呢?查看后续内容)
Element:存放上下文,通过Element遍历视图树,Element同时持有Widget和RenderObject
RenderObject:根据Widget的布局属性进行layout,paint Widget传入的内容

更新树 为什么widget都是immutable?

flutter界面开发是一种响应式编程,主张simple is fast,flutter设计的初衷希望数据变更时发送通知到对应的可变更节点(可能是一个StatefullWidget子节点,也可以是rootWidget),由上到下重新create widget树进行刷新,这种思路比较简单,不用关心数据变更会影响到哪些节点。

widget只是一个配置数据结构,创建是非常轻量的,加上flutter团队对widget的创建/销毁做了优化,不用担心整个widget树重新创建所带来的性能问题

树的更新规则

1.找到widget对应的element节点,设置element为dirty,触发drawframe, drawframe会调用element的performRebuild()进行树重建
2.widget.build() == null, deactive element.child,删除子树,流程结束
3.element.child.widget == NULL, mount 的新子树,流程结束
4.element.child.widget == widget.build() 无需重建,否则进入流程
5.Widget.canUpdate(element.child.widget, newWidget) == true,更新child的slot,element.child.update(newWidget)(如果child还有子节点,则递归上面的流程进行子树更新),流程结束,否则转6
6.Widget.canUpdate(element.child.widget, newWidget) != true(widget的classtype 或者 key 不相等),deactivew element.child,mount 新子树

注意事项:
1.element.child.widget == widget.build(),不会触发子树的update,当触发update的时候,如果没有生效,要注意widget是否使用旧widget,没有new widget,导致update流程走到该widget就停止了
2.子树的深度变化,会引起子树重建,如果子树是一个复杂度很高的树,可以使用GlobalKey做为子树widget的key。GlobalKey具有缓存功能

如何触发树更新
1.全局更新:调用runApp(rootWidget),一般flutter启动时调用后不再会调用
2.局部子树更新, 将该子树做StatefullWidget的一个子widget,并创建对应的State类实例,通过调用state.setState() 触发该子树的刷新

StatefullWidget vs StatelessWidget

1.StatelessWidget:无中间状态变化的widget,需要更新展示内容就得通过重新new,flutter推荐尽量使用StatelessWidget
2.StatefullWidget:存在中间状态变化,那么问题来了,widget不是都immutable的,状态变化存储在哪里?flutter 引入state的类用于存放中间态,通过调用state.setState()进行此节点及以下的整个子树更新
参考:https://www.yuque.com/xytech/flutter/tge705#kxc5ge
RendererBinding 混入类(mixin)是render tree和flutter engine的胶水。RendererBinding实现了drawFrame方法。

WidgetsBinding是widgets layer和flutter engine的胶水。WidgetsBinding实现了RendererBinding。

WidgetsBinding重写了RendererBinding的drawFrame方法,并在内部调用了super.drawFrame。

WidgetsFlutterBinding是类,不是mixin。负责绑定framework和flutter engine。

/// The glue between the render tree and the Flutter engine.
mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, HitTestable
/// The glue between the widgets layer and the Flutter engine.
mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding
/// A concrete binding for applications based on the Widgets framework.
///
/// This is the glue that binds the framework to the Flutter engine.
///
/// When using the widgets framework, this binding, or one that
/// implements the same interfaces, must be used. The following
/// mixins are used to implement this binding:
///
/// * [GestureBinding], which implements the basics of hit testing.
/// * [SchedulerBinding], which introduces the concepts of frames.
/// * [ServicesBinding], which provides access to the plugin subsystem.
/// * [PaintingBinding], which enables decoding images.
/// * [SemanticsBinding], which supports accessibility.
/// * [RendererBinding], which handles the render tree.
/// * [WidgetsBinding], which handles the widget tree.
class WidgetsFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(new MyApp());
}

image.png

binding更详细的说明:https://blog.csdn.net/jdsjlzx/article/details/127693662

这里的drawframe是WidgetsBinding.drawFrame。frame个人理解为帧的概念,而不是iOS里的位置大小信息,drawFrame代表绘制帧。

  1. void drawFrame() {

  2. ...

  3. try {

  4. if (renderViewElement != null)

  5. // 对标脏的Element进行重建,从而生成新的三棵树

  6. buildOwner!.buildScope(renderViewElement!);

  7. //见RendererBinding#drawFrame

  8. super.drawFrame();

  9. //卸载不再使用的Element

  10. buildOwner!.finalizeTree();

  11. } finally {

  12. }

  13. }

buildScope内调用

element.rebuild();

void rebuild() {
。。。

performRebuild();
}

dirty作为Element抽象类的属性。

abstract class Element extends DiagnosticableTree

/// Returns true if the element has been marked as needing rebuilding. bool get dirty => _dirty;
bool _dirty = true;

把一个element加入到脏列表,然后它将会被重建,当WidgetsBinding.drawFrame方法调用buildScope时。
前文

/// Adds an element to the dirty elements list so that it will be rebuilt /// when [WidgetsBinding.drawFrame] calls [buildScope].

class BuildOwner

final List<Element> _dirtyElements = <Element>[];

整棵「Element Tree」共享同一个BuildOwner实例 (全局的),在 Element 挂载过程中由 parent 传递给 child element。

BuildOwner

/// Cause the widget to update itself. /// /// Called by [rebuild] after the appropriate checks have been made. @protected
void performRebuild();
Element的该方法没有实现,其子类ComponentElement实现。

abstract class ComponentElement extends Element

class StatelessElement extends ComponentElement

class StatefulElement extends ComponentElement

abstract class StatelessWidget extends Widget {
const StatelessWidget({ Key? key }) : super(key: key);
@override
StatelessElement createElement() => StatelessElement(this);
@protected
Widget build(BuildContext context);
}

abstract class StatefulWidget extends Widget {
const StatefulWidget({ Key? key }) : super(key: key);
@override
StatefulElement createElement() => StatefulElement(this);
@protected
@factory State createState(); // ignore: no_logic_in_create_state, this is the original sin }

with Diagnosticable

@protected Widget build(BuildContext context);

可以看到StatelessWidget声明了build方法,但StatefulWidget没有build方法,而是在State里面提供build方法。

ComponentElement:

属性 Element? _child;

/// Calls the [StatelessWidget.build] method of the [StatelessWidget] object /// (for stateless widgets) or the [State.build] method of the [State] object /// (for stateful widgets) and then updates the widget tree. /// /// Called automatically during [mount] to generate the first build, and by /// [rebuild] when the element needs updating. @override
@pragma('vm:notify-debugger-on-exception')
void performRebuild() {
assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(true));
Widget? built;
try {
assert(() {
_debugDoingBuild = true;
return true;
}());
built = build();
assert(() {
_debugDoingBuild = false;
return true;
}());
debugWidgetBuilderValue(widget, built);
} catch (e, stack) {
_debugDoingBuild = false;
built = ErrorWidget.builder(
_debugReportException(
ErrorDescription('building this'), e, stack, informationCollector: () => <DiagnosticsNode>[ if (kDebugMode) DiagnosticsDebugCreator(DebugCreator(this)), ], ), ); } finally { // We delay marking the element as clean until after calling build() so // that attempts to markNeedsBuild() during build() will be ignored. _dirty = false; assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(false)); } try { _child = updateChild(_child, built, slot); assert(_child != null); } catch (e, stack) { built = ErrorWidget.builder( _debugReportException( ErrorDescription('buildingthis'),
e,
stack,
informationCollector: () => <DiagnosticsNode>[
if (kDebugMode)
DiagnosticsDebugCreator(DebugCreator(this)),
],
),
);
_child = updateChild(null, built, slot);
}
}

ComponentElement:
/// Subclasses should override this function to actually call the appropriate /// build function (e.g., [StatelessWidget.build] or [State.build]) for /// their widget. @protected Widget build();

class StatelessElement extends ComponentElement {

@override
Widget build() => (widget as StatelessWidget).build(this);

class StatefulElement extends ComponentElement {

State<StatefulWidget> get state => _state!;
State<StatefulWidget>? _state;

StatefulElement(StatefulWidget widget)
: _state = widget.createState(),
super(widget) {
。。。
@override
Widget build() => state.build(this);
以上可以看到StatelessElement和StatefulElement返回的就是widget.buid或者state.build。
且element在创建时候,同时会调用widget.createState()。
child即ComponentElement这个组合Element装载的合成后的Element。即build方法得到的widget关联的element。

Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
// 如果新的build方法得到的widget为nil,且旧的child存在,则遣散旧的child。
if (newWidget == null) {
if (child != null)
deactivateChild(child);
return null;
}
//如果新的build方法得到的widget不为nil,且旧的child存在
final Element newChild;
if (child != null) {
bool hasSameSuperclass = true;
// When the type of a widget is changed between Stateful and Stateless via
// hot reload, the element tree will end up in a partially invalid state. // That is, if the widget was a StatefulWidget and is now a StatelessWidget, // then the element tree currently contains a StatefulElement that is incorrectly // referencing a StatelessWidget (and likewise with StatelessElement). // // To avoid crashing due to type errors, we need to gently guide the invalid // element out of the tree. To do so, we ensure that the hasSameSuperclass condition // returns false which prevents us from trying to update the existing element // incorrectly. // // For the case where the widget becomes Stateful, we also need to avoid // accessing StatelessElement.widget as the cast on the getter will // cause a type error to be thrown. Here we avoid that by short-circuiting // the Widget.canUpdate check once hasSameSuperclass is false. assert(() {
final int oldElementClass = Element._debugConcreteSubtype(child);
final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);
hasSameSuperclass = oldElementClass == newWidgetClass;
return true;
}());
if (hasSameSuperclass && child.widget == newWidget) {
// We don't insert a timeline event here, because otherwise it's
// confusing that widgets that "don't update" (because they didn't // change) get "charged" on the timeline. if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
newChild = child;
} else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
final bool isTimelineTracked = !kReleaseMode && _isProfileBuildsEnabledFor(newWidget);
if (isTimelineTracked) {
Map<String, String> debugTimelineArguments = timelineArgumentsIndicatingLandmarkEvent;
assert(() {
if (kDebugMode) {
debugTimelineArguments = newWidget.toDiagnosticsNode().toTimelineArguments();
}
return true;
}());
Timeline.startSync(
'${newWidget.runtimeType}',
arguments: debugTimelineArguments,
);
}
child.update(newWidget);
if (isTimelineTracked)
Timeline.finishSync();
assert(child.widget == newWidget);
assert(() {
child.owner!._debugElementWasRebuilt(child);
return true;
}());
newChild = child;
} else {
deactivateChild(child);
assert(child._parent == null);
// The [debugProfileBuildsEnabled] code for this branch is inside
// [inflateWidget], since some [Element]s call [inflateWidget] directly // instead of going through [updateChild]. newChild = inflateWidget(newWidget, newSlot);
}
} else {
// The [debugProfileBuildsEnabled] code for this branch is inside
// [inflateWidget], since some [Element]s call [inflateWidget] directly // instead of going through [updateChild]. newChild = inflateWidget(newWidget, newSlot);
}

assert(() {
if (child != null)
_debugRemoveGlobalKeyReservation(child);
final Key? key = newWidget.key;
if (key is GlobalKey) {
assert(owner != null);
owner!._debugReserveGlobalKeyFor(this, newChild, key);
}
return true;
}());

return newChild;
}

当Flutter收到Vsync的时候就会做UI的构建工作,最终会调用RendererBinding的drawFrame()

@protected
void drawFrame() {
  assert(renderView != null);
  //1,布局逻辑,确定大小
  pipelineOwner.flushLayout();
  pipelineOwner.flushCompositingBits();
  //2,绘制逻辑,拿到SkCanvas绘制到layer上。具体逻辑见RenderObject中的paint方法
  pipelineOwner.flushPaint();
  if (sendFramesToEngine) {
    renderView.compositeFrame(); // this sends the bits to the GPU
    pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
    _firstFrameSent = true;
  }
}

renderView.compositeFrame()最终生成UI数据发送到GPU实现渲染,RenderView是Flutter中根部的RenderObject。compositeFrame的核心代码如下:

上传合成后的layer树到引擎,实际上引起渲染管线的输出结果显示到屏幕。

/// Uploads the composited layer tree to the engine. /// /// Actually causes the output of the rendering pipeline to appear on screen.

void compositeFrame() {
    //创建SceneBuilder,获取到引擎层的句柄
    final ui.SceneBuilder builder = ui.SceneBuilder();

    //Scene 最终是通过SceneBuilder生成的,也是引擎层的句柄,此处的layer就是根部的layer,它会合成所有的layer
    final ui.Scene scene = layer!.buildScene(builder);
    //发送Scene到引擎
    _window.render(scene);
    scene.dispose();
}

在上述代码中可以找到layer的身影, layer!.buildScene(builder)就是做Layer Tree的合成。此处的layer是一个TransformLayer是ContainerLayer的子类。

什么是layer?

在绘制过程中渲染树RenderObject Tree将生成一个图层树Layer Tree,Layer Tree合成后发送到引擎渲染上屏。大多数Layer的特性都可以更改,并且可以将图层移动到不同的父层,且Layer树不会保持其自身的脏状态。要合成树先要在根部的Layer创建SceneBuilder对象,并调用Layer中的addToScene方法添加到SceneBuilder上(Flutter中默认根部的layer是一个TransformLayer)。

dirty:表示当前组件为脏状态,下一帧时将会执行 build 函数,调用 setState 方法或者执行 didUpdateWidget 方法后,组件的状态为 dirty。
layer分为五大类:

PictureLayer 图像绘制 如:Text ,Image
TextureLayer 外接纹理 如: 视频播放
PlatformViewLayer Flutter嵌套Native View
PerformanceOverlayLayer 性能监控相关,能够显示出GPU对当前帧光栅化的耗时以及帧渲染的耗时
ContainerLayer 复合层 相当于一个根节点可以合成多个叶子节点的layer ,TransformLayer属于ContainerLayer的派生类


image.png

简而言之 RenderObject只负责绘制逻辑而 Layer才是最终输出到Skia的产物。

更详细的关于layer的说明:https://zhuanlan.zhihu.com/p/443562759

image.png

class RenderFlowLayout extends RenderLayoutBox
RenderFlexLayout extends RenderLayoutBox
class RenderLayoutBox extends RenderBoxModel
class RenderBoxModel extends RenderBox
其中,RenderFlowLayout RenderFlexLayout RenderLayoutBox RenderBoxModel都是webf的源码,RenderBox是flutter的源码。

因此,先要理解RenderBox。

abstract class RenderBox extends RenderObject
abstract class RenderObject extends AbstractNode
RenderFlex和RenderFlexLayout类似,RenderFlex也继承自RenderBox,RenderFlex是flutter内置类。
RenderFlex是flutter内部的renderObject,webf无法修改。RenderFlexLayout是自定义的renderObject。
RenderFlex extends RenderBox
class RenderView extends RenderObject
下面是flutter三棵树的继承结构:


image.png

可以看到RenderBox就是渲染树的节点。


image.png

/// The root of the render tree.
///
/// The view represents the total output surface of the render tree and handles
/// bootstrapping the rendering pipeline. The view has a unique child
/// [RenderBox], which is required to fill the entire output surface.
class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox> {
/// Creates the root of the render tree.
///

/// Typically created by the binding (e.g., [RendererBinding]).

RenderView 对应的是 RenderObject 树的根节点,RenderView 根节点下只有唯一一个 RenderBox 作为叶节点,一般在binding(比如RendererBinding)时创建。
可以看到,在RendererBinding类里,有个创建renderView方法。
/// Creates a [RenderView] object to be the root of the
/// [RenderObject] rendering tree, and initializes it so that it
/// will be rendered when the next frame is requested.
///
/// Called automatically when the binding is created.
void initRenderView() {
assert(!_debugIsRenderViewInitialized);
assert(() {
_debugIsRenderViewInitialized = true;
return true;
}());
renderView = RenderView(configuration: createViewConfiguration(), window: window);
renderView.prepareInitialFrame();
}
前文说到,当Flutter收到Vsync的时候就会做UI的构建工作,最终会调用RendererBinding的drawFrame(),而这里就会调用renderView(树的根节点)的合成方法。
renderView.compositeFrame(); // this sends the bits to the GPU

并进行layer的合成。
//Scene 最终是通过SceneBuilder生成的,也是引擎层的句柄,此处的layer就是根部的layer,它会合成所有的layer
final ui.Scene scene = layer!.buildScene(builder);
//发送Scene到引擎
_window.render(scene);
RenderObject 的子类继承树,
发现有 3 个 Mixin 以及 RenderAbstractViewport 和 RenderSliver 没有继承自 RenderBox。
RenderAbstractViewport 和 RenderSliver 主要处理滑动相关的控件展示,如 ListView 和 ScrollView。
DebugOverflowIndicatorMixin 用于在 debug 下提示绘制是否溢出,该类仅用于 debug。

RenderObjectWithChildMixin 用于为只有 1 个 child 的 RenderObject 提供 child 管理模型。

ContainerRenderObjectMixin 用于为有多个 child 的 RenderObject 提供 child 管理模型。

这两个 mixin 是非常常用的,看一下 Hierarchy 可以发现基本上每个 RenderBox 都混入了他们,省去了自己管理 child 的代码。

除此之外还有一个类也有相当多的子类:RenderProxyBox。

class RenderProxyBox extends RenderBox with RenderObjectWithChildMixin<RenderBox>, RenderProxyBoxMixin<RenderBox>

Flutter 本身提供了大量Widget以供开发,但是难免有通过组合完成不了的效果,此时就需要我们自己来实现 RenderObject 了。

RenderBox:
A render object in a 2D Cartesian coordinate system.

使用了 2D 笛卡尔坐标系来标识位置的renderobject。

RenderBox child 的逻辑区别以 mixin 来解决,如果想拥有 child,混入上一节所讲的 RenderObjectWithChildMixin 或 ContainerRenderObjectMixin 就可以了。
在 RenderBox 中,控件大小的值为 _size 成员,它只包含宽高两个属性值,我们可以通过该成员的 set 和 get 方法访问或修改它的值。
在测量时,parent 会传给当前 RenderBox 一个大小的限制,为 BoxConstraints 类型,
通过 constraints 这个 get 方法可以获取到,最后测量得到的 size 必须满足这个限制,
在 Flutter 的 debug 模式下对 size 是否满足 constraints 做了 assert 检查,如果检查未通过就会布局失败。所以测量上我们要做的是下面两点:
如果没有 child,那么根据自身的属性计算出满足 constraints 的 size.
如果有 child,那么综合自身的属性和 child 的测量结果计算出满足 constraints 的 size.
/// The layout, painting, hit testing, and semantics protocols are common to all /// render objects.

自定义的RenderObject还需要实现hitTest方法才能响应手势,这里hitTest类似iOS的hit Test。

关于自定义RenderObject可参考:http://www.xujingzi.com/nav/m/98639.html

webf中自定义了一下RenderObject。

class RenderFlowLayout extends RenderLayoutBox

RenderFlexLayout extends RenderLayoutBox

class RenderLayoutBox extends RenderBoxModel

class RenderBoxModel extends RenderBox

BoxConstraints:

class BoxConstraints extends Constraints

BoxConstraints( {double minWidth: 0.0, double maxWidth: double.infinity, double minHeight: 0.0, double maxHeight: double.infinity} )

BoxConstraints详细说明: https://zhuanlan.zhihu.com/p/473279008

image.png

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351

推荐阅读更多精彩内容