美文网首页
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