android层下发cmd_Android上Flutter动态化设计与实践

Flutter作为一种跨平台的解决方案被越来越多的人认可,各种类型的Flutter应用也纷纷上线。作为一款成熟上线的App,能否在少干扰用户的情况下及时修复bug,快速让用户体验到新功能一直是开发人员的研究方向。考虑到iOS平台的限制以及目前团队的主要诉求是快速上线新设计,对平台要求不大,所以我们先在Android平台上做了产物动态化的尝试和实践。

方案设计

对于一个Flutter应用,动态化主要考虑三个部分:

  • Dart代码

  • Asset资源文件

  • Java代码

Flutter应用中Java代码的动态化方案和普通的Android应用中方案一致,目前已经有很多成熟的优秀框架,在此不再赘述。接下来将重点分析Flutter应用中如何动态化Dart代码及Dart代码中用到的本地资源文件。

Dart代码产物libapp.so

在Flutter 1.7.8版本之前release包中产物文件有assets/isolate_snapshot_data,assets/isolate_snapshot_instr,assets/vm_snapshot_data,assets/vm_snapshot_instr,而从1.7.8版本开始产物变成了libapp.so,同时支持了32位和64位架构同时打包。所以现在讨论动态化Dart代码产物实际上也就是讨论如何让Flutter应用加载动态下发的libapp.so包。
先来了解下Dart代码产物libapp.so是如何设置给底层使用的。查看FlutterActivty的onCreate方法:

public class FlutterActivity extends Activity implements Host, LifecycleOwner {    protected void onCreate(@Nullable Bundle savedInstanceState) {        ......        this.delegate = new FlutterActivityAndFragmentDelegate(this);        this.delegate.onAttach(this);        this.delegate.onActivityCreated(savedInstanceState);        ......    }}

在FlutterActivityAndFragmentDelegate的onAttach方法中会对FlutterEngine做检查,未做特殊设置的情况下会走到构建FlutterEngine的流程,FlutterEngine的构造方法中会调用到flutterLoader.ensureInitializationComplete(context, dartVmArgs); :
/io/flutter/embedding/engine/loader/FlutterLoader.java

public void ensureInitializationComplete(@NonNull Context applicationContext, @Nullable String[] args) {            ......            List<String> shellArgs = new ArrayList<>();            ......            if (args != null) {                Collections.addAll(shellArgs, args);            }            String kernelPath = null;            if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) {              ......            } else {                shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + aotSharedLibraryName);                shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + applicationInfo.nativeLibraryDir + File.separator + aotSharedLibraryName);            }            ......            FlutterJNI.nativeInit(applicationContext, shellArgs.toArray(new String[0]), kernelPath, appStoragePath, engineCachesPath);          ......    }

重点看一下这个AOT_SHARED_LIBRARY_NAME参数,它取值为aotSharedLibraryName,默认值为libapp.so,也就是在这里将Dart代码产物libapp.so设置给了底层,注意这里AOT_SHARED_LIBRARY_NAME参数除了简单设置libapp.so值外,也支持设置so的完整路径:

applicationInfo.nativeLibraryDir + File.separator + aotSharedLibraryName

在这里得到启发,如果把这个路径替换成动态下发下载到本地的so路径是不是就可以实现Dart代码产物动态化呢?后面我们经过探索和实践也证明了这个想法的可行性。
那现在的问题是如何去设置这个AOT_SHARED_LIBRARY_NAME参数呢,可以看到这个参数是拼接传递给了List shellArgs这个局部变量,然后传递给了FlutterJNI去做初始化,外界并没有直接途径去更改shellArgs。但我们发现在设置AOT_SHARED_LIBRARY_NAME参数前,做了额外一些参数的插入:

if (args != null) {   Collections.addAll(shellArgs, args);}

很自然的想到是不是可以利用这个args参数来重新设置AOT_SHARED_LIBRARY_NAME? 有了这个思路后,接下来有两个问题:

  1. 在args中设置AOT_SHARED_LIBRARY_NAME参数会不会被之后的shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + aotSharedLibraryName)覆盖掉?

  2. 如何在args中设置AOT_SHARED_LIBRARY_NAME参数?

验证加载顺序

先来看第一个问题,查看FlutterJNI.nativeInit对应的底层源码,根据flutter_main.cc中的Register方法,可以看到FlutterJNI.nativeInit方法对应到了Init方法:
/shell/platform/android/flutter_main.cc

bool FlutterMain::Register(JNIEnv* env) {  static const JNINativeMethod methods[] = {      {          .name = "nativeInit",          .signature = "(Landroid/content/Context;[Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",          .fnPtr = reinterpret_cast<void*>(&Init),      },      ......  };  jclass clazz = env->FindClass("io/flutter/embedding/engine/FlutterJNI"); ......}

Init方法中将外界传入的参数(对应到Java层的shellArgs)进行解析,构建Settings对象,并将我们Java层设置的AOT_SHARED_LIBRARY_NAME参数传递给setting.application_library_path,然后settings.application_library_path会被作为native_library_path参数传递给SearchMapping方法:
/runtime/dart_snapshot.cc

static std::shared_ptr<const fml::Mapping> SearchMapping(    MappingCallback embedder_mapping_callback,    const std::string& file_path,    const std::vector<std::string>& native_library_path,    const char* native_library_symbol_name,    bool is_executable) {   ......  for (const std::string& path : native_library_path) {    auto native_library = fml::NativeLibrary::Create(path.c_str());    auto symbol_mapping = std::make_unique<const fml::SymbolMapping>(        native_library, native_library_symbol_name);    if (symbol_mapping->GetMapping() != nullptr) {      return symbol_mapping;    }  }  ......  return nullptr;}

看一下这里对native_library_path的查找过程,遍历列表找到对应值就返回。这就回答了我们之前提到的第一个问题,在FlutterLoader的ensureInitializationComplete方法参数args中设置AOT_SHARED_LIBRARY_NAME参数是会优先被查找的,而不会被之后的shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + aotSharedLibraryName);覆盖掉。

如何设置libapp.so的路径

那接下来看第二个问题,如何在ensureInitializationComplete参数args中加入AOT_SHARED_LIBRARY_NAME来指定动态化下发的Dart代码产物即libapp.so路径?
FlutterActivity.java中有一个getFlutterShellArgs方法:
io/flutter/embedding/android/FlutterActivity.java

public class FlutterActivity extends Activity implements FlutterActivityAndFragmentDelegate.Host,LifecycleOwner{  ......  @Override  public FlutterShellArgs getFlutterShellArgs() {    return FlutterShellArgs.fromIntent(getIntent());  }}

跟踪发现这里的FlutterShellArgs会作为参数传递给FlutterEngine构造方法,并在构造方法中传递给FlutterLoader的ensureInitializationComplete方法。82ae3013ec7cc7871643ff501202003b.png所以我们只需要重写FlutterActivity的getFlutterShellArgs方法,包装上AOT_SHARED_LIBRARY_NAME参数,即可指定libapp.so的路径。

Assets资源文件

Flutter应用中除了Dart代码资源,另外一部分经常需要更新的是Dart中用到的本地图片资源。简单回顾一下Flutter中如何加载本地图片:

  1. 创建存放图片资源的文件目录比如images,根据需要在此目录下创建Nx文件夹,比如2.0x,3.0x等,并放入需要的图片如icon.png

  2. 在pubspec.yaml中配置资源目录,现在已经支持配置整个文件夹

    flutter:  assets:    - images/
  3. 代码中使用Image.asset("images/icon.png")来加载图片资源

要找到动态化图片资源的方法,先了解下通过Image.asset是如何加载出本地图片资源的,跟踪代码

image.asset->AssetImage->AssetBundleImageProvider->AssetBundleImageKey->PlatformAssetBundle

看一下load方法:
/services/asset_bundle.dart

class PlatformAssetBundle extends CachingAssetBundle {  @override  Futureload(String key) async {    final Uint8List encoded = utf8.encoder.convert(Uri(path: Uri.encodeFull(key)).path);    final ByteData asset =        await defaultBinaryMessenger.send('flutter/assets', encoded.buffer.asByteData());    if (asset == null)      throw FlutterError('Unable to load asset: $key');    return asset;  }}

可以看出Flutter加载本地图片资源其实最终是通过BinaryMessager通知给了Platform,对应的channel名字为flutter/assets,message通常为1)AssetManifest.json 2)资源名,如 image/3.0x/icon.png (注意这里是处理过的资源名,而非代码中的原始参数值image/icon.png)。
AssetManifest.json是自动生成的文件,以json结构的形式描述了asset资源信息,在apk包中路径为 assets/flutter_assets/AssetManifest.json。对于message为AssetManifest.json的情况,flutter/assets这个channel和Platform通信是为了获取AssetManifest.json中资源项,可以断点看一下这种情况下获取到的map结构内容:edeaf6994d058e0484ed2a3da63fbde8.png获取到AssetManifest.json中的所有资源结构后,通过painting/image_resolution.dart中的_chooseVariant方法将原始的资源名image/icon.png根据当前设备的像素密度匹配出合适的资源名chosenName: image/3.0/icon.png:

final String chosenName = _chooseVariant(keyName, configuration,manifest == null ? null : manifest[keyName], );

然后根据这个chosenName再次调用PlatformAssetBundle的load方法,利用flutter/asset这个channel通知给Platform去取对应资源。那Platform对应的channel收到请求后是怎么取资源的呢?先看一下消息接收:
/shell/common/engine.cc

static constexpr char kAssetChannel[] = "flutter/assets";void Engine::HandlePlatformMessage(fml::RefPtr message) {  if (message->channel() == kAssetChannel) {    HandleAssetPlatformMessage(std::move(message));  } else {    delegate_.OnEngineHandlePlatformMessage(std::move(message));  }}

收到flutter/assets这个channel发来的消息后,调用HandleAssetPlatformMessage -> asset_manager_的GetAsMapping(asset_name) 方法:
engine/src/flutter/shell/platform/android/apk_asset_provider.cc

std::unique_ptr<:mapping> APKAssetProvider::GetAsMapping(    const std::string& asset_name) const {  std::stringstream ss;  ss << directory_.c_str() << "/" << asset_name;  AAsset* asset =      AAssetManager_open(assetManager_, ss.str().c_str(), AASSET_MODE_BUFFER);  if (!asset) {    return nullptr;  }  return std::make_unique(asset);}

重点注意这里的AAssetManager_open方法。在Android ndk api level 9之后,提供了一套称为AssetManager的api,这个api核心是通过JNI将Android端getAssets得到的AssetManager传递给c层,c层通过传过来的这个AssetManager对象可以操作Assets资源,这里涉及到的方法 

AAsset * AAssetManager_open( AAssetManager *mgr, const char *filename, int mode)

作用是读取对应名称的asset资源。
至此,可以明确Image.asset('images/icon.png')方法底层实际上是通过Platform的AssetManager对象去获取对应的图片资源。

结合之前Android项目中资源动态化经验,反射使用AssetManager的addAssetPath方法即可设置指定的asset资源目录,核心代码如下:

AssetManager assetManager = context.getAssets();Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);addAssetPath.setAccessible(true);addAssetPath.invoke(assetManager, downloadAssetPath);

在实际的Asset动态化实践中,踩过几个坑:

  1. context.getAssets()获取AssetManager时,这个context需要使用FlutterActivity而不要使用Application。因为传递给c底层的是FlutterActivity对应的AssetManager。不能保证各机器上这两个context是同一个对象。

  2. addAssetPath方法传递参数直接设置压缩包路径就行,如果设置成解压路径,在华为荣耀9青春版等机器上资源会加载失败。

  3. 下发资源包中带上AssetManifest.json,如之前分析,Flutter会根据这个文件来匹配最合适当前像素密度的图片尺寸。

  4. 下发资源包中带上AndroidManifest.xml,因为addAssetPath方法底层会去校验是否有AndroidManifest.xml文件,如果没有则会添加资源路径失败,导致动态化下发的图片加载失败(motorola XT1581 Android5.1.1)。

实践

根据以上分析,在Android上的Flutter项目动态化产物组成如下图(目前只讨论Dart代码和资源的动态化,如需要Java代码相关,可参考目前成熟的Android热修复框架):f9bfae85a7f5adba5d009fdc6e13111f.png

来试验一下实际效果吧~
动态化前,假设apk包为app-old.apk:8fb0ff69b610e9efc11ff8b508e6adae.png将底部tab的icon换成新添加的images/icon.png,修改tab文案,然后打包成app-new.apk。脚本比对app-new.apk和app-old.apk中的资源差异,提取有变化的资源项,结合app-new.apk中的AssetManifest.json,AndroidManifest.xml以及新的libapp.so,组成动态化产物包bundle.zip。对app-old.apk版本下发bundle.zip,解压校验成功后,加载新的libapp.so文件并添加新的asset路径,即可在无需进行app版本升级的情况下更新用户体验。动态化效果如下:1d7cacff591339d774fd3a392c29032d.png可以看到底部第一个tab的icon和文案已经被成功替换了。

总结

这个方案简单易处理,但弊端也很明显,动态化产物的包体积会比较大,有点浪费用户流量。所以,在这个方案基础上做进一步优化,可以将产物做差分以减小产物包体积。目前微信的Tinker方案已经很成熟了,而且也支持对so的差分。了解了Android上Flutter应用动态化原理后,再结合Tinker方案思路,就可以很方便的使用Tinker做Flutter的动态化了。接入Tinker后,下发包体积大小有很明显的减少。c091dd75ab008546bf2f9ea1abcbde00.png

关于Android上Flutter应用动态化的设计和实践就介绍到这里。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值