Exploration of Flutter Hybrid Architecture Solution

Thanks to Flutter's excellent cross-platform performance, hybrid development can be seen everywhere in today's apps. For example, the official version of Skyline, a new rendering engine for small programs announced by WeChat recently , also uses Flutter for underlying rendering, claiming that the rendering speed has increased by 50%.

It is not a simple matter to introduce Flutter into the existing native App for development. It is necessary to solve various problems caused by the mixed mode, such as routing stack management, package volume and memory burst, etc.; there is also a special case, An app originally developed by Flutter may also be mixed in later and developed with native View.

My team is currently in this situation. Flutter is not yet perfect in terms of performance, the overall page is not smooth enough, and there will be serious heating behavior in some complex page scenarios. Although the Flutter team has released a new The rendering engine impeller , which performs well on iOS, has a qualitative improvement in fluency, but still cannot completely solve some performance problems and the impeller under Android has not yet been developed.

In order to deal with the current dilemma and the unknown problems that may arise in the future, we expect to expand more possibilities through the mixed mode.

routing management

The most difficult thing to deal with under mixed development is the routing problem. We know that native and Flutter have their own routing management systems. How to manage and interact with each other in the case of interspersed native pages and Flutter pages is a major difficulty. Currently popular , the representative framework is flutter_boost单引擎方案 produced by Xianyu team ; the multi-engine solution FlutterEngineGroup officially represented by flutter .

Single engine solution flutter_boost

flutter_boost achieves the purpose of minimum memory by multiplexing the Engine, the following picture is its design architecture diagram ( picture from the official )

v2-cec9a709ee743c3abd1a2b7949b24a7a_720w.png

In terms of engine processing, flutter_boost defines a general CacheId: "flutter_boost_default_engine". When the native needs to jump to the Flutter page, by FlutterEngineCache.getInstance().get(ENGINE_ID);obtaining the same Engine, no matter how many Flutter pages A, B, and C in the figure are opened , there will be no additional Enginememory consumption.

public class FlutterBoost { 
    public static final String ENGINE_ID = "flutter_boost_default_engine"; 
    ... 
}

 In addition, both ends are registered with the navigation interface, which is Channelnotified through to request routing changes, page return, and page life cycle processing. In this mode, Channelthe interface processing at this layer is the focus.

Multi-engine solution FlutterEngineGroup

In order to deal with the problem of memory explosion, the official optimized the multi-engine scenario, which came into FlutterEngineGroupbeing. FlutterEngineGroupThe following Engines share some common resources, such as GPU context, thread snapshot, etc. When generating additional Engines, it is claimed that the memory usage is reduced to 180k. This level can basically be regarded as normal loss.

Take the B and C pages in the above figure as an example. Both are Flutter pages. Under the processing of FlutterEngineGroup, because the Engines they are in are not the same, this will produce complete isolation behavior, that is, the B and C pages use different The stacks are in different Isolates, and the two cannot interact directly.

The advantage of multi-engine is that it can erase the internal routes such as F, E, C, D, and A shown in the above figure. Every time a new Flutter page is added, all of them are called back to the native, so that the native generates a new Engine to host the page , so that the routing management is all handled by the native, and one Engine only corresponds to one Flutter page.

But it will also bring some additional processing. As mentioned above, there is no direct interaction between Flutter pages under different Engines. If it involves a scene that requires notification and interaction, it has to be forwarded through native.

For FlutterEngineGroupmore information, you can refer to the official instructions .

performance comparison

It is officially claimed that the new Engine created by FlutterEngineGroup will only occupy 180k of memory, so is it really as it says? Let's do a memory usage test for the above two solutions

flutter_boost

Test model: OPPO CPH2269

Test code: github.com/alibaba/flu…

Memory dump command: adb shell dumpsys meminfo com.idlefish.flutterboost.example

condition PSS RSS biggest change
1 Native 88667 165971
+26105 +28313 +27M
1 Native + 1 Flutter 114772 194284
-282 +1721 +1M
2 Native + 2 Flutter 114490 196005
+5774 +5992 +6M
5 Native + 5 Flutter 120264 201997
+13414 +14119 +13m
10 Native + 10 Flutter 133678 216116

When loading the Flutter page for the first time, increase the memory by about 27M, and then open an additional page, and the memory increase will show a steeper and steeper trend from 1M -> 2M -> 2.6 M (the value is only for reference, because there are Native pages, only Look at the trend change)

FlutterEngineGroup

Test model: OPPO CPH2269

Test code: github.com/flutter/sam…

Memory dump command: adb shell dumpsys meminfo dev.flutter.multipleflutters

condition PSS RSS biggest change
1 Native 45962 140817
+29822 +31675 +31M
1 Native + 1 Flutter 75784 172492
-610 +2063 +2M
2 Native + 2 Flutter 75174 174555
+7451 +7027 +3.7M
5 Native + 5 Flutter 82625 181582
+8558 +7442 +8M
10 Native + 10 Flutter 91183 189024

When loading the Flutter page for the first time, increase the memory by about 31M, and then open an additional page to increase the memory, showing a steeper trend from 1M -> 1.2M -> 1.6 M (the value is only for reference, because there are Native pages, Just look at the trend change)

in conclusion

The two tests use different demo codes, which cannot be judged by numerical values. However, through the numerical performance, we can basically confirm that neither of the two solutions will cause abnormal memory surges, which are completely within the acceptable range.

PlatformView

PlatformView can also implement hybrid UI, and WebView in Flutter is introduced through PlatformView.

PlatformView allows us to insert native View into the Flutter interface, wrap a layer of PlatformView on the outermost layer of a page, and manage routing by Flutter. In this way, no additional Engine is generated, and it is the simplest mixing method.

But it also has disadvantages. It is not suitable for the scene where the main Native is mixed with Flutter, and now most of the scenes are mainly for the main Native mixed with Flutter. In addition, due to its underlying implementation, PlatformView will have compatibility issues. In some models, keyboard problems, flickering or other performance overhead may occur. For details, see this introduction

data sharing

Native and Flutter use different development languages ​​for development, so the data structure objects and memory objects defined on one side cannot be perceived by the other party, and other means must be used for data synchronization and processing.

MethodChannel

Flutter developers must be familiar with MethodChannel. It is inevitable to interact with the native during development. MethodChannel is a two-way design, which allows us to call native methods in Flutter and also allows us to call Flutter methods in native. If you don’t know much about Channel, you can read the official document . As mentioned in the document, the data needs to be encoded and decoded during the transmission of this channel. The corresponding relationship kotlinis taken as an example (see the document for the complete mapping):

Dart                         | Kotlin      |
| -------------------------- | ----------- |
| null                       | null        |
| bool                       | Boolean     |
| int                        | Int         |
| int, if 32 bits not enough | Long        |
| double                     | Double      |
| String                     | String      |
| Uint8List                  | ByteArray   |
| Int32List                  | IntArray    |
| Int64List                  | LongArray   |
| Float32List                | FloatArray  |
| Float64List                | DoubleArray |
| List                       | List        |
| Map                        | HashMap     |

local storage

This method is easier to understand. Local storage is regarded as a transfer station. Data operations are stored locally in Flutter, and the local database can be queried at a certain time (such as onResume) when returning to the original page, and vice versa.

question

Whether it is MethodChannelor is 本地存储, there will be a problem: the data structure of the object is independent, and both sides need to be defined repeatedly. For example, I have a Student object in Flutter, and I also need to define a Student with the same structure on the Android side, so as to facilitate operation. Now I will transfer it to, Student studentdecode Unit8Listit into an operable AndroidChannel , and then translate it into an object.KotlinByteArrayByteArrayAndroidStudent

class Student { 
    String name; 
    int age; 
    Student(this.name, this.age); 
}

The best solution to this problem is to use DSLa class of frameworks, such as Google's ProtoBuf , to compile the same object configuration file into different language environments, which can save this part of double-ended repeated definition behavior.

image cache

In terms of memory, if the same image is loaded on both sides, both native and Flutter will generate a cache. Under Flutter, it will be cached by default ImageCache. Different frameworks under the native environment are responsible for different objects. In order to remove duplicate image caches, it is necessary to unify the image loading management.

The same is true for Ali's solution , which shares the local text cache and memory cache of pictures by connecting to the native picture library. Its implementation idea is to connect to an external library through customization ImageProviderand to obtain image data for analysis, and the processing of the connection is to extend the Flutter Engine.Codec

If you do not want to modify the Flutter Engine, you can also use external textures to handle it. By PlatformChannelrequesting the original, the external texture data of the picture is displayed, and TextTurethe picture is displayed through the component.

// 自定义 ImageProvider 中,通过 Channel 去请求 textureId
var id = await _channel.invokeMethod('newTexture', {
  "imageUrl": imageUrl,
  "width": width ?? 0,
  "height": height ?? 0,
  "minWidth": constraints.minWidth,
  "minHeight": constraints.minHeight,
  "maxWidth": constraints.maxWidth,
  "maxHeight": constraints.maxHeight,
  "cacheKey": cacheKey,
  "fit": fit.index,
  "cacheOriginFile": cacheOriginFile,
});

// ImageWidget 中展示时通过 textureId 去显示图片
SizedBox(
  width: width,
  heigt: height,
  child: Texture(
    filterQuality: FilterQuality.high,
    textureId: _imageProvider.textureId.value,
  ),
)

Summarize

Different businesses have different requirements for the degree and requirements of mixing, and there is no universal solution. For example, in the case of my team 主Flutter混原生, 路由管理I chose PlatformViewthis processing mode on the website, which is easier to develop and maintain. If there are compatibility problems later, I can also transition to flutter_boostthe FlutterEngineGroupwebsite.


Link: https://juejin.cn/post/7262616799219482681

Guess you like

Origin blog.csdn.net/Goals1989/article/details/132097317