Deferred Components-realize the dynamic delivery of Dart code during Flutter runtime | JD Cloud technical team

guide

Deferred Components, an officially implemented solution for dynamically delivering Flutter code. This article mainly introduces the implementation details of the official solution, explores the use of Deferred Components in the domestic environment, and implements the minimum verification demo. After reading this article, you can realize the dynamic delivery of Dart file-level code.

I. Introduction

Deferred Components is a function introduced by Flutter2.2, which relies on the new support for Split AOT compilation in Dart2.13. Each Dart library and assets resource package that can be downloaded separately at runtime is called a delayed loading component, that is, Deferred Components. After the Flutter code is compiled, all business logic will be packaged in a libapp.so file. But if lazy loading is used, it can be split into multiple so files, and even a Dart file can be compiled into a single so file.

The benefits brought by this are obvious. You can put some infrequently used functions into a separate so file, and then download it when the user uses it, which can greatly reduce the size of the installation package and improve the download conversion rate of the application. In addition, because Flutter has the ability to deliver dynamically at runtime, this allows everyone to see another possibility of implementing Flutter hot fixes. So far, the official implementation must rely on Google Play. Although a custom solution that does not rely on Google Play is also provided for Chinese developers, the implementation details are not given, and there are no articles on custom implementation on the market. . This article will first briefly introduce the official implementation plan, explore its details, find ideas for custom implementation, and finally implement a minimal Demo for your reference.

2. Research on the official implementation plan

2.1 Basic steps

2.1.1. Introduce play core dependencies.

dependencies {
  implementation "com.google.android.play:core:1.8.0"
}

2.1.2. Modify the onCreate method and attachBaseContext method of the Application class.

@Override
protected void onCreate(){
 super.onCreate()
// 负责deferred components的下载与安装
 PlayStoreDeferredComponentManager deferredComponentManager = new
  PlayStoreDeferredComponentManager(this, null);
FlutterInjector.setInstance(new FlutterInjector.Builder()
    .setDeferredComponentManager(deferredComponentManager).build());
}


@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    // Emulates installation of future on demand modules using SplitCompat.
    SplitCompat.install(this);
}

2.1.3. Modify the pubspec.yaml file.

flutter:
    deferred-components:

2.1.4. Add two files, box.dart and some_widgets.dart, to the flutter project. DeferredBox is the control to be delayed in loading. In this example, box.dart is called a loading unit, namely loading_unit. Each loading_unit corresponds to a unique id, a deferred component can contain multiple loading units. Remember this concept, it will be used later.

// box.dart


import 'package:flutter/widgets.dart';


/// A simple blue 30x30 box.
class DeferredBox extends StatelessWidget {
  DeferredBox() {}


  @override
  Widget build(BuildContext context) {
    return Container(
      height: 30,
      width: 30,
      color: Colors.blue,
    );
  }
}

<!---->

import 'box.dart' deferred as box;


class SomeWidget extends StatefulWidget {
  @override
  _SomeWidgetState createState() => _SomeWidgetState();
}


class _SomeWidgetState extends State<SomeWidget> {
  Future<void> _libraryFuture;


  @override
  void initState() {
 //只有调用了loadLibrary方法,才会去真正下载并安装deferred components.
    _libraryFuture = box.loadLibrary();
    super.initState();
  }


  @override
  Widget build(BuildContext context) {
    return FutureBuilder<void>(
      future: _libraryFuture,
      builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
        if (snapshot.connectionState == ConnectionState.done) {
          if (snapshot.hasError) {
            return Text('Error: ${snapshot.error}');
          }
          return box.DeferredBox();
        }
        return CircularProgressIndicator();
      },
    );
  }
}

2.1.5. Then add a button to jump to the SomeWidget page in main.dart.

 Navigator.push(context, MaterialPageRoute(
      builder: (context) {
        return const SomeWidget();
      },
    ));

2.1.6. Run  the flutter build appbundle  command in the terminal. At this time, gen_snapshot will not immediately compile the app, but run a verification program first, the purpose is to verify whether the project conforms to the format of the dart code dynamically delivered, the first build will definitely not succeed, you just need to follow the compilation prompts Just go to modify it. When all modifications are completed, the final .aab type installation package will be obtained.

The above are the basic steps of the official implementation plan. For more details, please refer to the official document
https://docs.flutter.dev/perf/deferred-components

2.2 Local authentication

Before uploading the generated aab installation package to Google Play, it is best to verify it locally.

First, you need to download bundletool, and then run the following commands in order to package the aab installation on the phone for final verification.

java -jar bundletool.jar build-apks --bundle=<your_app_project_dir>/build/app/outputs/bundle/release/app-release.aab --output=<your_temp_dir>/app.apks --local-testing


java -jar bundletool.jar install-apks --apks=<your_temp_dir>/app.apks

2.3 Life cycle of loadLibrary() method call

Figure 1 Introduction to the official implementation plan

(Source: https://github.com/flutter/flutter/wiki/Deferred-Components)

From the official implementation plan, we can know that only after the loadLibrary method is called, the download and installation of deferred components will be actually executed. Now focus on the life cycle of this method.

After calling the loadLibrary method, dart will internally query the id of the loading unit and pass it down. When reaching the jni layer, jni is responsible for passing the name of the deferred component corresponding to the loading unit and the loading unit id together For
PlayStoreDynamicFeatureManager, this class is responsible for downloading the corresponding Deferred Components from the Google Play Store server and responsible for installing them. After the installation is complete, it will be notified layer by layer, and finally tell the dart layer to display the dynamically delivered controls when the next frame is rendered.

3. Custom implementation

3.1 Ideas

After sorting out the life cycle of the loadLibrary method call, you only need to implement a class to replace
the function of PlayStoreDynamicFeatureManager. In the official solution, the entity class responsible for completing the function of PlayStoreDynamicFeatureManager is io.flutter.embedding.engine.deferredcomponents.PlayStoreDeferredComponentManager, which inherits from DeferredComponentManager. Analysis of the source code shows that its two most important methods are installDeferredComponent and loadDartLibrary.

  • installDeferredComponent : This method is mainly responsible for the download and installation of the component. After the download and installation is complete, the loadLibrary method will be called. If it is an asset-only component, then the DeferredComponentChannel.completeInstallSuccess or DeferredComponentChannel.completeInstallError method also needs to be called.

<!---->

  • loadDartLibrary : It is mainly responsible for finding the location of the so file, and calling the FlutterJNI dlopen command to open the so file. You can directly pass in the location of the apk, and flutterJNI will directly load the so in the apk, avoiding the logic of decompressing the apk.

Then the basic idea is there, implement an entity class by yourself, inherit DeferredComponentManager, and implement these two methods.

3.2 Code implementation

This example is just a minimal demo implementation, the cpu architecture adopts arm64, and the asset-only type of component is not considered for the time being.

3.2.1. Add
the CustomDeferredComponentsManager class, which inherits from DeferredComponentManager.

3.2.2. Implement the installDeferredComponent method, put the so file in the external SdCard storage, and the code is responsible for copying it to the application's private storage to simulate the network download process. code show as below:

@Override
public void installDeferredComponent(int loadingUnitId, String componentName) {
    String resolvedComponentName = componentName != null ? componentName : loadingUnitIdToComponentNames.get(loadingUnitId);
    if (resolvedComponentName == null) {
         Log.e(TAG, "Deferred component name was null and could not be resolved from loading unit id.");
         return;
     }
     // Handle a loading unit that is included in the base module that does not need download.
     if (resolvedComponentName.equals("") && loadingUnitId > 0) {
     // No need to load assets as base assets are already loaded.
         loadDartLibrary(loadingUnitId, resolvedComponentName);
         return;
     }
     //耗时操作,模拟网络请求去下载android module
     new Thread(
         () -> {
//将so文件从外部存储移动到内部私有存储中
              boolean result = moveSoToPrivateDir();
              if (result) {
                 //模拟网络下载,添加2秒网络延迟
                 new Handler(Looper.getMainLooper()).postDelayed(
                                () -> {
                                    loadAssets(loadingUnitId, resolvedComponentName);
                                    loadDartLibrary(loadingUnitId, resolvedComponentName);
                                    if (channel != null) {
                                        channel.completeInstallSuccess(resolvedComponentName);
                                    }
                                }
                                , 2000);
                 } else {
                        new Handler(Looper.getMainLooper()).post(
                                () -> {
                                    Toast.makeText(context, "未在sd卡中找到so文件", Toast.LENGTH_LONG).show();


                                    if (channel != null) {
                                        channel.completeInstallError(resolvedComponentName, "未在sd卡中找到so文件");
                                    }


                                    if (flutterJNI != null) {
                                        flutterJNI.deferredComponentInstallFailure(loadingUnitId, "未在sd卡中找到so文件", true);
                                    }
                                }
                        );
                  }
              }
        ).start();
    }

3.2.3. To implement the loadDartLibrary method, you can directly copy
this method in the PlayStoreDeferredComponentManager class. The comment has been added. Its main function is to find the so file in the internal private storage, and call the FlutterJNI dlopen command to open the so file.

  @Override
    public void loadDartLibrary(int loadingUnitId, String componentName) {
        if (!verifyJNI()) {
            return;
        }
        // Loading unit must be specified and valid to load a dart library.
        //asset-only的component的unit id为-1,不需要加载so文件
        if (loadingUnitId < 0) {
            return;
        }


        //拿到so的文件名字
        String aotSharedLibraryName = loadingUnitIdToSharedLibraryNames.get(loadingUnitId);
        if (aotSharedLibraryName == null) {
            // If the filename is not specified, we use dart's loading unit naming convention.
            aotSharedLibraryName = flutterApplicationInfo.aotSharedLibraryName + "-" + loadingUnitId + ".part.so";
        }


        //拿到支持的abi格式--arm64_v8a
        // Possible values: armeabi, armeabi-v7a, arm64-v8a, x86, x86_64, mips, mips64
        String abi;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            abi = Build.SUPPORTED_ABIS[0];
        } else {
            abi = Build.CPU_ABI;
        }
        String pathAbi = abi.replace("-", "_"); // abis are represented with underscores in paths.


        // TODO(garyq): Optimize this apk/file discovery process to use less i/o and be more
        // performant and robust.


        // Search directly in APKs first
        List<String> apkPaths = new ArrayList<>();
        // If not found in APKs, we check in extracted native libs for the lib directly.
        List<String> soPaths = new ArrayList<>();


        Queue<File> searchFiles = new LinkedList<>();
        // Downloaded modules are stored here--下载的 modules 存储位置
        searchFiles.add(context.getFilesDir());
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            //第一次通过appbundle形式安装的split apks位置
            // The initial installed apks are provided by `sourceDirs` in ApplicationInfo.
            // The jniLibs we want are in the splits not the baseDir. These
            // APKs are only searched as a fallback, as base libs generally do not need
            // to be fully path referenced.
            for (String path : context.getApplicationInfo().splitSourceDirs) {
                searchFiles.add(new File(path));
            }
        }


        //查找apk和so文件
        while (!searchFiles.isEmpty()) {
            File file = searchFiles.remove();
            if (file != null && file.isDirectory() && file.listFiles() != null) {
                for (File f : file.listFiles()) {
                    searchFiles.add(f);
                }
                continue;
            }
            String name = file.getName();
            // Special case for "split_config" since android base module non-master apks are
            // initially installed with the "split_config" prefix/name.
            if (name.endsWith(".apk")
                    && (name.startsWith(componentName) || name.startsWith("split_config"))
                    && name.contains(pathAbi)) {
                apkPaths.add(file.getAbsolutePath());
                continue;
            }
            if (name.equals(aotSharedLibraryName)) {
                soPaths.add(file.getAbsolutePath());
            }
        }


        List<String> searchPaths = new ArrayList<>();


        // Add the bare filename as the first search path. In some devices, the so
        // file can be dlopen-ed with just the file name.
        searchPaths.add(aotSharedLibraryName);


        for (String path : apkPaths) {
            searchPaths.add(path + "!lib/" + abi + "/" + aotSharedLibraryName);
        }
        for (String path : soPaths) {
            searchPaths.add(path);
        }
//打开so文件
        flutterJNI.loadDartDeferredLibrary(loadingUnitId, searchPaths.toArray(new String[searchPaths.size()]));
    }

3.2.4. Modify the code of Application and delete
the dependency of com.google.android.play:core.

override fun onCreate() {
        super.onCreate()
        val deferredComponentManager = CustomDeferredComponentsManager(this, null)
        val injector = FlutterInjector.Builder().setDeferredComponentManager(deferredComponentManager).build()
        FlutterInjector.setInstance(injector)

So far, the core code has been fully implemented. For other detailed codes, please refer to
https://coding.jd.com/jd_logistic/deferred_component_demo/. If you need to add permissions, you can contact shenmingliang1.

3.3 Local Authentication

  • Run the flutter build appbundle --release --target-platform android-arm64 command to generate the app-release.aab file.
  • .Run the following command to parse app-release.aab into locally installable apks files: java -jar bundletool.jar build-apks --bundle=app-release.aab --output=app.apks --local-testing
  • Unzip the app.apks file generated in the previous step, find splits/scoreComponent-arm64_v8a_2.apk in the pressurized app folder, continue to unzip this apk file, and find lib/arm64-v8a/ in the generated scoreComponent-arm64_v8a_2 folder libapp.so-2.part.so file.
  • Execute the java -jar bundletool.jar install-apks --apks=app.apks command to install app.apks, open the installed app at this time, click the button in the lower right corner of the home page to jump to the DeferredPage page, and the page will not be loaded successfully at this time , and you will be prompted "the so file was not found in the sd card".
  • Push the lipase.so-2.part.so found in step 3 to the specified folder, the command is as follows: adb push libapp.so-2.part.so /storage/emulated/0/Android/data/com.example. deferred_official_demo/files. Restart the app process and reopen the DeferredPage interface.

4. Summary

For the domestic use of the official implementation plan, the biggest limitation is undoubtedly Google Play. This article implements a minimal demo that breaks away from the restrictions of Google Play, and verifies the feasibility of using deferred components in China.

reference:

  1. https://docs.flutter.dev/perf/deferred-components
  2. https://github.com/flutter/flutter/wiki/Deferred-Components

Author: JD Logistics Shen Mingliang

Content source: JD Cloud developer community

{{o.name}}
{{m.name}}

おすすめ

転載: my.oschina.net/u/4090830/blog/8881164