Flutter: analysis of the principle of flutter boost

Android

We learned before How Flutter interacts with Native (Android) With this knowledge, it is easy to understand the principle of flutter boost. So how does it work?

Flutter boost defines an Activity - BoostFlutterActivity. When it is used, an Intent will be created through newengineintensbuilder. Its build code is as follows:

public Intent build(@NonNull Context context) {
    ...
    return new Intent(context, activityClass)
            .putExtra(EXTRA_BACKGROUND_MODE, backgroundMode)
            .putExtra(EXTRA_DESTROY_ENGINE_WITH_ACTIVITY, false)
            .putExtra(EXTRA_URL, url)
            .putExtra(EXTRA_PARAMS, serializableMap);
}
copy

It can be seen that it supports not only route, but also parameter passing params, which is exactly what we need. How is this implemented?

First, look at its onCreate function:

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    ...
    delegate = new FlutterActivityAndFragmentDelegate(this);
    delegate.onAttach(this);
    ...
    setContentView(createFlutterView());
    ...
}
copy

Create a view through createFlutterView and setContentView. In createFlutterView:

protected View createFlutterView() {
    return delegate.onCreateView(null,null,null);
}
copy

delegate is the FlutterActivityAndFragmentDelegate object. Its onCreateView:

public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    mSyncer = FlutterBoost.instance().containerManager().generateSyncer(this);
    ensureAlive();
    flutterView = new XFlutterView(host.getActivity(), FlutterBoost.instance().platform().renderMode(), host.getTransparencyMode());
    ...
    mSyncer.onCreate();
    return flutterSplashView;
}
copy

The first line passes containermanager () generateSyncer creates an mSyncer. containerManager() gets a FlutterViewContainerManager object. Its generateSyncer:

@Override
public IOperateSyncer generateSyncer(IFlutterViewContainer container) {
    ...
    ContainerRecord record = new ContainerRecord(this, container);
    ...
    mRefs.add(new ContainerRef(record.uniqueId(),container));
    return record;
}
copy

Here you can see that mSyncer is actually a ContainerRecord. This is very important. It will be used to implement route later.

However, we haven't seen how URLs and params are used yet. Looking back at the onResume function of BoostFlutterActivity, all lifecycle functions of BoostFlutterActivity will call the corresponding function of delegate, so we can directly look at its onResume:

public void onResume() {
    mSyncer.onAppear();
    ...
}
copy

You can see that the onAppear function of mSyncer is called at the beginning, and mSyncer already knows that it is ContainerRecord, so its onAppear:

@Override
public void onAppear() {
    ...
    mState = STATE_APPEAR;
    mManager.pushRecord(this);
    mProxy.appear();
    mContainer.getBoostFlutterView().onAttach();
}
copy

The point is mProxy Appearance (), this mProxy is an internal class MethodChannelProxy. Its appear ance:

private void appear() {
    invokeChannelUnsafe("didShowPageContainer",
            mContainer.getContainerUrl(),
            mContainer.getContainerUrlParams(),
            mUniqueId
    );
    mState = STATE_APPEAR;
}
copy

Here we see the mContainer Getcontainerurl() and mContainer Getcontainerurlparams (), this mContainer is the first flutterboost Instance() Containermanager() Generatesynchronizer (this); The passed in this is FlutterActivityAndFragmentDelegate. Its two functions (of the Host interface) call the functions corresponding to BoostFlutterActivity (which implements the Host):

@Override
public String getContainerUrl() {
    if (getIntent().hasExtra(EXTRA_URL)) {
        return getIntent().getStringExtra(EXTRA_URL);
    }
    return "";
}

@Override
public Map getContainerUrlParams() {
    if (getIntent().hasExtra(EXTRA_PARAMS)) {
        SerializableMap serializableMap = (SerializableMap) getIntent().getSerializableExtra(EXTRA_PARAMS);
        return serializableMap.getMap();
    }
    Map<String, String> params = new HashMap<>();
    return params;
}
copy

This is the url and params we passed in. So next, let's look at how the invokeChannelUnsafe function is executed:

public void invokeChannelUnsafe(String method, String url, Map params, String uniqueId) {
    HashMap<String, Object> args = new HashMap<>();
    args.put("pageName", url);
    args.put("params", params);
    args.put("uniqueId", uniqueId);
    FlutterBoost.instance().channel().invokeMethodUnsafe(method, args);
}
copy

The FlutterBoost channel() returns the FlutterBoostPlugin. Its invokeMethodUnsafe layer by layer calls are finally executed:

public void invokeMethod(final String name, Serializable args, MethodChannel.Result result) {
    ...
    mMethodChannel.invokeMethod(name, args, result);
}
copy

mMethodChannel is a MethodChannel type, which we explained earlier. It is one of the interaction methods between native and fluent. So finally, the flutter's didShowPageContainer is executed, and the url and params are passed in as parameters. So how does flutter handle it? After searching, it is found that in the ContainerCoordinator class:

Future<dynamic> _onMethodCall(MethodCall call) {
    final String pageName = call.arguments['pageName'] as String;
    final Map<String, dynamic> params =
        (call.arguments['params'] as Map<dynamic, dynamic>)
            ?.cast<String, dynamic>();
    final String uniqueId = call.arguments['uniqueId'] as String;

    switch (call.method) {
      ...
      case 'didShowPageContainer':
        nativeContainerDidShow(pageName, params, uniqueId);
        break;
      ...
    }
    return Future<dynamic>(() {});
  }
  
  bool nativeContainerDidShow(
  String name,
  Map<String, dynamic> params,
  String pageId,
) {
  FlutterBoost.containerManager
      ?.showContainer(_createContainerSettings(name, params, pageId));

  // Compatible to accessibility mode on Android.
  if (Platform.isAndroid) {
    try {
      final SemanticsOwner owner =
          WidgetsBinding.instance.pipelineOwner?.semanticsOwner;
      final SemanticsNode root = owner?.rootSemanticsNode;
      root?.detach();
      root?.attach(owner);
    } catch (e) {
      assert(false, e.toString());
    }
  }
copy

The corresponding execution method of didShowPageContainer is nativeContainerDidShow. The first line of code executes containerManager showContainer, containerManager is a bootcontainermanager. Its showContainer:

void showContainer(BoostContainerSettings settings) {
    if (settings.uniqueId == _onstage.settings.uniqueId) {
      _onShownContainerChanged(null, settings.uniqueId);
      return;
    }

    final int index = _offstage.indexWhere((BoostContainer container) =>
        container.settings.uniqueId == settings.uniqueId);
    if (index > -1) {
      _offstage.add(_onstage);
      _onstage = _offstage.removeAt(index);

      setState(() {});

      for (final BoostContainerObserver observer in FlutterBoost
          .singleton.observersHolder
          .observersOf<BoostContainerObserver>()) {
        observer(ContainerOperation.Onstage, _onstage.settings);
      }
      Logger.log('ContainerObserver#2 didOnstage');
    } else {
      pushContainer(settings);
    }
  }
copy

If the page does not exist before, execute pushContainer(settings):

 void pushContainer(BoostContainerSettings settings) {
    assert(settings.uniqueId != _onstage.settings.uniqueId);
    assert(_offstage.every((BoostContainer container) =>
        container.settings.uniqueId != settings.uniqueId));

    _offstage.add(_onstage);
    _onstage = BoostContainer.obtain(widget.initNavigator, settings);

    setState(() {});

    for (final BoostContainerObserver observer in FlutterBoost
        .singleton.observersHolder
        .observersOf<BoostContainerObserver>()) {
      observer(ContainerOperation.Push, _onstage.settings);
    }
    Logger.log('ContainerObserver#2 didPush');
  }
copy

Here, the bootcontainer Obtain to create a widget and assign it to_ onstage, the source code of this function:

 factory BoostContainer.obtain(
      Navigator navigator,
      BoostContainerSettings settings,
      ) =>
      BoostContainer(
        key: GlobalKey<BoostContainerState>(),
        settings: settings,
        onGenerateRoute: (RouteSettings routeSettings) {
          if (routeSettings.name == '/') {
            return BoostPageRoute<dynamic>(
              pageName: settings.name,
              params: settings.params,
              uniqueId: settings.uniqueId,
              animated: false,
              settings: RouteSettings(
                name: settings.name,
                arguments: routeSettings.arguments,
              ),
              builder: settings.builder,
            );
          } else {
            return navigator.onGenerateRoute(routeSettings);
          }
        },
        observers: <NavigatorObserver>[
          ContainerNavigatorObserver.bindContainerManager(),
          HeroController(),
        ],
        onUnknownRoute: navigator.onUnknownRoute,
      );
copy

You can see that this is to create widget s through RouteFactory, which we are familiar with. In this way, the router is implemented and the parameters are transferred.

Observe the BoostContainerManager (container_mannager.dart) to find that_ onstage is the currently displayed page, and_ The offstage page is the previous level page, and the layout is actually all stacked:

final List<BoostContainer> containers = <BoostContainer>[];
containers.addAll(_offstage);

assert(_onstage != null, 'Should have a least one BoostContainer');
containers.add(_onstage);
copy

So by changing_ onstage and_ offstage is used to switch pages. Therefore, the essence of flutter boost is to use a page to switch different contents, and all pages share a flutter engine (all pages enter a page, so initialRoute is fixed). In this way, except for the first time, opening it again will be very fast, and the startup speed will be accelerated.

In this way, we have a general understanding of the startup principle of flutter boost. Of course, flutter boost has many functions, but after understanding the startup principle, we can try to implement a simple framework by ourselves.

iOS

ios is actually similar to android. ios interacts with fluent in the same three ways.

In ios, we use the FlutterViewController to display the flutter page. You can refer to Flutter mixed development: introducing flutter into existing iOS projects , so this is equivalent to the FlutterActivity in android. At the same time, we know that the function didShowPageContainer is used to display the page. Because this function is defined in flutter, it should also be called at the ios level. This is relatively simple.

We can find flbflutterviewcontainer M (shutter_boost/ios/classes/container/). Search this file for didShowPageContainer and find the following code:

- (void)viewDidAppear:(BOOL)animated
{
    [FLUTTER_APP addUniqueViewController:self];
    
    //Ensure flutter view is attached.
    [self attatchFlutterEngine];
 
    [BoostMessageChannel didShowPageContainer:^(NSNumber *result) {}
                                           pageName:_name
                                             params:_params
                                           uniqueId:self.uniqueIDString];
    //NOTES: be sure to update after show, otherwise it will flash; Or cause sideslip. When returning, the previous page will be the same as the top page
    [self surfaceUpdated:YES];
    
    [super viewDidAppear:animated];
}
FLBFlutterViewContainer inherit ios of UIViewController´╝îsee FLBFlutterViewContainer.h
#import <UIKit/UIKit.h>
#import <Flutter/Flutter.h>
#import "FLBFlutterContainer.h"

NS_ASSUME_NONNULL_BEGIN
@interface FLBFlutterViewContainer  : FlutterViewController<FLBFlutterContainer>
@property (nonatomic,copy,readwrite) NSString *name;
- (instancetype)init NS_DESIGNATED_INITIALIZER;
- (void)surfaceUpdated:(BOOL)appeared;
@end
NS_ASSUME_NONNULL_END
copy

The viewDidAppear function is inherited from him. Its life cycle is similar to that of resume in android. Therefore, execute the didShowPageContainer at this stage and complete the widget switching in the shutter.

So we can see that the principle of ios is basically similar to that of android. It also rewrites the class that carries the flutter page, and then notifies flutter interactively. In flutter, it is the way of switching widget s on a single page. In this way, a flutter engine can be used to initialize and warm up the engine in advance (running in ios) to improve the loading efficiency.

Through the above principles, we can develop a simple flutter startup plugin by ourselves. We will implement it in the next article.

Posted by infratl on Tue, 31 May 2022 10:29:37 +0530