2
头图

introduction

The Flutter CLI tool supports packaging the Flutter Module into an Android AAR package for external dependencies, that is, Flutter AAR. There is no problem in integrating Flutter AAR in an Android project that does not use the Flutter technology stack, but if the target project itself already uses the Flutter framework, accessing Flutter AAR on this basis will fail. We call it Flutter. Instance problem. This article mainly introduces a solution to the Flutter multi-instance problem under the Android platform.

background

The business of an enterprise is often complex and diverse. If it is a ToC business, we usually need to develop a well-experienced application APP; if it is a ToB business, we often need to provide an SDK that is easy to access and use. In the ToC business, the cross-platform, efficient development and high-performance features provided by the Flutter framework make mobile application development easier and more efficient; in the ToB business, can SDK development enjoy these bonuses provided by the Flutter framework? ? This is especially important for service and capability providers like our NetEase Yunxin. NetEase Yunxin is a convergent communication cloud service expert built by NetEase’s 21 years of IM and audio and video technology. It is a stable and easy-to-use communication and video PaaS platform. Most of its services are provided in the form of capability SDK. If it can improve the productivity and efficiency of SDK The benefits of R&D efficiency are self-evident. Therefore, the answer to the above question is of course yes! Just like using Flutter to develop apps, we can also use Flutter for SDK development, so as to share consistent business logic implementations on Android / iOS and even more platforms, reducing manpower, improving production efficiency and research and development efficiency.

When using Flutter for SDK development, the product packaging methods mainly have the following two forms:

  • Flutter Package / Flutter Plugin : This packaging method needs to be published to Pub.dev or GitHub in the form of Dart source code. Third-party developers essentially rely on the form of source code when accessing, and the access party needs to build and import locally. Flutter development environment. This method has obvious flaws: first, the source code release will completely expose the internal implementation details of the SDK (the Flutter framework does not provide an obfuscation tool similar to Proguard), which is unacceptable for non-open source projects of enterprises; secondly, It requires the access party to use the Flutter technology stack in disguise. For access parties who are not currently developing using Flutter in the target project, the threshold is high, and the access experience is not very friendly.
  • Android AAR : AAR is the official dependency form of Android applications, and there is no obvious shortcoming. With the CLI tool provided by the Flutter framework, the Flutter Module can be easily packaged into AAR and released without worrying about leaking the business source code or losing the access experience. Because the packaging tool compiles the business code of the Flutter layer into an AOT shared library, and the Java business code of the platform layer can turn on confusion and avoid decompilation (for simplicity, the Flutter AAR will be used later to name the Android AAR package packaged by the Flutter Module. ).

In summary, for a commercial SDK project of an enterprise, if you choose to use the Flutter technology stack for development, then it is wise to use the Flutter AAR format to release it. But in fact, this will introduce new problems. In the previous article Flutter Hybrid Development Foundation , we introduced the package structure of a Flutter APP, which includes engine library libflutter.so , business library libapp.so , and flutter_assets . In the same way, the AAR packaged by a Flutter Module will also contain similar structure and product files. So in a Flutter APP, what kind of posture should be used to connect to Flutter AAR? It is foreseeable that there must be conflicts between them. File conflicts are already obvious. Classes, resources, and even Flutter Engine may also conflict. This kind of conventional Flutter AAR package obviously cannot be integrated into the Flutter APP project. If there is a question, there will be an answer. Next, we will analyze and explore the solution to the problem together.

Flutter APP integration Flutter AAR problem analysis

As mentioned above, the Flutter APP cannot integrate the conventionally packaged Flutter AAR, because there are a series of conflicts, but what kind of errors will occur, we still need to actually integrate them before we can know. Friends who are interested in this link can try it by themselves and will not repeat them. The following conclusions will be given directly to illustrate the problems of the coexistence of the two:

  • The build fails, in fact, the compilation fails because of file and class conflicts. The main conflicts are:

    • Flutter version dependency conflict: the Flutter APP host project is inconsistent with the Flutter version used by Flutter AAR, including Flutter Embedding Jar and Flutter SO Jar. The former contains platform layer Java code, and the latter contains libflutter.so engine library files. Through Gradle we can resolve this dependency version conflict, such as forcing the use of one of the versions, but doing so is very likely to cause runtime errors.
    • Flutter Plugin platform code/resource conflict: Flutter APP and Flutter AAR refer to the same Plugin but the versions are inconsistent. The plug-in will contain platform-level code, and inconsistent versions may also cause compilation failure or runtime errors.
    • GeneratedPluginRegistrant.java file conflict: This file is the plug-in automatic registration class generated by the Flutter tool, and is used to automatically load the required plug-ins when the Flutter Engine starts. Both Flutter APP and Flutter AAR have corresponding class files, which are responsible for loading the plug-ins they depend on. Both are indispensable.
    • Libapp.so conflict: This is a dynamic library generated by Dart code through AOT. Both Flutter APP and Flutter AAR will generate corresponding so libraries. We can't just use one of them, because the AOT code they contain is different The source code is compiled.
  • Runtime error

    • The same Flutter Engine does not support loading multiple AOT libraries: Flutter Engine dynamically links libapp.so this AOT library when it is initialized, parses the data segment in it, and executes the machine instructions in the code segment. But in our scenario, the runtime actually contains two AOT libraries, both of which need to be loaded into the Flutter Engine. Using the same Engine cannot meet the demand, because in the implementation of Flutter, one Engine can only Corresponds to an AOT library.
    • Image resources and font libraries cannot be displayed normally: Such resources will be packaged into flutter_assets, and the corresponding Manifest resource description manifest file will be generated. However, the resource manifest file generated by the Flutter APP will overwrite the resource manifest file in the Flutter AAR, which causes the Flutter Engine to be unable to query the corresponding resource from the manifest file when loading the resource, so the loading fails.

The above are the problems we encountered when accessing Flutter AAR in the Flutter APP. In response to these problems, the first thing we think of is, does the Flutter Team or the open source community already have solutions for such problems? But after investigation, it is found that there is currently no. The Flutter framework supports multiple Engines, including the newly supported Engine Group of Flutter 2.0, which only supports loading and running the code under the same AOT library, which obviously cannot meet our needs. We also provided the official with the corresponding Issue ( https://github.com/flutter/flutter/issues/64542) for discussion, but we have not yet obtained a satisfactory solution, so we had to embark on our own exploration and solution The road to self-reliance of the program.

Solution exploration

Through the above analysis, we have understood the specific error and the cause of the error during the access process. Before really exploring the solution, some principles that the target solution should meet should also be established:

  • First of all, the plan should work towards minimal or even no changes to the engine. Because the Flutter framework has been iteratively evolving, if we modify the logic of the engine, unless these changes can enter the main branch through PR, once the engine is updated, our solution will have to be re-adapted, and subsequent maintenance work will be heavy.
  • Secondly, the program should try not to rely on the host project for additional modification or support. First, Flutter APP access to Flutter AAR is as simple as ordinary Android APP access to Android AAR, no additional plug-ins or Gradle scripts should be introduced; secondly, the Flutter runtime environment of Flutter AAR and Flutter APP should be isolated as much as possible.

After clarifying the goal, let's take a look at where to start. Since engine changes need to be avoided as much as possible, it should be top-down, first cut from the application layer to see if a countermeasure can be found. This requires us to go deep into the source code and understand the initialization and operating mechanism of the Flutter framework from top to bottom. It will not be explained separately here, but will be explained on the analysis and solution of specific problems. Now let's go back and look at the series of problems we encountered initially, and try to use our knowledge of Android and Flutter frameworks to solve them.

Class conflict resolution

Class conflicts are because Flutter AAR and Flutter APP have their own Plugins dependencies and may depend on different versions of Flutter Embedding Jar. These dependent libraries contain platform code, which will cause duplicate classes during compilation and fail. How to solve this problem?
The simplest and most violent method is to rename (modify the class name or package name) the source code of all Plugins and Embedding Jars that Flutter AAR depends on. Although it can solve the problem, the workload is huge, the modification is wide, and it is inflexible. Once Plugin Or Flutter version update needs to be revised.

Is there a better way? The answer is custom ClassLoader . Specifically, when building Flutter AAR, after the source code is compiled into .class stage, all plug-ins and .class files corresponding to Flutter Embedding Jar are collected, packaged into a DEX file and placed in the assets of Flutter AAR. At runtime, you need to copy the DEX file under assets to the private data directory of the application, and then use DexClassLoader to dynamically load the DEX. It should be noted here that the DEX file is the concept of the version number, which is bound to the version number of Flutter AAR, which means that every time this DEX is loaded, we first need to check whether the file version in the current private directory is the same as that of Flutter AAR. If the version is the same, just load it directly. If the version is the same, you need to delete the original DEX file and re-copy it before loading. The key code is as follows:

image.png

image.png

For DEX file loading, generally speaking, we only need to use the system class DexClassLoader, but here we need to inherit BaseDexClassLoader and rewrite the findClass method.

The default class loading is based on the parent delegation model. Generally, the parent loader is requested to load first. If the parent loader fails to load, the child loader has a chance to load. But here, the logic of our findClass needs to do the opposite. Classes that need to be loaded by Flutter AAR should be loaded from the DEX file using the child loader first, and can only be loaded by the parent loader after the loading fails. code show as below:

image.png

Library file conflict resolution

libflutter.so is the Flutter Engine dynamic library file, which will be loaded by the Flutter Embedder Jar at runtime. This library file conflicts, we cannot simply use the library file with the same name in the host, because the Engine version of the two may be inconsistent and does not violate the goal of Flutter version isolation at runtime.

Here easiest way to resolve conflict is rename . By reading the code, we found that Android saves all loaded dynamic libraries with the path of the so library as the key. Even the same so library can be loaded at the same time as long as the file paths are inconsistent. Therefore, the file conflict problem can be solved by renaming here, and it will not affect the loading of so.

The libapp.so conflict is similar, we also need to rename libapp.so in Flutter AAR. In addition, we also need to handle the loading process of these two so specially. Because Flutter hard-codes the name of the dynamic library when it is running, if you do not modify the loading process, you will find the library file generated by the Flutter APP instead of our Flutter AAR library file when you check the library.

The initialization of Flutter Engine is in the FlutterLoader class, where libflutter.so will be loaded and a series of parameters will be configured to initialize the Native Engine. What we need to do is to replace the loading logic of libflutter.so and load the renamed Engine library file instead. For libapp.so, it is not loaded at the Java layer, but is linked by the Native Engine through dlopen. By consulting the code of Engine, we found that --aot-shared-library-name option can set the target libapp.so path to be loaded. The key code is as follows:

image.png

Flutter resource conflict resolution

Flutter related resources are packaged and placed in the assets directory, and declared through the corresponding Manifest files, which are: FontManifest.json and files. These two files respectively list all font resources and path mapping relationships, image resources and path mapping relationships that Flutter depends on.

Flutter-Engine uses these two files to parse images and font resources at runtime. Although these two files are also included in Flutter AAR, they will be overwritten by files with the same name in the Flutter APP host, causing fonts or resources to fail to load. So, here are two simple solutions:

  • Support the merging of the corresponding resource list json file during compilation; this requires the development of Plugin plug-ins for the host to use, which is complicated to implement and unfriendly to access;
  • An independent resource package package is extracted from Flutter AAR for the Flutter APP to rely on. The resource package only contains all the pictures and font resources referenced by Flutter AAR (does not contain any business logic, so you can safely publish to the pub platform). The Flutter layer relies on this Package, so that the Flutter tool will merge all resources and generate a complete resource manifest file when the host is built.

So far, we have solved the coexistence problem of Flutter AAR and Flutter APP. Of course, when the whole solution is implemented, some other problems will be encountered, such as: when the generated DEX file needs to access other classes in the host, how to ensure that the DEX accesses the classes and methods in the main ClassLoader when confusion is enabled No problem; another example: What if Android components are included in the DEX of Flutter AAR? The four major components of Android need to be loaded by the main ClassLoader of the application. If these classes are not included in the main DEX, the startup must fail; and so on, I won’t list them all here.

Summarize

The following figure shows the architecture diagram of Flutter multi-instance runtime. Similar to multiple Flutter Engines, the multiple Flutter instances implemented by the above solution are also achieved by creating multiple Native AndroidShellHolders. The difference is that different ShellHolders under multiple Engines are bound to the same libapp.so, and under multiple instances are bound to different libapp.so, so this solution can isolate the Flutter APP and the Flutter runtime of Flutter AAR at runtime environment.

image.png

The main advantages of this program are:

  • No Engine customization, high maintainability
  • Flutter APP and Flutter AAR's Flutter version and runtime environment are independent of each other

There are gains and losses. Relatively, in other respects, the program has some shortcomings:

  • A separate Flutter Engine library file is used, so the package size will increase
  • Two different Flutter Engines will be loaded, and the memory will increase

In summary, the use of Flutter technology in SDK development can also give play to the advantages of Flutter in APP development, provided that we can solve the problem of Flutter multi-instance. This article mainly explains an implementation idea of Android Flutter multi-instance, I hope it can be helpful to everyone.

About the Author

Li Chengda, a senior mobile development engineer at NetEase Yunxin, is keen on researching cross-platform development technology and engineering efficiency improvement. Currently, he is mainly responsible for the related research and development of video conference component SDK.

For more technical dry goods, please pay attention to [Netease Smart Enterprise Technology+] WeChat public account


网易数智
619 声望140 粉丝

欢迎关注网易云信 GitHub: