Flutter 的原理及美团的实践(中),直击优秀开源框架灵魂
third_party/dart/sdk/lib/typed_data/*.dart
third_party/dart/sdk/lib/vmservice/*.dart
flutter/lib/ui/*.dart
platform.dill 则是实现了页面逻辑的代码,也包括 Flutter Framework 和其他由 pub 依赖的库代码:
flutter_tutorial_2/lib/main.dart
flutter/packages/flutter/lib/src/widgets/*.dart
flutter/packages/flutter/lib/src/services/*.dart
flutter/packages/flutter/lib/src/semantics/*.dart
flutter/packages/flutter/lib/src/scheduler/*.dart
flutter/packages/flutter/lib/src/rendering/*.dart
flutter/packages/flutter/lib/src/physics/*.dart
flutter/packages/flutter/lib/src/painting/*.dart
flutter/packages/flutter/lib/src/gestures/*.dart
flutter/packages/flutter/lib/src/foundation/*.dart
flutter/packages/flutter/lib/src/animation/*.dart
.pub-cache/hosted/pub.flutter-io.cn/collection-1.14.6/lib/*.dart
.pub-cache/hosted/pub.flutter-io.cn/meta-1.1.5/lib/*.dart
.pub-cache/hosted/pub.flutter-io.cn/shared_preferences-0.4.2/*.dart
kernel_blob.bin 和 platform.dill 都是由 flutter_tools 中的bundle.dart中调用KernelCompiler生成。
在 Release 模式(flutter run --release
)下,Flutter 会使用 Dart 的 AOT 运行模式,编译时将 Dart 代码转换成 ARM 指令:
kernel_blob.bin 和 platform.dill 都不在打包后的 APK 中,取代其功能的是(isolate/vm)snapshot(data/instr)四个文件。snapshot 文件由 Flutter SDK 中的flutter/bin/cache/artifacts/engine/android-arm-release/darwin-x64/gen_snapshot
命令生成,vm_snapshot_*是 Dart 虚拟机运行所需要的数据和代码指令,isolate_snapshot_*则是每个 isolate 运行所需要的数据和代码指令。
Flutter App 运行机制
Flutter 构建出的 APK 在运行时会将所有 assets 目录下的资源文件解压到 App 私有文件目录中的 flutter 目录下,主要包括处理字符编码的 icudtl.dat,还有 Debug 模式的 kernel_blob.bin、platform.dill 和 Release 模式下的 4 个 snapshot 文件。默认情况下 Flutter 在Application#onCreate
时调用FlutterMain#startInitialization
来启动解压任务,然后在FlutterActivityDelegate#onCreate
中调用FlutterMain#ensureInitializationComplete
来等待解压任务结束。
Flutter 在 Debug 模式下使用 JIT 执行方式,主要是为了支持广受欢迎的热刷新功能:
触发热刷新时 Flutter 会检测发生改变的 Dart 文件,将其同步到 App 私有缓存目录下,DartVM 加载并且修改对应的类或者方法,重建控件树后立即可以在设备上看到效果。
在 Release 模式下 Flutter 会直接将 snapshot 文件映射到内存中执行其中的指令:
在 Release 模式下,FlutterActivityDelegate#onCreate
中调用FlutterMain#ensureInitializationComplete
方法中会将 AndroidManifest 中设置的 snapshot(没有设置则使用上面提到的默认值)文件名等运行参数设置到对应的 C++同名类对象中,构造FlutterNativeView
实例时调用nativeAttach
来初始化 DartVM,运行编译好的 Dart 代码。
打包 Android Library
了解 Flutter 项目的构建和运行机制后,我们就可以按照其需求打包成 AAR 然后集成到现有原生 App 中了。首先在 andorid/app/build.gradle 中修改:
简单修改后我们就可以使用 Android Studio 或者 Gradle 命令行工具将 Flutter 代码打包到 aar 中了。Flutter 运行时所需要的资源都会包含在 aar 中,将其发布到 maven 服务器或者本地 maven 仓库后,就可以在原生 App 项目中引用。
但这只是集成的第一步,为了让 Flutter 页面无缝衔接到外卖 App 中,我们需要做的还有很多。
Flutter 默认将所有的图片资源文件打包到 assets 目录下,但是我们并不是用 Flutter 开发全新的页面,图片资源原来都会按照 Android 的规范放在各个 drawable 目录,即使是全新的页面也会有很多图片资源复用的场景,所以在 assets 目录下新增图片资源并不合适。
Flutter 官方并没有提供直接调用 drawable 目录下的图片资源的途径,毕竟 drawable 这类文件的处理会涉及大量的 Android 平台相关的逻辑(屏幕密度、系统版本、语言等等),assets 目录文件的读取操作也在引擎内部使用 C++实现,在 Dart 层面实现读取 drawable 文件的功能比较困难。Flutter 在处理 assets 目录中的文件时也支持添加多倍率的图片资源,并能够在使用时自动选择,但是 Flutter 要求每个图片必须提供 1x 图,然后才会识别到对应的其他倍率目录下的图片:
flutter:
assets:
images/cat.png
images/2x/cat.png
images/3.5x/cat.png
new Image.asset('images/cat.png');
这样配置后,才能正确地在不同分辨率的设备上使用对应密度的图片。但是为了减小 APK 包体积我们的位图资源一般只提供常用的 2x 分辨率,其他分辨率的设备会在运行时自动缩放到对应大小。针对这种特殊的情况,我们在不增加包体积的前提下,同样提供了和原生 App 一样的能力:
在调用 Flutter 页面之前将指定的图片资源按照设备屏幕密度缩放,并存储在 App 私有目录下。
Flutter 中使用时通过自定义的
WMImage
控件来加载,实际是通过转换成 FileImage 并自动设置 scale 为 devicePixelRatio 来加载。
这样就可以同时解决 APK 包大小和图片资源缺失 1x 图的问题。
Flutter 和原生代码的通信
我们只用 Flutter 实现了一个页面,现有的大量逻辑都是用 Java 实现,在运行时会有许多场景必须使用原生应用中的逻辑和功能,例如网络请求,我们统一的网络库会在每个网络请求中添加许多通用参数,也会负责成功率等指标的监控,还有异常上报,我们需要在捕获到关键异常时将其堆栈和环境信息上报到服务器。这些功能不太可能立即使用 Dart 实现一套出来,所以我们需要使用 Dart 提供的 Platform Channel 功能来实现 Dart→Java 之间的互相调用。
以网络请求为例,我们在 Dart 中定义一个MethodChannel
对象:
import 'dart:async';
import 'package:flutter/services.dart';
const MethodChannel _channel = const MethodChannel('com.sankuai.waimai/network');
Future<Map<String, dynamic>> post(String path, [Map<String, dynamic> form]) async {
return _channel.invokeMethod("post", {'path': path, 'body': form}).then((result) {
return new Map<String, dynamic>.from(result);
}).catchError((_) => null);
}
然后在 Java 端实现相同名称的 MethodChannel:
public class FlutterNetworkPlugin implements MethodChannel.MethodCallHandler {
private static final String CHANNEL_NAME = "com.sankuai.waimai/network";
@Override
public void onMethodCall(MethodCall methodCall, final MethodChannel.Result result) {
switch (methodCall.method) {
case "post":
RetrofitManager.performRequest(post((String) methodCall.argument("path"), (Map) methodCall.argument("body")),
new DefaultSubscriber<Map>() {
@Override
public void onError(Throwable e) {
result.error(e.getClass().getCanonicalName(), e.getMessage(), null);
}
@Override
public void onNext(Map stringBaseResponse) {
result.success(stringBaseResponse);
}
}, tag);
break;
default:
result.notImplemented();
break;
}
}
}
在 Flutter 页面中注册后,调用 post 方法就可以调用对应的 Java 实现:
loadData: (callback) async {
Map<String, dynamic> data = await post("home/groups");
if (data == null) {
callback(false);
return;
}
_data = AllCategoryResponse.fromJson(data);
if (_data == null || _data.code != 0) {
callback(false);
return;
}
callback(true);
}),
SO 库兼容性
Flutter 官方只提供了四种 CPU 架构的 SO 库:armeabi-v7a、arm64-v8a、x86 和 x86-64,其中 x86 系列只支持 Debug 模式,但是外卖使用的大量 SDK 都只提供了 armeabi 架构的库。
虽然我们可以通过修改引擎src
根目录和third_party/dart
目录下build/config/arm.gni
,third_party/skia
目录下的BUILD.gn
等配置文件来编译出 armeabi 版本的 Flutter 引擎,但是实际上市面上绝大部分设备都已经支持 armeabi-v7a,其提供的硬件加速浮点运算指令可以大大提高 Flutter 的运行速度,在灰度阶段我们可以主动屏蔽掉不支持 armeabi-v7a 的设备,直接使用 armeabi-v7a 版本的引擎。
做到这点我们首先需要修改 Flutter 提供的引擎,在 Flutter 安装目录下的bin/cache/artifacts/engine
下有 Flutter 下载的所有平台的引擎:
我们只需要修改 android-arm、android-arm-profile 和 android-arm-release 下的 flutter.jar,将其中的 lib/armeabi-v7a/libflutter.so 移动到 lib/armeabi/libflutter.so 即可:
cd $FLUTTER_ROOT/bin/cache/artifacts/engine
for arch in android-arm android-arm-profile android-arm-release; do
pushd $arch
cp flutter.jar flutter-armeabi-v7a.jar # 备份
unzip flutter.jar lib/armeabi-v7a/libflutter.so
mv lib/armeabi-v7a lib/armeabi
zip -d flutter.jar lib/armeabi-v7a/libflutter.so
zip flutter.jar lib/armeabi/libflutter.so
popd
done
这样在打包后 Flutter 的 SO 库就会打到 APK 的 lib/armeabi 目录中。在运行时如果设备不支持 armeabi-v7a 可能会崩溃,所以我们需要主动识别并屏蔽掉这类设备,在 Android 上判断设备是否支持 armeabi-v7a 也很简单:
public static boolean isARMv7Compatible() {
try {
if (SDK_INT >= LOLLIPOP) {
for (String abi : Build.SUPPORTED_32_BIT_ABIS) {
if (abi.equals("armeabi-v7a")) {
return true;
}
}
} else {
if (CPU_ABI.equals("armeabi-v7a") || CPU_ABI.equals("arm64-v8a")) {
return true;
}
}
} catch (Throwable e) {
L.wtf(e);
}
return false;
}
灰度和自动降级策略
Horn 是一个美团内部的跨平台配置下发 SDK,使用 Horn 可以很方便地指定灰度开关:
在条件配置页面定义一系列条件,然后在参数配置页面添加新的字段 flutter 即可:
因为在客户端做了 ABI 兜底策略,所以这里定义的 ABI 规则并没有启用。
Flutter 目前仍然处于 Beta 阶段,灰度过程中难免发生崩溃现象,观察到崩溃后再针对机型或者设备 ID 来做降级虽然可以尽量降低影响,但是我们可以做到更迅速。外卖的 Crash 采集 SDK 同时也支持 JNI Crash 的收集,我们专门为 Flutter 注册了崩溃监听器,一旦采集到 Flutter 相关的 JNI Crash 就立即停止该设备的 Flutter 功能,启动 Flutter 之前会先判断FLUTTER_NATIVE_CRASH_FLAG
文件是否存在,如果存在则表示该设备发生过 Flutter 相关的崩溃,很有可能是不兼容导致的问题,当前版本周期内在该设备上就不再使用 Flutter 功能。
除了崩溃以外,Flutter 页面中的 Dart 代码也可能发生异常,例如服务器下发数据格式错误导致解析失败等等,Dart 也提供了全局的异常捕获功能:
import 'package:wm_app/plugins/wm_metrics.dart';
void main() {
runZoned(() => runApp(WaimaiApp()), onError: (Object obj, StackTrace stack) {
uploadException("stack");
});
}
评论