作者 | 俞家欢
低头需要勇气,抬头需要实力
跨平台概念是软件开发中一个重要的概念,即不依赖于操作系统,也不依赖硬件环境。一个操作系统下开发的应用,放到另一个操作系统下依然可以运行。
众所周知目前移动端主要有两大体系,一个是 Google 公司引领的 Android 体系,一个是 Apple 公司引领的 iOS 体系。两大平台应用开发技能树完全不同,因此当需要开发一个移动端应用时,需要在此两种系统体系上分别开发一遍。每一端都需要独立研发、测试,包括后续的维护工作也是如此,这样就会造成开发工作量和开发人员成倍增涨,增加研发费用和研发周期,可能会拖慢产品迭代的节奏。为了实现“偷懒”和“省钱”的目标,移动端跨平台技术便应运而生,目的是能够减小多端带来的开发成本,可以实现用一套代码跑通不同的移动平台。
移动端跨平台需要从两个维度去跨平台,一个是渲染层面,另一个逻辑层面。跨平台技术既可以是两个维度都跨平台,也可以只选一个维度进行跨平台。跨平台技术发展至今主要是是下面三种划分:
Web 容器 渲染使用系统原生提供的浏览器控件 (iOS 为 UIWebView/WKWebView,Android 为 WebView) 渲染 HTML5 页面,逻辑层面使用 JavaScript,定义 JS 与 原生代码的交互协议,将原生系统能力暴露给 H5。优点是可以开发效率高、维护成本低、具有动态化,缺点是渲染、性能与兼容性较差。代表框架为 Cordova。
原生渲染,逻辑跨平台 渲染使用各自系统提供的渲染方式,逻辑层使用特定语言将其编译成各端可以识别的产物去执行。代表框架为 ReactNative、Weex,代表语言为 Kotlin、C/C++。
自绘引擎,逻辑跨平台 渲染使用自建的跨平台绘制引擎,逻辑层使用特定语言将其编译成各端可以识别的产物去执行。代表框架为 Flutter。
Flutter 是 Google 开发的跨平台 UI 框架,当前支持移动端、Web 以及桌面端。Flutter 使用 Dart 作为开发语言,底层使用 Skia 渲染引擎。
Skia 是一款用 C++ 开发的、开源的、跨平台图形库。Skia 在图形转换、文字渲染、位图渲染方面都表现卓越,并提供了开发者友好的 API。
Flutter 架构于 Skia 之上 ,也因此拥有了彻底的跨平台渲染能力。通过与 Skia 的深度定制及优化,Flutter 可以最大限度地抹平平台差异,提高渲染效率与性能。底层渲染能力统一了,上层开发接口和功能体验也就随即统一了,开发者也就不用操心平台相关的渲染特性。也就是说,Skia 保证了同一套代码调用在各个平台上的渲染效果是完全一致的。Android 系统中默认集成了 Skia 绘制引擎,所以在打包 Android 产物时,不需要打包 Skia 引擎,这也是产物大小比 iOS 小的原因。
Dart 是快速开发多端应用的客户端优化语言。其目标是为多端开发提供高效的编程,并为应用程序框架提供灵活的执行运行时平台。
Dart 有两大类编译器
Native 平台
针对移动端以及桌面端设备应用。Dart Native 包括使用即时编译(JIT)的 Dart VM 和用于生成机器码的预先编译(AOT)编译器。
在应用开发过程中,高效开发至关重要。Dart VM 提供了即时编译(JIT),具有增量式编译(热更新/热重启)的特性以及丰富的调试支持。当应用程序准备部署到生产环境中时,Dart AOT 编译器可以提前编译成本地 ARM 或 x86_64 机器码。AOT 编译的代码运行在一个高效的 Dart 运行时中,该运行时强制开启类型安全检查,并使用快速对象分配和分代垃圾收集器管理内存。
Web 平台
针对 Web 应用。Dart Web 包括开发时编译器(dartdevc,增量开发)和生产时编译器(dart2js),两者都会将 Dart 代码翻译成 JavaScript 语言。
在 Dart Native 中,线程被抽象成了 isolate。每个 isolate 独享内存,包含两个事件队列和一个事件循环,执行过程如下图所示:
优先执行 MicroTaskQueue 里的任务,MicroTaskQueue 中的任务都执行完了,再去执行 EventQueue 任务,EventQueue 中每执行完一个任务,都会去检查 MicroTaskQueue 是否有任务,有任务的话,就会优先循环执行完 MicroTaskQueue。
Event Queue
负责处理 I/O 事件、绘制事件、手势事件、接收其他 isolate 消息等事件。
Microtask Queue
事件的优先级比 Event Queue高,Flutter 内部会将手势识别、文本输入、滚动视图、保存页面等一些高优先级的事件放在此事件队列中。
Dart 为 Event Queue 的任务建立提供了一层封装,叫作 Future,借助 await 与 async 关键字,可以通过事件循环实现非阻塞的同步等待。Dart 会将调用体的函数也视作异步函数,将等待语句的上下文放入 Event Queue 中,一旦有了结果,事件循环就会把它从 Event Queue 中取出,等待代码继续执行。
Flutter 在技术架构分为三层,从上至下依次是 Dart Framework, C/C++ Engine,Platform-specific Embedder。
Framework
用 Dart 编写,封装整个 Flutter 框架的核心功能,封装整个 Flutter 架构的核心功能,包括 Widget、动画、绘制、手势等功能,有Material(Android 风格 UI)和 Cupertino(iOS 风格)的 UI 界面,可构建 Widget 控件以及实现UI布局。
Engine
用 C++ 编写,用于高质量移动应用的轻量级运行时环境,实现了 Flutter 核心库,包括 Dart 虚拟机、动画和图形、文字渲染、通信通道、事件通知、插件架构等。
Embedder
平台中间层。
在不同的平台上,Framwork 和 Engine 是一样的,Embedder 中间层的实现是不同的。
Engine 不创建和管理线程,会要求 Embedder 创建 Platform、UI、Raster(GPU)以及 IO 四个 Task Runner 供 Engine 使用。Engine 并不关心这四个 Task Runner 分别具体跑在哪个线程中。但是为了获得最佳性能,Embedder 应该为每个 Task Runner 专门创建一个线程。
Platform Task Runner
对应平台(Android、iOS)的主线程,负责Embedder 和 Engine 层的交互,处理平台的消息。
UI Task Runner
对应平台(Android、iOS)的子线程,负责 Flutter Engine 执行 Root Isolate 中的所有 Dart 代码(所有 Dart Framework 层的代码都在此 Runner 中运行),执行渲染与处理 Vsync 信号,将 Widget 转换生成 Layer Tree。
Raster Task Runner
对应平台(Android、iOS)的子线程,真正执行渲染任务,光栅化所有从 UI Task Runner 中提交过来的任务,最终渲染到屏幕上。
IO Task Runner
对应平台(Android、iOS)的子线程,从磁盘/网络中读取图片,将图片数据进行处理为 GPU Task Runner 所需的格式进行渲染。
当前 iOS 和 Android 平台 上每个 Flutter Engine 实例启动都会为 UI、Raster 以及 IO 创建新线程,所有 Engine 实例共享一个 Platform Task Runner 和线程。
Flutter 官方提供了一种 Platform Channel 的方案,用于 Dart 和平台之间相互通信。其核心流程是:
Flutter 通过 Platform Channel 将传递的数据编码成消息的形式发送到平台。
平台接收到 Platform Channel 的消息后,调用相应平台的 API。
执行完成后将结果数据通过同样方式原路返回。
Platform Channel 涉及三个要素:
Channel Name
一个 Flutter 应用中可能存在多个 Channel,每个 Channel 在创建时必须指定一个独一无二的 Channel Name,Channel 之间使用 Channel Name 来区分彼此。当有消息从Flutter 发送到平台时,会根据其传递过来的 Channel Name 找到该 Channel 对应的消息处理器。
BinaryMessager
BinaryMessenger 是平台与 Flutter 通信的工具,通信使用的消息格式为二进制数据。当初始化一个 Platform Channel,并向该 Platform Channel 注册处理器时,会生成一个与之对应的 BinaryMessageHandler,并以 Channel Name 为键值,注册到 BinaryMessenger 中。当 Flutter 发送消息时,BinaryMessenger 会根据其传入的 Channel Name 找到对应的 BinaryMessageHandler,并交由其处理。由于 Platform Channel 从 BinaryMessageHandler 接收到的消息是二进制格式数据,无法直接使用。因此 Platform Channel 会将该二进制消息通过Codec(消息编解码器)解码为能识别的消息数据。
Codec
消息编解码器 Codec 主要用于将二进制格式的数据转化为能够识别的数据,Flutter 定义了两种 Codec,分别是 MessageCodec 和 MethodCodec。MessageCodec 用于二进制格式数据与基础数据之间的编解码。BasicMessageChannel 所使用的编解码器就是 MessageCodec。MethodCodec 用于二进制数据与方法调用和方法返回结果之间的编解码。MethodChannel 和 EventChannel 所使用的编解码器均为 MethodCodec。BasicMessageChannel、MethodChannel 以及 EventChannel 是 Flutter 提供的三种不同类型的 Platform Channel,在开发实践小节中会介绍其用法。
Flutter 默认的消息编解码器其支持的数据类型如下:
Widget 是 Flutter 世界里对视图的一种结构化描述,是控件实现的基本逻辑单位,里面存储的是有关视图渲染的配置信息,包括布局、渲染属性、事件响应信息等。Widget 可以是一个按钮,一种字体,一种颜色,一种布局,在 Flutter 的世界里"一切皆为 Widget"。Widget 被设计成是不可变的,所以当视图渲染的配置信息发生变化时,Flutter 会选择重建 Widget 树的方式进行数据更新。Widget 可以分为 StatelessWidget(无状态) 和 StatefulWidget(有状态)。
StatelessWidget
一旦创建成功就不再关心、也不响应任何数据变化进行重绘。如 Text、Container 等。
StatefulWidget
此类 Widget 创建完成后,还需要关心和响应数据变化来进行重绘。如 Image 等。调用 setState 方法会触发当前 Widget 以及其子 Widget 的重建。
initState
当 Widget 创建后调用的第一个方法,整个生命周期内只调用一次。
didChangeDependencies
第一次构建 Widget 时,会在 initState 之后立即调用此方法。build 方法总是在此方法被调用之后调用。
didUpdateWidget
组建重建时会调用此方法,如父 Widget 调用了 setState 方法。build 方法总是在此方法被调用之后调用。
deactivate
当 State 从树中移除的时候被调用。
dispose
当 State 永久被移除的时候被调用。
本文对应的 Flutter 开发环境如下:
Flutter 1.22.5 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 7891006299 (6 weeks ago) • 2020-12-10 11:54:40 -0800
Engine • revision ae90085a84
Tools • Dart 2.10.4
Flutter 开发 IDE 可以选择 vscode 也可以选择 Android Studio,本文将以 AndroidStudio 为例,插件版本如下:
Dart: 201.9317
Flutter: 52.1.1
Flutter 在表示一个控件大小时是不需要写单位的,如:
Container(
width: 100,
height: 100,
);
此 Container 在不同像素密度的屏幕上表示的像素大小是不同的。对于一倍屏,像素大小就为 100px_100px。两倍屏就表示为 200px_200px,以此类推。为了让 UI 设计能够在不同分辨率以及不同尺寸的屏幕下显示效果保持一致,可以借助小程序的屏幕适配思想,设计一个单位叫 rpx,让所有设备的宽度都变为 375 个长度单位。
class SizeFit {
static double rpx;
static void initialize(BuildContext context, {double standardWidth = 375}) {
if (rpx != null) {
return;
}
rpx = MediaQuery.of(context).size.width / standardWidth;
}
static double setRpx(num size) {
return SizeFit.rpx * size;
}
}
利用 Dart 语言的扩展方法,可以给 num 类型的值扩展 rpx 属性
extension NumberExt on num {
double get rpx {
return SizeFit.setRpx(this);
}
}
那么这样之后对于一个控件的大小表示就变为:
Container(
width: 100.0.rpx,
height: 100.0.rpx,
);
100.rpx 在设备上显示的像素大小就为 【宽度】*100/375。
从上面的小节知道,Flutter 与原生通信是通过 Flutter Platform Channel 实现的,Flutter 定义了三种不同类型的 Channel。
双向通信,用于传递字符串和半结构化信息。
Flutter
BasicMessageChannel messageChannel = BasicMessageChannel("CHANNEL_NAME", StandardMessageCodec());
messageChannel.send(message);
messageChannel.setMessageHandler((message) async {
return "";
});
消息监听
消息发送
定义
Android
定义
val messageChannel = BasicMessageChannel(binding.binaryMessenger, "CHANNEL_NAME", StandardMessageCodec())
消息发送
messageChannel.send(message:Any)
消息监听
messageChannel.setMessageHandler(object: BasicMessageChannel.MessageHandler {
override fun onMessage(message: Any?, reply: BasicMessageChannel.Reply) {
}
})
双向通信,用于传递方法调用。
Flutter
定义
MethodChannel methodChannel = MethodChannel("CHANNEL_NAME");
方法调用
methodChannel.invokeMethod("METHOD_NAME", arguments);
方法调用监听
_methodChannel.setMethodCallHandler((MethodCall call) async {
});
Android
定义
val methodChannel = MethodChannel(binding.binaryMessenger, "CHANNEL_NAME")
方法调用
methodChannel.invokeMethod("METHOD_NAME", arguments)
方法调用监听
channel.setMethodCallHandler(object : MethodChannel.MethodCallHandler{
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
}
})
单向通信(原生 => Flutter),用于事件通知。
Flutter
定义
EventChannel eventChannel = EventChannel("CHANNEL_NAME");
事件监听
StreamSubscription eventListener = eventChannel.receiveBroadcastStream().listen((event) {
});
取消监听事件
eventListener.cancel();
Android
定义
val eventChannel = EventChannel(binding.binaryMessenger, "CHANNEL_NAME")
事件通知
eventChannel.setStreamHandler(object: EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
}
override fun onCancel(arguments: Any?) {
}
})
try {
String a = null;
print(a.length);
} catch (e) {
// 异常处理
} finally {
print("finally");
}
Flutter 框架为我们在很多关键的地方进行了异常捕获。
举个例子,当布局发生越界或不和规范时,Flutter 就会自动弹出一个错误界面,这是因为 Flutter 已经在执行 build 方法时添加了异常捕获。
@override
void performRebuild() {
...
try {
// 执行build方法
built = build();
} catch (e, stack) {
// 有异常时则弹出错误提示
built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
}
...
}
可以看到,在发生异常时,Flutter 默认的处理方式是弹一个ErrorWidget,
继续追踪源码可以发现,Flutter 会调用 FlutterError 类的 onError 方法。那么只需要在 main 方法中,给 FlutterError 类的 onError 赋值需要的处理异常的逻辑即可。
void main() {
FlutterError.onError = (FlutterErrorDetails details) {
// 异常处理
};
}
在 Dart 中,异常分两类:同步异常和异步异常,同步异常可以通过 try/catch 捕获,而异步异常则比较麻烦,如下面的代码是捕获不了Future的异常的:
try {
Future.delayed(Duration(seconds: 1)).then((e) => Future.error("..."));
} catch (e) {
print(e)
}
Dart 中有一个 runZoned 方法,可以给执行对象指定一个 Zone。Zone 表示一个代码执行的环境范围,可以理解为沙箱。runZoned 可以捕获被包裹代码块中所有发生的未捕获的异常,包括异步异常。这样一来,就可以在 main 方法中做如下处理
void main() {
runZoned(() {
runApp(MyApp());
}, onError: (Object obj, StackTrace stack) {
// 异常处理
});
}
一般来说,不可能将现有的原生应用用 Flutter 重写一遍,大都数情况都是将 Flutter 作为一个库或模块,集成进现有的原生应用当中,去实现一些业务场景。目前 Flutter 混合开发是有在杏仁医生项目中应用,我们将原有通过 RN 编写的业务用 Flutter 重写了一下。
flutter create --org com.example --template=module hello
module 类型的 Flutter 工程就是专门用于集成到现有原生项目中的工程模版,此类型的 Flutter 工程不包含原生的代码,原生代码都需要编写在原生工程中。
Android
Android 项目有两种集成方式:一种是主工程直接依赖 Flutter 工程源码,另一种是主工程依赖 Flutter 工程的 aar 产物。这里只介绍第二种方式,这种方法更适合团队开发,符合组件化的思想。在 Flutter 工程根目录运行:
flutter build aar
执行完毕后就会在 ${rootProject}/build/host/repo 下生成工程本身以及依赖的 aar 产物。如果不指定任何参数,默认会生成三个变体的 aar 包,分别是 debug、profile 以及 release。如果不想构建 profile 变体,则可以尝试如下命令:
flutter build aar --no-profile
产物构建完毕后,在原生项目中直接引用即可:
debugImplementation 'xxxxxxxxxx:flutter_debug:1.0'
releaseImplementation 'xxxxxxxxxx:flutter_release:1.0'
Android
创建 Activity 类
// SampleFlutterActivity.kt
class SampleFlutterActivity : FlutterActivity() {
}
覆写方法,返回需要跳转的路由
// SampleFlutterActivity.kt
class SampleFlutterActivity : FlutterActivity() {
override fun getInitialRoute(): String {
return "${Flutter 页面路由}"
}
}
AndroidManifest 添加 FlutterActivity 的声明
启动 Activity
// SomeActivity/Fragment.kt
startActivity(
FlutterActivity
.withNewEngine()
.initialRoute("${Flutter 页面路由}")
.build(this)
)
常规方式
以上这种方式比较适合可以直接打开不需要传递参数的 Flutter 页面,需要处理参数的可以使用下面介绍的【继承方式】。
继承方式
以上方式就可以在 onCreate 回调中处理业务参数,跳转不同的路由或向 Flutter 页面中传递不同的参数。
每个 FlutterActivity 都会创建一个默认 FlutterEngine。每个 FlutterEngine 都会有一个启动预热时间。在原生跳转 Flutter 模块页面时会有一个 FlutterEngine 初始化延迟,在 debug 模式下,可以非常明显的感觉到,在 release 模式下就比较难感受到这个延迟。要最小化这种延迟,可以在启动 FlutterActivity 之前,预先创建一个 FlutterEngine 并初始化它。
Android
初始化 FlutterEngine
// MyApplication.kt
class MyApplication : Application() {
lateinit var flutterEngine : FlutterEngine
override fun onCreate() {
super.onCreate()
flutterEngine = FlutterEngine(this)
flutterEngine.getNavigationChannel().setInitialRoute("${Flutter 页面路由}");
flutterEngine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
)
FlutterEngineCache
.getInstance()
.put("${flutter_engine_id}", flutterEngine) // 此处填写一个自定义的 FlutterEngine ID
}
}
启动的时候带上 FlutterEngine Id
// SomeActivity/Fragment.kt
startActivity(
FlutterActivity
.withCachedEngine("${flutter_engine_id}")
.build(this)
)
【继承方式】重写相应方法
// SampleFlutterActivity.kt
class SampleFlutterActivity : FlutterActivity() {
override fun getCachedEngineId(): String? {
return "${flutter_engine_id}"
}
}
清除 FlutterEngine 的相关资源
FlutterEngineCache.getInstance().get("${flutter_engine_id}")?.destroy()
在 FlutterEngine 启动过程中,我们可以添加一个过渡加载页面友好显示。
Android
添加过渡页只适合【继承方式】
// SampleFlutterActivity.kt
class SampleFlutterActivity : FlutterActivity() {
override fun provideSplashScreen(): SplashScreen? {
return FlutterSplashScreen()
}
}
// FlutterSplashScreen.kt
class FlutterSplashScreen : SplashScreen {
override fun createSplashView(context: Context, savedInstanceState: Bundle?): View? {
val frameLayout = FrameLayout(context)
val progressBar = ProgressBar(context)
frameLayout.addView(
View(context),
FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
)
frameLayout.addView(
progressBar,
FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT)
.apply {
gravity = Gravity.CENTER
}
)
return frameLayout
}
override fun transitionToFlutter(onTransitionComplete: Runnable) {
onTransitionComplete.run()
}
}
Flutter 模块如何脱离原生工程直接运行呢?这里有两种情况。
Flutter 模块中不牵扯与任何与原生工程的交互
可以直接运行。默认程序的入口是 main.dart 中的 main 方法,可以在此文件中修改初始路由去直接跳转对应的页面。
// main.dart
...
void main() {
runApp(App());
}
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
...
initialRoute: window.defaultRouteName, // 此处可以修改初始路由
...
);
}
}
Flutter 模块中需要与原生工程交互
此种情况下直接独立运行会因为找不到 MethodChannel 而报错。为了解决此问题,在杏仁医生项目中我们的处理方式是定一个标记来表示当前 Flutter 模块是【独立运行】还是【集成运行】的。我们定一个 main_dev.dart 文件,通过 IDE 修改启动入口为 main_dev.dart,在 main_dev.dart 中修改标记为【独立运行】。在原生交互层对于识别到【独立运行】标记时,程序会 mock 与原生交互的数据。
// main_dev.dart
...
void main() {
AppConfig.isRunInFlutter = true;
runApp(App());
}
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
...
initialRoute: window.defaultRouteName, // 此处可以修改初始路由
...
);
}
}
// 原生交互层
...
Future
AndroidStudio 程序入口配置如下:
集成开发调试
从 App 中打开 Flutter 模块开发的页面
AndroidStudio 中点击 Flutter Attach
等待文件同步完成
同步完成之后,就可以 Hot Reload 或者 Hot Restart
纯 Dart 库,不涉及平台调用。
创建 Package
flutter create --org com.example --template=package hello
这条指令会在当前位置创建一个文件夹名为 hello 的 Package 项目。目录结构如下:
├── CHANGELOG.md
├── LICENSE
├── README.md
├── hello.iml
├── lib
│ └── hello.dart
├── pubspec.lock
├── pubspec.yaml
└── test
└── hello_test.dart
实现 Package
对于纯 Dart 库的 Package,只要在 /hello/lib 目录中创建 Dart 文件编写 Dart 代码实现功能。
使用 Dart 编写,按需使用 Java/Kotlin 和 OC/Swift 分别在 Android 和 iOS 平台实现的库。
创建 Plugin
flutter create --org com.example --template=plugin --platforms=android,ios hello
这条指令会在当前位置创建一个文件夹名为 hello 的 Plugin 项目。目录结构如下:
├── CHANGELOG.md
├── LICENSE
├── README.md
├── android
│ ├── build.gradle
│ ├── gradle
│ ├── gradle.properties
│ ├── hello_android.iml
│ ├── local.properties
│ ├── settings.gradle
│ └── src
├── example
│ ├── README.md
│ ├── android
│ ├── hello_example.iml
│ ├── ios
│ ├── lib
│ ├── pubspec.lock
│ ├── pubspec.yaml
│ └── test
├── hello.iml
├── ios
│ ├── Assets
│ ├── Classes
│ └── hello.podspec
├── lib
│ └── hello.dart
├── pubspec.lock
├── pubspec.yaml
└── test
└── hello_test.dart
实现 Plugin
使用 AndroidStudio 打开 /hello/example/android 文件夹。
在工程中找到 /hello/android 路径的 module,在此 module 中编写 Android 平台相关的代码。
在 /hello/lib 目录下创建 Dart 文件编写 Dart 代码。
添加 Android 平台代码
动态化也需要两个维度分别考虑去实现,一个是渲染,一个是逻辑。Flutter 本身是不支持动态化的。
渲染层面
可以通过某种形式(AST)描述页面,在运行时将此描述动态转成 Flutter 的 Widget 实现 UI 动态化。
逻辑层面(由于 iOS 平台的限制,逻辑层面也没有其他方案可以选择)
iOS 平台使用自带的 JSCore,Android 平台使用 V8 引擎来实现动态化。因此逻辑代码需要使用 JavaScript 来编写。
完整的流程应该是这样的:
前端可以使用 HTML/CSS/JS 开发页面以及功能逻辑。
前端编译器将开发好的程序转成特殊的描述方式。
Flutter 应用可以在运行时解析此描述方式进行 UI 渲染和事件绑定,利用 JSCore 和 V8 引擎加载 Javascript 脚本。
[Flutter 官方文档] : https://flutter.dev/docs
[Flutter 开发文档] : https://github.com/flutter/flutter
[Dart 官方文档] : https://dart.dev/
[移动端跨平台开发的深度解析] : https://zhuanlan.zhihu.com/p/41595544
[跨平台历史发展逻辑] : https://cloud.tencent.com/developer/article/1485193
全文完
以下文章您可能也会感兴趣:
简单说说spring的循环依赖
Mysql redo log 漫游
一个 AOP 缓存失效问题的排查
小程序开发的几个好的实践
RabbitMQ 如何保证消息可靠性
压力测试必知必会
分布式 Session 之 Spring Session 架构与设计
缓存的那些事
Java 并发编程 -- 线程池源码实战
Lombok Builder 构建器做了哪些事情?
WePY 2.0 新特性
SSL证书的自动化管理
了解一下第三方登录
分布式 ID 生成策略
我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 [email protected] 。