Flutter的原理及美团的实践(中)

.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一样的能力:

  1. 在调用Flutter页面之前将指定的图片资源按照设备屏幕密度缩放,并存储在App私有目录下。

  2. 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() {

@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.gnithird_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(“KaTeX parse error: Undefined control sequence: \n at position 4: obj\̲n̲stack”);

});

}

这样我们就可以实现全方位的异常监控和完善的降级策略,最大程度减少灰度时可能对用户带来的影响。

分析崩溃堆栈和异常数据

Flutter的引擎部分全部使用C/C++实现,为了减少包大小,所有的SO库在发布时都会去除符号表信息。和其他的JNI崩溃堆栈一样,我们上报的堆栈信息中只能看到内存地址偏移量等信息:


Build fingerprint: ‘Rock/odin/odin:7.1.1/NMF26F/1527007828:user/dev-keys’

Revision: ‘0’

Author: collect by ‘libunwind’

ABI: ‘arm64-v8a’

pid: 28937, tid: 29314, name: 1.ui >>> com.sankuai.meituan.takeoutnew <<<

signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0

backtrace:

r0 00000000 r1 ffffffff r2 c0e7cb2c r3 c15affcc

r4 c15aff88 r5 c0e7cb2c r6 c15aff90 r7 bf567800

r8 c0e7cc58 r9 00000000 sl c15aff0c fp 00000001

ip 80000000 sp c0e7cb28 lr c11a03f9 pc c1254088 cpsr 200c0030

#00 pc 002d7088 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so

#01 pc 002d5a23 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so

#02 pc 002d95b5 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so

#03 pc 002d9f33 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so

#04 pc 00068e6d /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so

#05 pc 00067da5 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so

#06 pc 00067d5f /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so

#07 pc 003b1877 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so

#08 pc 003b1db5 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so

#09 pc 0000241c /data/data/com.sankuai.meituan.takeoutnew/app_flutter/vm_snapshot_instr

单纯这些信息很难定位问题,所以我们需要使用NDK提供的ndk-stack来解析出具体的代码位置:

ndk-stack -sym PATH [-dump PATH]

Symbolizes the stack trace from an Android native crash.

-sym PATH sets the root directory for symbols

-dump PATH sets the file containing the crash dump (default stdin)

如果使用了定制过的引擎,必须使用engine/src/out/android-release下编译出的libflutter.so文件。一般情况下我们使用的是官方版本的引擎,可以在flutter_infra页面直接下载带有符号表的SO文件,根据打包时使用的Flutter工具版本下载对应的文件即可。比如0.4.4 beta版本:

$ flutter --version # version命令可以看到Engine对应的版本 06afdfe54e

Flutter 0.4.4 • channel beta • https://github.com/flutter/flutter.git

Framework • revision f9bb4289e9 (5 weeks ago) • 2018-05-11 21:44:54 -0700

Engine • revision 06afdfe54e

Tools • Dart 2.0.0-dev.54.0.flutter-46ab040e58

$ cat flutter/bin/internal/engine.version # flutter安装目录下的engine.version文件也可以看到完整的版本信息 06afdfe54ebef9168a90ca00a6721c2d36e6aafa

06afdfe54ebef9168a90ca00a6721c2d36e6aafa

最后

在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

flutter安装目录下的engine.version文件也可以看到完整的版本信息 06afdfe54ebef9168a90ca00a6721c2d36e6aafa

06afdfe54ebef9168a90ca00a6721c2d36e6aafa

最后

在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
[外链图片转存中…(img-GSr0VMXj-1715355452973)]

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 11
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值