美文网首页
Flutter在Android端的热更新方案

Flutter在Android端的热更新方案

作者: Jagtu | 来源:发表于2022-09-05 17:53 被阅读0次

Flutter打包成apk的产物

本文使用的是Android原生项目+Flutter Module 混合开发的模式,详见:将 Flutter 集成到现有应用

我们将项目打包成apk后,解压得到如下

image.png

其中,我们通过分析Flutter的源代码可知道:
libfutter.so:运行Flutter依赖so文件
libapp.so: 这里就是dart代码编译后的产物
flutter_asserts: 这里存放的项目中,Flutter模块用到包括图片、字体等资源

所以,如果这些产物能够动态替换,应该就能实现Android端热更新的功能。

Flutter在Android的运行分析

Flutter运行图

查看FlutterEngine的构建函数,如下

public FlutterEngine(
      @NonNull Context context,
      @Nullable FlutterLoader flutterLoader,
      @NonNull FlutterJNI flutterJNI,
      @NonNull PlatformViewsController platformViewsController,
      @Nullable String[] dartVmArgs,
      boolean automaticallyRegisterPlugins,
      boolean waitForRestorationData) {
    AssetManager assetManager;
    try {
      assetManager = context.createPackageContext(context.getPackageName(), 0).getAssets();
    } catch (NameNotFoundException e) {
      assetManager = context.getAssets();
    }

    FlutterInjector injector = FlutterInjector.instance();

    if (flutterJNI == null) {
      flutterJNI = injector.getFlutterJNIFactory().provideFlutterJNI();
    }
    this.flutterJNI = flutterJNI;

    this.dartExecutor = new DartExecutor(flutterJNI, assetManager);
    this.dartExecutor.onAttachedToJNI();

    DeferredComponentManager deferredComponentManager =
        FlutterInjector.instance().deferredComponentManager();

    accessibilityChannel = new AccessibilityChannel(dartExecutor, flutterJNI);
    deferredComponentChannel = new DeferredComponentChannel(dartExecutor);
    keyEventChannel = new KeyEventChannel(dartExecutor);
    lifecycleChannel = new LifecycleChannel(dartExecutor);
    localizationChannel = new LocalizationChannel(dartExecutor);
    mouseCursorChannel = new MouseCursorChannel(dartExecutor);
    navigationChannel = new NavigationChannel(dartExecutor);
    platformChannel = new PlatformChannel(dartExecutor);
    restorationChannel = new RestorationChannel(dartExecutor, waitForRestorationData);
    settingsChannel = new SettingsChannel(dartExecutor);
    systemChannel = new SystemChannel(dartExecutor);
    textInputChannel = new TextInputChannel(dartExecutor);

    if (deferredComponentManager != null) {
      deferredComponentManager.setDeferredComponentChannel(deferredComponentChannel);
    }

    this.localizationPlugin = new LocalizationPlugin(context, localizationChannel);

    if (flutterLoader == null) {
      flutterLoader = injector.flutterLoader();
    }

    if (!flutterJNI.isAttached()) {
      flutterLoader.startInitialization(context.getApplicationContext());
      flutterLoader.ensureInitializationComplete(context, dartVmArgs);
    }

    flutterJNI.addEngineLifecycleListener(engineLifecycleListener);
    flutterJNI.setPlatformViewsController(platformViewsController);
    flutterJNI.setLocalizationPlugin(localizationPlugin);
    flutterJNI.setDeferredComponentManager(injector.deferredComponentManager());

    // It should typically be a fresh, unattached JNI. But on a spawned engine, the JNI instance
    // is already attached to a native shell. In that case, the Java FlutterEngine is created around
    // an existing shell.
    if (!flutterJNI.isAttached()) {
      attachToJni();
    }

    // TODO(mattcarroll): FlutterRenderer is temporally coupled to attach(). Remove that coupling if
    // possible.
    this.renderer = new FlutterRenderer(flutterJNI);

    this.platformViewsController = platformViewsController;
    this.platformViewsController.onAttachedToJNI();

    this.pluginRegistry =
        new FlutterEngineConnectionRegistry(context.getApplicationContext(), this, flutterLoader);

    // Only automatically register plugins if both constructor parameter and
    // loaded AndroidManifest config turn this feature on.
    if (automaticallyRegisterPlugins && flutterLoader.automaticallyRegisterPlugins()) {
      GeneratedPluginRegister.registerGeneratedPlugins(this);
    }
  }

我们发现有个关键方法:

      flutterLoader.startInitialization(context.getApplicationContext());
      flutterLoader.ensureInitializationComplete(context, dartVmArgs);

加载libfutter.so以及libapp.so就是在这里处理的,下面把相关的源码抠出来解释一下:

FlutterLoader.java
// 只截取关键代码 其他的代码省略...

//  声明的两个常量  看名字即可知道对应于哪个so文件
private static final String DEFAULT_AOT_SHARED_LIBRARY_NAME = "libapp.so";
private static final String DEFAULT_LIBRARY = "libflutter.so";

// 初始化libflutter.so的入口
public void startInitialization(@NonNull Context applicationContext, @NonNull Settings settings) {
    ...
    System.loadLibrary("flutter");
    ...
}

// 初始化libapp.so的入口
public void ensureInitializationComplete(@NonNull Context applicationContext, @Nullable String[] args) {
    ...
    try {
        String kernelPath = null;
        if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) {
            ...
        } else {
            // 这里的   aotSharedLibraryName = "libapp.so";
            shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + aotSharedLibraryName);
            // 这里的 applicationInfo.nativeLibraryDir + File.separator + aotSharedLibraryName
            // 指的就是我们的so路径下的/libapp.so
            shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + applicationInfo.nativeLibraryDir + File.separator + aotSharedLibraryName);
        }
        ...
        initialized = true;
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

我们再回过头来看FlutterEngine里的flutterLoader,发现他来着FlutterInjector类单例:

    FlutterInjector injector = FlutterInjector.instance();
    if (flutterLoader == null) {
      flutterLoader = injector.flutterLoader();
    }

动态替换so文件

通过以上分析,我们可以开始替换So文件里,本文通过实现FlutterLoader的子类,进而覆盖其ensureInitializationComplete,修改其加载so文件的路径,这样就完成so文件的替换了。
以下是参考代码

package com.ylzpay.androidtest.hotfix

import android.content.Context
import io.flutter.FlutterInjector
import io.flutter.Log
import io.flutter.embedding.engine.loader.FlutterApplicationInfo
import io.flutter.embedding.engine.loader.FlutterLoader
import java.io.File

class YHFlutterLoader : FlutterLoader() {

    companion object {
        ///完成对FlutterInjector单例的重置,使其属性flutterLoader指向我们的子类
        fun activation() {
            //这里直接使用默认构造函数,与FlutterInjector类中初始化flutterLoader效果是一样的(详见FlutterInjector的fillDefaults()方法)
            val flutterLoader: YHFlutterLoader = YHFlutterLoader()
            val flutterInjector = FlutterInjector.Builder().setFlutterLoader(flutterLoader).build()
            //重置FlutterInjector单例
            FlutterInjector.reset()
            FlutterInjector.setInstance(flutterInjector)
            Log.i("------", "已重置FlutterInjector单例")
        }
    }
    //返回准备好的热更新包的路径(本文方案是从服务端下载到zip文件并解压放置到这个路径)
    private fun getHotAppBundlePath(applicationContext: Context): String {
        return applicationContext.filesDir.absolutePath + File.separator + "hot/lib/libapp.so";
    }

    override fun ensureInitializationComplete(
        applicationContext: Context,
        args: Array<out String>?
    ) {
        super.ensureInitializationComplete(applicationContext, args)

        val soFile: File = File(getHotAppBundlePath(applicationContext))
        if (soFile.exists()) {
            try {
                //1.拿到flutterApplicationInfo字段
                val flutterApplicationInfoField = FlutterLoader::class.java.getDeclaredField("flutterApplicationInfo")
                flutterApplicationInfoField.isAccessible = true
                val flutterApplicationInfo = flutterApplicationInfoField[this] as FlutterApplicationInfo
                Log.i(
                    "========",
                    "--aot-shared-library-name=" + flutterApplicationInfo.nativeLibraryDir + flutterApplicationInfo.aotSharedLibraryName
                )

                //2.拿到aotSharedLibraryName修改路径
                val aotSharedLibraryNameField =
                    FlutterApplicationInfo::class.java.getDeclaredField("aotSharedLibraryName")
                aotSharedLibraryNameField.isAccessible = true
                aotSharedLibraryNameField[flutterApplicationInfo] = soFile.absolutePath

                Log.i(
                    "========",
                    "--aot-shared-library-name=" + flutterApplicationInfo.nativeLibraryDir + flutterApplicationInfo.aotSharedLibraryName
                )

                super.ensureInitializationComplete(applicationContext, args)

            } catch (e: Exception) {
                e.printStackTrace()
                e.message?.let { Log.e("----", it) }
            }
        } else {
            Log.i("----", "load fail. 补丁不存在")
        }
    }
}
调用

在继承FlutterActivity的子Activity的onCreate方法中,提前调用YHFlutterLoader.activation()
或者,Application中调用

class App : Application() {
    override fun onCreate() {
        super.onCreate()
        //激活YHFlutterLoader
        YHFlutterLoader.activation()
    }
}

到此第一步替换so文件完成。

动态替换资源

在flutter中我们会把图片资源放在一个images目录下并注册声明完后,通常的使用方式:

AssetImage("images/icon.png")

通过查看源码可以找到最终是走到AssetBundle类中去,最终是由它的子类比如PlatformAssetBundle进行加载,而这个AssetBundle我们可以自己指定是要系统默认的还是自己实现的,所以这里可以通过自定义AssetBundle从而实现加载我们下载目录下images中的相关图片资源。实现HotAssetBundle如下:

import 'dart:io';
import 'dart:typed_data';

import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';

class HotAssetBundle extends PlatformAssetBundle {

  HotAssetBundle() {
    /// 这里是自己下载成功的图片资源路径
    dataPath = "/data/data/com.ylzpay.androidtest/files/hot/flutter_assets";
    debugPrint("-------------- HsaAssetBundle资源存放地址 = $dataPath");
  }

  /// 路径拼接前缀 Android = /data/data/xxx.xxx.xxx/cache
  String dataPath = "";

  @override
  Future<ByteData> load(String key) async {
    final ByteData? asset;
    debugPrint("try load file : $dataPath/$key");
    File file = File("$dataPath/$key");
    if(file.existsSync()){
      debugPrint("load file success! ${file.path}");
      Uint8List bytes = await file.readAsBytes();
      asset = bytes.buffer.asByteData();
    }else{
      debugPrint("load file faile!");
      asset = await super.load(key);
    }
    return asset;
  }
}

最后一步就是把这个我们自定义的AssetBundle配置使用,替换默认的PlatformAssetBundle,具体使用如下:

runApp(
      Container(
        child: DefaultAssetBundle(
          bundle: HotAssetBundle(),
          child: MaterialApp(
              ...
          )) 
      )
  );

其他

工程目录下原有的so文件以及flutter_assert也可以移除掉,这样子能真减少apk大小,在自己的build.gradle进行配置:

        // 移除Flutter相关的so文件 采用动态下发
        exclude 'lib/xxxx/libapp.so'
        exclude 'lib/xxxx/libflutter.so'

        variant.mergeAssets.doLast {
            //删除assets文件夹下的flutter_assets 采用动态下发
            delete(fileTree(dir: variant.mergeAssets.outputDir, includes: ['flutter_assets', 'flutter_assets/**']))
        }

相关文章

网友评论

      本文标题:Flutter在Android端的热更新方案

      本文链接:https://www.haomeiwen.com/subject/qlfbnrtx.html