版本号0.1.54
看源码之前,我先去看下官方文档,对于其源码的设计说明,文中所说的原生都是指android

看完官方文档的说明,我有以下几个疑问

第一个:容器是怎么设计的?

第二个:native和flutter的channel的通道是如何设计的?

第三个:Flutter是适配层到底再做些什么?

中控中心FlutterBoost

单独拎出来讲讲,这个类比较简单,就是集合各个模块并让其初始化,同时也是该插件入口处,不管原生和flutter都一样,看源码也是从这里开始看起,但原生和flutter的初始化流程稍微有少许区别,主要还是因为原生是作为容器,flutter的容器是依赖于原生容器。

原生init

入口:/flutter_boost/example/android/app/src/main/java/com/taobao/idlefish/flutterboostexample/MyApplication.java

FlutterBoost.init从这里开始进入

 FlutterBoost.init(new Platform() {

            @Override
            public Application getApplication() {
                return MyApplication.this;
            }

            @Override
            public boolean isDebug() {
                return true;
            }

            @Override
            public void openContainer(Context context, String url, Map<String, Object> urlParams, int requestCode, Map<String, Object> exts) {
                PageRouter.openPageByUrl(context, url, urlParams, requestCode);
            }

            @Override
            public IFlutterEngineProvider engineProvider() {
                //注意这里  覆写了createEngine
                return new BoostEngineProvider() {
                    @Override
                    public BoostFlutterEngine createEngine(Context context) {
                        return new BoostFlutterEngine(context, new DartExecutor.DartEntrypoint(
                                context.getResources().getAssets(),
                                FlutterMain.findAppBundlePath(context),
                                "main"), "/");
                    }
                };
            }

            @Override
            public int whenEngineStart() {
                return ANY_ACTIVITY_CREATED;
            }
        });

        BoostChannel.addActionAfterRegistered(new BoostChannel.ActionAfterRegistered() {
            @Override
            public void onChannelRegistered(BoostChannel channel) {
                //platform view register should use FlutterPluginRegistry instread of BoostPluginRegistry
                TextPlatformViewPlugin.register(FlutterBoost.singleton().engineProvider().tryGetEngine().getPluginRegistry());
            }
        });
    }

上面大部分方法,做过android也知道是干嘛的,这里重点讲讲IFlutterEngineProvider这个接口,这里有3个方法,如下

/**
 * a flutter engine provider
 */
public interface IFlutterEngineProvider {

    /**
     * create flutter engine, we just hold a single instance now
     * @param context
     * @return
     */
    BoostFlutterEngine createEngine(Context context);

    /**
     * provide a flutter engine
     * @param context
     * @return
     */
    BoostFlutterEngine provideEngine(Context context);

    /**
     * may return null
     * @return
     */
    BoostFlutterEngine tryGetEngine();
}


抽象成接口,根据项目的实际情况,开发者可以自己实现flutter引擎,或采用官方源码里自己的实现类即BoostEngineProvider,但想不明白 createEngine 和provideEngine 到底有何区别,到底为何弄成两个方法,不就是个提供个flutter引擎实例吗?

看了下createEngine的实现,主要加载实例BoostFlutterEngine,这个实例看名字也清楚是进行flutter引擎的初始化,设置了dart默认入口点即main,设置了路由起点及插件的声明注册一类
然后去看provideEngine方法的实现,代码较少,如下

   @Override
    public BoostFlutterEngine provideEngine(Context context) {
        Utils.assertCallOnMainThread();

        if (mEngine == null) {
            FlutterShellArgs flutterShellArgs = new FlutterShellArgs(new String[0]);
            FlutterMain.ensureInitializationComplete(
                    context.getApplicationContext(), flutterShellArgs.toArray());

            mEngine = createEngine(context.getApplicationContext());

            final IStateListener stateListener = FlutterBoost.sInstance.mStateListener;
            if(stateListener != null) {
                stateListener.onEngineCreated(mEngine);
            }
        }
        return mEngine;
    }

初始化flutter参数及增加一个回调,没什么特别之处,然后去翻了下flutter.jar的FlutterActivity源码,它的flutter引擎初始化最后是追踪到FlutterFragment,关键代码如下

public void onAttach(Context context) {
        super.onAttach(context);
        //这里初始化flutter参数
        this.initializeFlutter(this.getContextCompat());
        if (this.flutterEngine == null) {
        //这里是初始化flutter引擎
            this.setupFlutterEngine();
        }

        this.platformPlugin = new PlatformPlugin(this.getActivity(), this.flutterEngine.getPlatformChannel());
    }


这里是连在一起的,flutter源码没有翻来覆去全看一遍,闲鱼进行这样的接口设计应该是有一定的原因

这里再单独讲下插件的注册,我们知道native是作为插件库需要原生项目依赖,在初始化中,注意一下插件的注册,是用反射实现的,如下
路径:flutter_boost/android/src/main/java/com/idlefish/flutterboost/BoostFlutterView.java 中的init方法,代码如下

  private void init() {
 ...
        mFlutterEngine.startRun((Activity)getContext());
...
    }

跟随startRun方法深入,就会找到FlutterBoost.singleton().platform().registerPlugins(mBoostPluginRegistry),跟着下去,会发现使用反射方式来实现插件注册 如下代码

 @Override
    public void registerPlugins(PluginRegistry registry) {
        try {
            Class clz = Class.forName("io.flutter.plugins.GeneratedPluginRegistrant");
            Method method = clz.getDeclaredMethod("registerWith",PluginRegistry.class);
            method.invoke(null,registry);
        }catch (Throwable t){
            throw new RuntimeException(t);
        }
    }

毕竟引擎初始化框架重新编写了,所以在插件的注册上也改变了,init的原生部分就讲解到此

flutter

入口:/flutterProject/flutter_boost/example/lib/main.dart
flutter的源码查看前,大家务必先去看看flutter的初始化流程,Navigator源码解析及Route源码解析,因为不晓得相关初始化流程及Navigator的设计原理,里面的关键调用 大家都可能看不明白,我这边可能也是直接就过了,这里给个链接大家可以去看看

Flutter 源码解析

@override
  void initState() {
    super.initState();
    print('_MyAppState initState');
    ///路由注册,原生通过MethodChannel通道来启动对应的flutter页面
    FlutterBoost.singleton.registerPageBuilders({
      'first': (pageName, params, _) => FirstRouteWidget(),
      'second': (pageName, params, _) => SecondRouteWidget(),
      'tab': (pageName, params, _) => TabRouteWidget(),
      'flutterFragment': (pageName, params, _) => FragmentRouteWidget(params),

      ///可以在native层通过 getContainerParams 来传递参数
      'flutterPage': (pageName, params, _) {
        print("flutterPage params:$params");

        return FlutterRouteWidget();
      },
    });
  }

  @override
  Widget build(BuildContext context) {
    print('_MyAppState build');
    return MaterialApp(
        title: 'Flutter Boost example',
        builder: FlutterBoost.init(postPush: _onRoutePushed),
        home: Container());
  }
  ///flutter 路由push 监听,每启动一个新的flutter页面 就回调该方法
  void _onRoutePushed(
      String pageName, String uniqueId, Map params, Route route, Future _) {
    print('pageName'+pageName+"n");
  }

先跟随FlutterBoost.singleton进去看看,其构造函数如下

FlutterBoost(){
    Logger.log('FlutterBoost 构造函数');
    ContainerCoordinator(_boostChannel);
  }

跟随着ContainerCoordinator去看看,ContainerCoordinator和BoostChannel如何一起来维护通信通道,ContainerCoordinator构造函数如下

ContainerCoordinator(BoostChannel channel) {
    assert(_instance == null);

    _instance = this;

    channel.addEventListener("lifecycle",
        (String name, Map arguments) => _onChannelEvent(arguments));

    channel.addMethodHandler((MethodCall call) => _onMethodCall(call));
  }

增加native生命周期监听,增加方法监听,再去看看_onChannelEvent和_onMethodCall方法就应该清楚ContainerCoordinator其实就是翻译员,将与原生通信的协议进行解释翻译,根据传过来的事件名,方法名 逐一进行需要的框架处理,BoostChannel其实是声明通道,将接收和发送功能进行封装,接收natvie传来的信息,将从flutter的信息发送到native,当然也做了一部分的框架业务处理,将event和method事件进行区分
分发,个人觉得将该功能直接丢至ContainerCoordinator处理可能更好点,应该是出于为了区分event和method特意在BoostChannel进行处理

接下来看看ContainerCoordinator对于native传过来的通信数据处理,代码如下,就分为之前说的event和method两类,代码注释也写了

 /// 对native 整个应用的 生命周期 进行抽象出的几个行为事件,让flutter做相应的处理
  /// android端 基本上除了有回退事件的处理,剩余的生命周期 仅仅是做了监听没做任何处理
  /// 分别是回退处理 android才有
  /// foreground  本应用是处于前台
  /// background  本应用是处于后台
  /// scheduleFrame 触发一帧的绘制,但ios和android 都没找到发送该事件的代码,老版本遗留代码?
  Future<dynamic> _onChannelEvent(dynamic event) {
     ...
  }

  /// 对native view生命周期(在android 就是activity)进行抽象出的 几个行为事件,
  /// 让flutter做相应的框架处理
  Future<dynamic> _onMethodCall(MethodCall call) {
  ...
  }


这里就不讲Method的处理逻辑,后面会结合容器部分重点讲

接下来再回到main.dart文件,跟随FlutterBoost.init方法进去看一下,就是初始化BoostContainerManager,不再深入,后面会结合起来一起讲BoostContainerManager

channle

这模块代码比较少,先从这模块开始讲起

我们知道原生和flutter之间的通信就是通过MethodChannel这个类实现的(原生和flutter的类名一样),前面有讲flutter的boost_channel.dart的作用,native的BoostChannel其实也一样,将接收和发送功能进行封装,接收flutter传来的信息,将从native的信息发送到flutter

原生部分

前面原生初始化 讲到插件的注册是通过反射实现的,GeneratedPluginRegistrant.java当中的registerWith方法我们接下去看一下,注册的时候做了哪些事,路径lutter_boost/android/src/main/java/com/idlefish/flutterboost/BoostChannel.java

public static void registerWith(PluginRegistry.Registrar registrar) {
        sInstance = new BoostChannel(registrar);
        //通道注册后,处理flutter的method 调用处理
        for(ActionAfterRegistered a : sActions) {
            a.onChannelRegistered(sInstance);
        }
        //状态监听 回调
        if(FlutterBoost.sInstance != null) {
            final IStateListener stateListener = FlutterBoost.sInstance.mStateListener;
            if (stateListener != null) {
                stateListener.onChannelRegistered(registrar, sInstance);
            }
        }

        sActions.clear();
    }

看到了吧,BoostChannel的实例化是在插件注册的时候进行的,继续深入,如下代码

  private BoostChannel(PluginRegistry.Registrar registrar){
        mMethodChannel = new MethodChannel(registrar.messenger(), "flutter_boost");

        mMethodChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
            @Override
            public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {

                if (methodCall.method.equals("__event__")) {
                    String name = methodCall.argument("name");
                    Map args = methodCall.argument("arguments");

                    Object[] listeners = null;
                    synchronized (mEventListeners) {
                        Set<EventListener> set = mEventListeners.get(name);
                        if (set != null) {
                            listeners = set.toArray();
                        }
                    }

                    if(listeners != null) {
                        for(Object o:listeners) {
                            ((EventListener)o).onEvent(name,args);
                        }
                    }
                }else{
                    Object[] handlers;
                    synchronized (mMethodCallHandlers) {
                        handlers = mMethodCallHandlers.toArray();
                    }

                    for(Object o:handlers) {
                        ((MethodChannel.MethodCallHandler)o).onMethodCall(methodCall,result);
                    }
                }
            }
        });
    }

对于通道上的数据分为两类event和method,都是和flutter一一对应的,前面flutter初始化中,也讲过,在原生这边event 没有框架上的业务处理,但提供了回调,根据自己的业务是否需要增加监听

method的处理,去查看BoostMethodHandler,FlutterBoost.java作为内部类存在,如下

class BoostMethodHandler implements MethodChannel.MethodCallHandler {

        @Override
        public void onMethodCall(MethodCall methodCall, final MethodChannel.Result result) {
            switch (methodCall.method) {
                case "pageOnStart":
              ...
                break;
                case "openPage":
               ...
                break;
                case "closePage":
               ...
                break;
                case "onShownContainerChanged":
               ...
                break;
                default:
                {
                    result.notImplemented();
                }
            }
        }
    }

看这些switch的case处理,大概也猜出来是干嘛的了,是flutter通知native的页面行为事件,例openPage,closePage等等 ,在初始化中,讲了挺多flutter的chanell,所以这里就不在讲了,但是讲讲两边的设计
两边都有个channel类,主要都是用来接收和发送消息的
flutter专门有一个类ContainerCoordinator.dart,中文翻译过来就是集装箱协调员,用于通信事件的统一处理,就是将从原生接收到的信息进行处理,但是在原生那边并没有类似的类,而是将这个工作放在FlutterBoost.java这个内部类中,个人觉得为了保持统一可以专门抽象出个类,将该功能放置该类中,放在FlutterBoost.java不能保持高度统一且不雅观吧

讲到这里,其实通道的设计大家应该理解得差不多了(解决开头提出的问题)

native和flutter的channel的通道是如何设计的?

容器

原生部分

闲鱼的栈管理方案,是将栈的管理都放置原生,所以在原生必须暴露栈的管理,让项目接入方能在原有栈的解决方案上 融合进闲鱼的栈管理方案,所以页面的打开就是入口处,从该入口处去查看容器的设计,先从demo中的PageRouter.java看起,如下代码

public class PageRouter {

    public static final String NATIVE_PAGE_URL = "sample://nativePage";
    public static final String FLUTTER_PAGE_URL = "sample://flutterPage";
    public static final String FLUTTER_FRAGMENT_PAGE_URL = "sample://flutterFragmentPage";

    public static boolean openPageByUrl(Context context, String url,Map params) {
        return openPageByUrl(context, url,params, 0);
    }

    public static boolean openPageByUrl(Context context, String url, Map params, int requestCode) {
        try {
            if (url.startsWith(FLUTTER_PAGE_URL)) {
                context.startActivity(new Intent(context, FlutterPageActivity.class));
                return true;
            } else if (url.startsWith(FLUTTER_FRAGMENT_PAGE_URL)) {
                context.startActivity(new Intent(context, FlutterFragmentPageActivity.class));
                return true;
            } else if (url.startsWith(NATIVE_PAGE_URL)) {
                context.startActivity(new Intent(context, NativePageActivity.class));
                return true;
            } else {
                return false;
            }
        } catch (Throwable t) {
            return false;
        }
    }
}

如果大家用过阿里的Aroute路由框架,就会觉得很亲切,将每个View配置一个路由,还是前端的思想借鉴过来,一个统一的界面打开处,根据路由路径,判断是原生view还是FlutterView,分别打开不同的Activity

接入方,在这里可以根据自身的原生栈管理再进行抽象封装就ok了

接下来看看哪里调用了openPageByUrl(注意是下面那个)方法,发现正是我们一开始框架初始化的时候在调用,如下,文件路径flutter_boost/example/android/app/src/main/java/com/taobao/idlefish/flutterboostexample/MyApplication.java

 @Override
            public void openContainer(Context context, String url, Map<String, Object> urlParams, int requestCode, Map<String, Object> exts) {
                PageRouter.openPageByUrl(context, url, urlParams, requestCode);
            }


再查看是哪里调用了该方法,一直追踪到FlutterBoost.java,关键代码如下

case "openPage":
                {
                    try {
                        Map<String,Object> params = methodCall.argument("urlParams");
                        Map<String,Object> exts = methodCall.argument("exts");
                        String url = methodCall.argument("url");

                        mManager.openContainer(url, params, exts, new FlutterViewContainerManager.OnResult() {
                            @Override
                            public void onResult(Map<String, Object> rlt) {
                                if (result != null) {
                                    result.success(rlt);
                                }
                            }
                        });
                    }catch (Throwable t){
                        result.error("open page error",t.getMessage(),t);
                    }
                }

Flutter 通过channel通道 通知原生 要打开一个新的页面,然后原生将自身的生命周期通过通道告知flutter,flutter再进行相应的页面处理,虽然短短一句话,但其中的逻辑及代码量还是很多的...

前面已经讲了通道部分,这里再贴点关键代码,原生将自身的生命周期通过通道告知flutter,关键代码如下

private class MethodChannelProxy {
        private int mState = STATE_UNKNOW;

        private void create() {
          ...
        }
        private void appear() {
            ...
        }
        private void disappear() {
           ...
            }
        }
        private void destroy() {
            ..
        }
        public void invokeChannel(String method, String url, Map params, String uniqueId) {
            ...
        }
        public void invokeChannelUnsafe(String method, String url, Map params, String uniqueId) {
           ..
        }
    }
    public static String genUniqueId(Object obj) {
        return System.currentTimeMillis() + "-" + obj.hashCode();
    }
}

ok,现在已经找到了MethodChannelProxy类,那我们就继续回找(注意我这里讲解都是从冰山一角再慢慢往上查,最终再将冰山一起探索完毕)MethodChannelProxy是作为ContainerRecord.java的内部类存在。接下来我们来看ContainerRecord类,其实现了IContainerRecord接口,再继续深究找到IOperateSyncer接口,代码如下

public interface IOperateSyncer {

    void onCreate();

    void onAppear();

    void onDisappear();

    void onDestroy();

    void onBackPressed();

    void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults);

    void onNewIntent(Intent intent);

    void onActivityResult(int requestCode, int resultCode, Intent data);

    void onContainerResult(int requestCode, int resultCode, Map<String,Object> result);

    void onUserLeaveHint();

    void onTrimMemory(int level);

    void onLowMemory();
}

该接口是通过对原生的生命周期再结合flutter的生命周期特色及android自身的特性(有回退物理键)抽象出来的,继续回到接下来我们来看ContainerRecord类,发现MethodChannelProxy类其实就是做个代理功能,看名字也清楚,在接口方法被调用的时候,通知flutter

接下来看看IContainerRecord的方法被调用处,追踪到BoostFlutterActivity.java和BoostFlutterFragment.java,这里我们只看Activity,Fragment基本差不多。我们看到BoostFlutterActivity在走onCreate生命周期方法时,创建了mSyncer,关键代码如下

   @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        configureWindowForTransparency();

mSyncer = FlutterBoost.singleton().containerManager().generateSyncer(this);


        mFlutterEngine = createFlutterEngine();
        mFlutterView = createFlutterView(mFlutterEngine);

        setContentView(mFlutterView);

        mSyncer.onCreate();

        configureStatusBarForFullscreenFlutterExperience();
    }

FlutterBoost.singleton().containerManager().generateSyncer() 继续深入,追踪到FlutterViewContainerManager类,关键代码如下

@Override
    public IOperateSyncer generateSyncer(IFlutterViewContainer container) {
        Utils.assertCallOnMainThread();
        //创建容器记录实例
        ContainerRecord record = new ContainerRecord(this, container);
        if (mRecordMap.put(container, record) != null) {
            Debuger.exception("container:" + container.getContainerUrl() + " already exists!");
        }
        mRefs.add(new ContainerRef(record.uniqueId(),container));
        //讲接口引用返回
        return record;
    }


ContainerRecord实例在这里创建,同时将接口引用返给Activity,这样就和原生View的生命周期关联起来了

注意到这里我们已经追踪到FlutterViewContainerManager.java类,已经可以从上帝视角去看了。

刚刚从容器打开出入一直追踪到FlutterViewContainerManager.java类,该类看名字就清楚就是容器的管理者,容器创建、打开、关闭、销毁、弹出、移除等等工作都是在这儿,这里最关键的generateSyncer方法刚刚追踪的时候已经讲过。这里再重点讲讲该类的setContainerResult方法,如下

void setContainerResult(IContainerRecord record,int requestCode, int resultCode, Map<String,Object> result) {

        IFlutterViewContainer target = findContainerById(record.uniqueId());
        if(target == null) {
            Debuger.exception("setContainerResult error, url="+record.getContainer().getContainerUrl());
        }

        if (result == null) {
            result = new HashMap<>();
        }

        result.put("_requestCode__",requestCode);
        result.put("_resultCode__",resultCode);

        final OnResult onResult = mOnResults.remove(record.uniqueId());
        if(onResult != null) {
            onResult.onResult(result);
        }
    }

单独拎出来讲,主要是本人好奇 目标页 向 起始面 如何传输数据的,
在纯ntive就是靠着onActivityResult回调拿到目标页传回的数据,该方法就是处理目标页传回来后的处理

在混合栈中 就分为3种情况

1.native-flutter
2.flutter-native
3.flutter-flutter

native和ntive就不用说了,都用不到该框架


第一种情况:native-flutter
demo自身当中并没有相关的演示代码,于是我按照原生是如何接受传回来的数据去进行更改,改了两处如下,类路径flutterProject/flutter_boost/example/android/app/src/main/java/com/taobao/idlefish/flutterboostexample/MainActivity.java

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
data.getSerializableExtra(IFlutterViewContainer.RESULT_KEY);
Debuger.log("MainActivityResult"+data.getSerializableExtra(IFlutterViewContainer.RESULT_KEY).toString());
}
还有一处,类路径flutterProject/flutter_boost/example/android/app/src/main/java/com/taobao/idlefish/flutterboostexample/PageRouter.java
如下

public static boolean openPageByUrl(Context context, String url, Map params, int requestCode) {
        try {
            if (url.startsWith(FLUTTER_PAGE_URL)) {
                //接受目标页的回传必须通过startActivityForResult进行打开
                ((Activity)context).startActivityForResult(new Intent(context, FlutterPageActivity.class),requestCode);
                return true;
            } else if (url.startsWith(FLUTTER_FRAGMENT_PAGE_URL)) {
                context.startActivity(new Intent(context, FlutterFragmentPageActivity.class));
                return true;
            } else if (url.startsWith(NATIVE_PAGE_URL)) {
                context.startActivity(new Intent(context, NativePageActivity.class));
                return true;
            } else {
//                context.startActivity(new Intent(context, FlutterTwoPageActivity.class));
                return false;
            }
        } catch (Throwable t) {
            return false;
        }
    }

还有记得修改调起的Flutter页面是'second',因为demo中只有它才有传回数据,实现原理这里我简单描述,不详细讲了,就是flutter在关闭页面的时候,传回数据,如下

class SecondRouteWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Second Route"),
      ),
      body: Center(
        child: RaisedButton(
          onPressed: () {
            // Navigate back to first route when tapped.

            BoostContainerSettings settings =
                BoostContainer.of(context).settings;
            FlutterBoost.singleton.close(settings.uniqueId,
                result: {"result": "data from second"});
          },
          child: Text('Go back with result!'),
        ),
      ),
    );
  }
}

跟踪关闭代码逻辑,通过之前建立的通信通道,传过去相关方法,即closePage,原生接收到之后的逻辑代码处理如下(类文件路径flutterProject/flutter_boost/android/src/main/java/com/idlefish/flutterboost/FlutterBoost.java):

 case "closePage":
                {
                    try {
                        String uniqueId = methodCall.argument("uniqueId");
                        Map<String,Object> resultData = methodCall.argument("result");
                        Map<String,Object> exts = methodCall.argument("exts");

                        mManager.closeContainer(uniqueId, resultData,exts);
                        result.success(true);
                    }catch (Throwable t){
                        result.error("close page error",t.getMessage(),t);
                    }
                }

追踪closeContainer,一直追踪到BoostFlutterActivity.java的finishContainer方法,如下

@Override
    public void finishContainer(Map<String,Object> result) {
        if(result != null) {
            FlutterBoost.setBoostResult(this,new HashMap<>(result));
            finish();
        }else{
            finish();
        }
    }

再跟踪下去,逻辑很明朗了就不详细讲了


第二种情况:flutter-native
闲鱼的混合栈方案里,每个flutter都有自己的独立原生宿主View,所以回调也得依赖原生

原生我们知道生命周期里就有回调方法,即onActivityResult方法,但是Flutter并没有该方法,闲鱼的混合框架里也并没有专门把这个生命周期给抽出来,本人更倾向于把这个给抽出来,这样框架也比较清晰。不过现在很多原生业务都已经很少用这种方式进行页面传值,因为业务复杂起来,用这种方式反而更麻烦,所以原生就出现了很多eventBus类似的通信框架,所以设计混合栈框架的时候,就直接忽略,而直接用自带的flutter api来实现该功能,怎么实现的?继续看

先看下invokeMethod这个方法,原生和flutter都会有个回调函数,flutter页面拿到目标页的数据传回就是采用该方法,接下来咱们去看flutter页面打开native页面开始看起,类路径flutterProject/flutter_boost/example/lib/simple_page_widgets.dart,关键代码如下:

InkWell(
            child: Container(
                padding: const EdgeInsets.all(8.0),
                margin: const EdgeInsets.all(8.0),
                color: Colors.yellow,
                child: Text(
                  'open native page',
                  style: TextStyle(fontSize: 22.0, color: Colors.black),
                )),

            ///后面的参数会在native的IPlatform.startActivity方法回调中拼接到url的query部分。
            ///例如:sample://nativePage?aaa=bbb
            onTap: () =>
                FlutterBoost.singleton.open("sample://nativePage", urlParams: {
                  "query": {"aaa": "bbb"}
                }).then((Map value) {
                    print(
                        "call me when page is finished. did recieve second route result $value");
                  }),
          )

FlutterBoost.singleton.open 跟踪下去,发现最终调用的就是invokeMethod,
如下

  Future<Map<dynamic,dynamic>> open(String url,{Map<dynamic,dynamic> urlParams,Map<dynamic,dynamic> exts}){

    Map<dynamic, dynamic> properties = new Map<dynamic, dynamic>();
    properties["url"] = url;
    properties["urlParams"] = urlParams;
    properties["exts"] = exts;
    return channel.invokeMethod<Map<dynamic,dynamic>>(
        'openPage', properties);
  }

然后返回的Future,异步的回调函数,拿到原生页面的回传数据。这里的逻辑很简单,重点是原生那边怎么保存该回调,然后在关闭容器的时候进行调用 回调函数以此将数据传给Flutter

接下来看原生对于openPage的处理,之前在讲通道的时候提过,类路径
flutterProject/flutter_boost/android/src/main/java/com/idlefish/flutterboost/FlutterBoost.java,代码如下

  case "openPage":
                {
                    try {
                        Map<String,Object> params = methodCall.argument("urlParams");
                        Map<String,Object> exts = methodCall.argument("exts");
                        String url = methodCall.argument("url");
                        mManager.openContainer(url, params, exts, new FlutterViewContainerManager.OnResult() {
                            @Override
                            public void onResult(Map<String, Object> rlt) {
                                if (result != null) {
                                    result.success(rlt);
                                }
                            }
                        });
                    }catch (Throwable t){
                        result.error("open page error",t.getMessage(),t);
                    }
                }
                break;

重点看 mManager.openContainer方法,传入了一个回调函数,最后调用
result.success(rlt);,数据就传回flutter页面,接下来我们跟踪去看看如何去保存该回调并 最终调用回调

继续跟踪openContainer方法,代码如下:

void openContainer(String url, Map<String, Object> urlParams, Map<String, Object> exts,OnResult onResult) {
        Context context = FlutterBoost.singleton().currentActivity();
        if(context == null) {
            context = FlutterBoost.singleton().platform().getApplication();
        }

        if(urlParams == null) {
            urlParams = new HashMap<>();
        }

        int requestCode = 0;
        final Object v = urlParams.remove("requestCode");
        if(v != null) {
            requestCode = Integer.valueOf(String.valueOf(v));
        }

        final String uniqueId = ContainerRecord.genUniqueId(url);
        urlParams.put(IContainerRecord.UNIQ_KEY,uniqueId);
        if(onResult != null) {
            mOnResults.put(uniqueId,onResult);
        }

        FlutterBoost.singleton().platform().openContainer(context,url,urlParams,requestCode,exts);
    }

注意 这里有个mOnResults的Map类型参数,就是保存回调函数,通过每个Flutter页面容器的uniqueId做key(只有Flutter页面会建立容器),但前提是该起始容器打开的时候必须传入Key,不然就无法回调,因为找不到该回调了。这就会出现一个问题,就是我们第一个打开的Flutter页面并不是通过onePage打开的,而是直接通过Context.startActivity方法打开,那么就不会保存该回调,也就无法将目标页的数据传回起始页了,已经反馈给闲鱼官方了,本人想过几种方式,为了这个简单的功能,就破坏整体框架得不偿失,等闲鱼官方更优雅的解决方式吧

继续这个mOnResults这个参数,验证我们的猜想,看看哪里在使用,刚刚只是写入回调函数,就找到setContainerResult这个方法,就回到刚刚说要重点讲的方法那了,关键代码如下:

void setContainerResult(IContainerRecord record,int requestCode, int resultCode, Map<String,Object> result) {
....
        final OnResult onResult = mOnResults.remove(record.uniqueId());
        Debuger.log("setContainerResult uniqueId "+record.uniqueId());
        if(onResult != null) {
            Debuger.log("onResult has result");
            onResult.onResult(result);
        }
    }

看看哪里有调用这个方法,发现有两处,一处就是原生的生命周期onDestroy的时候,代码如下:

    @Override
    public void onDestroy() {
        ...
        mManager.setContainerResult(this,-1,-1,null);
        ...
    }


这个是当前页面销毁的时候,但并没有数据传回,明显不是

ok,继续看另外追踪后的一处关键代码,
代码如下:

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    ...
        mSyncer.onContainerResult(requestCode,resultCode,result);
    }

ok,Flutter起始页拿到native传回的数据


第三种情况:flutter-flutter
这里不细讲了,因为就是第一种和第二种的逻辑区分,无非目标页不太一样,传值就是第一种情况的逻辑,拿值就是第二种情况的逻辑

原生关于容器部分,再讲下IFlutterViewContainer.java和IContainerRecord.java 这两个类,因为容器部分主要就是围绕 这两个抽象出来的接口进行一系列的框架实现,这里面做了相当多的抽象,IContainerRecord.java比较好理解,对原生View 生命周期的部分方法抽象,例:onCreate方法,通知flutter页面可以做一些初始化工作(这里面就涉及到flutter容器部分了),还有引擎部分的部分方法抽象等

IFlutterViewContainer.java这个类主要是用于业务代码使用的,你可以看它的实现类都是在demo当中,然后抽象出的方法都是传参,传路由路径,容器关闭时的参数回传等等

好了原生容器的讲解就到此,应该还是遗漏了不少细节的地方,本人觉得好理解就直接过去了

Flutter部分

讲这一部分之前,我们得先了解个flutter的一个widget 叫做Overlay!

了解这玩意,就能弄清楚混合栈是如何做flutter页面的栈,这个组件最大的特点就是提供了动态的在Flutter的渲染树上插入布局的特性。那岂不是很适合Toast这样的场景? 是的去Google下,发现的全是用Overlay来做Toast功能

基于Overlay的特性,就可以用全屏非透明的Overlay,每增加一个flutter页面就增加一个包含自定义的Widget的OverlayEntry,然后覆盖在上一个OverlayEntry上,用户反正看到的只是覆盖在最顶层的OverlayEntry,如果还不能理解可以看看这篇文章

ok,背景交代完毕,现在要去看闲鱼如何设计的这个容器及页面栈,我们就从打开第一个flutter页面作为入口开始看起。类路径:flutterProject/flutter_boost/example/android/app/src/main/java/com/taobao/idlefish/flutterboostexample/PageRouter.java,第一个打开的Flutter页面是FlutterPageActivity.java,前面在讲通道设计的时候,讲到过原生生命周期和Flutter生命周期的绑定,提到过一个抽象出来的接口IOperateSyncer.java,先从onCreate方法开始看起,经过生命周期绑定调用,生成原生容器,提取定义好的通道参数,然后经过通道通信,最后追踪到flutter的didInitPageContainer的方法处理
(类路径flutterProject/flutter_boost/lib/container/container_coordinator.dart)

继续跟踪,中间会经过生命周期的监听调用,最终调用_createContainerSettings方法
(类路径flutterProject/flutter_boost/lib/container/container_coordinator.dart),代码如下

BoostContainerSettings _createContainerSettings(
      String name, Map params, String pageId) {
    Widget page;

    final BoostContainerSettings routeSettings = BoostContainerSettings(
        uniqueId: pageId,
        name: name,
        params: params,
        builder: (BuildContext ctx) {
          //Try to build a page using keyed builder.
          if (_pageBuilders[name] != null) {
            page = _pageBuilders[name](name, params, pageId);
          }

          //Build a page using default builder.
          if (page == null && _defaultPageBuilder != null) {
            page = _defaultPageBuilder(name, params, pageId);
          }

          assert(page != null);
          Logger.log('build widget:$page for page:$name($pageId)');

          return page;
        });

    return routeSettings;
  }

根据方法,再过一遍代码,就是flutter页面容器的参数配置,同时找到一开始注册好的page页面

接下来跟踪原生的appear方法,同样的一阵信号传输...,最终进入flutter的ContainerCoordinator.dart类中的didShowPageContainer方法,继续跟踪,追踪到flutter_boost/lib/container/container_coordinator.dart的showContainer方法

注意的是 flutter容器初始化的过程中做了很多兼容工作,兼容ios兼容android,毕竟两个平台的生命周期是有所差别,但最终要抽象成一样的生命周期,所以要做不少的兼容工作,例如连续2次(didInitPageContainer和didShowPageContainer)进行初始化flutter容器参数

继续看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 (BoostContainerObserver observer in FlutterBoost
          .singleton.observersHolder
          .observersOf<BoostContainerObserver>()) {
        observer(ContainerOperation.Onstage, _onstage.settings);
      }
      Logger.log('ContainerObserver#2 didOnstage');
    } else {
    //push flutter栈
      pushContainer(settings);
    }
  }

这里的逻辑很简单,重点看下pushContainer方法,代码如下

 void pushContainer(BoostContainerSettings settings) {
    assert(settings.uniqueId != _onstage.settings.uniqueId);
    assert(_offstage.every((BoostContainer container) =>
        container.settings.uniqueId != settings.uniqueId));
    //将当前页面的add
    _offstage.add(_onstage);
    //需要push的页面容器创建
    _onstage = BoostContainer.obtain(widget.initNavigator, settings);

    setState(() {});
    //观察者回调
    for (BoostContainerObserver observer in FlutterBoost
        .singleton.observersHolder
        .observersOf<BoostContainerObserver>()) {
      observer(ContainerOperation.Push, _onstage.settings);
    }
    Logger.log('ContainerObserver#2 didPush');
  }


flutter的容器的创建,调用setState方法,跟随进去,发现一个东西,一般flutter页面开发都用不着,就是SchedulerBinding,这里有个文章介绍,这里我简单讲解下,我们可以想想flutter的启动流程中,肯定是有个调度节点,例如:Widget什么时候处理build,什么时候处理动画计算等,就是调度。我们如果要写框架,肯定是要对flutter的调度 得清楚,这样才能写出闲鱼这样的混合栈方案,代码如下

@override
  void setState(VoidCallback fn) {
    Logger.log('BoostContainerManager setState');
    if (SchedulerBinding.instance.schedulerPhase ==
        SchedulerPhase.persistentCallbacks) {
        //主要在下一帧之前,做一些清理工作或者准备工作
      SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
        Logger.log('BoostContainerManager persistentCallbacks');
        _refreshOverlayEntries();
      });
    } else {
      Logger.log('BoostContainerManager '+SchedulerBinding.instance.schedulerPhase.toString());
      _refreshOverlayEntries();
    }

    fn();
    //return super.setState(fn);
  }

如果当前调度的是SchedulerPhase.persistentCallbacks,那么就加一个回调,在persistent之后进行调用_refreshOverlayEntries方法,
SchedulerPhase.persistentCallbacks 是处理build/layout/paint工作

可以这么理解,SchedulerPhase.persistentCallbacks就是在搭建舞台,舞台搭建好了,那么表演者就可以上台表演了 即调用_refreshOverlayEntries方法

继续查看_refreshOverlayEntries方法,代码如下

 void _refreshOverlayEntries() {
    final OverlayState overlayState = _overlayKey.currentState;

    if (overlayState == null) {
      return;
    }

    if (_leastEntries != null && _leastEntries.isNotEmpty) {
      for (_ContainerOverlayEntry entry in _leastEntries) {
        entry.remove();
      }
    }

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

    assert(_onstage != null, 'Should have a least one BoostContainer');
    containers.add(_onstage);
    //一层层的entry覆盖上去
    _leastEntries = containers
        .map<_ContainerOverlayEntry>(
            (BoostContainer container) => _ContainerOverlayEntry(container))
        .toList(growable: false);

    overlayState.insertAll(_leastEntries);

    SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
      final String now = _onstage.settings.uniqueId;
      if (_lastShownContainer != now) {
        final String old = _lastShownContainer;
        _lastShownContainer = now;
        _onShownContainerChanged(old, now);
      }
      //将焦点切换至当前的BoostContainerState
      updateFocuse();
    });
  }

调用OverlayState的insertAll方法,将_leastEntries 覆盖上去,push flutter页面的讲解就到这人,pop其实也一样,将当前的页面栈弹出,当然也有特殊的业务处理,例如非当前的栈弹出,而是某个flutter页弹出,这里就不细讲,逻辑还是比较清晰好理解

其实本人在看完flutter的源码之后,对于BoostContainer.dart比较有疑问,其实是对其背后的对于Navigator和Overlay有疑问,BoostContainer要继承的是Navigator,这明明是个导航控制器,其实刚刚给出的文章里面讲得非常通俗易懂了。我自己疑问的原因主要是认为一个flutter app应该就只有一个Navigator,其实主要是flutter业务开发做多了而进去的误区。闲鱼的混合栈中的flutter页面栈管理就跟平常的flutter页面栈很不一样。其实最好的理解方式,自己写一个最简单的类似的flutter页面管理,然后再看那篇文章,就豁然开朗了。

容器讲解就到此了,解决疑问中的第一个问题

第一个:容器是怎么设计的?

适配层

适配层 只有原生才需要做相应的工作,看之前,想想如果要做适配层,要做哪些适配?
做过flutter业务开发,肯定在软键盘上面花过不少心思去做相应的界面适配工作~
的确,看原生代码里就有个XInputConnectionAdaptor.java的类,其实要看适配层,要花不少精力的,要弄清楚flutter的启动流程,然后重写FlutterView即XFlutterView,其实跟官方提供的FlutterView改动并不是很多


遗留问题:
因为 存在第一个打开的Flutter页面无法将数据传回起始页问题,
后来又去翻了下通道的相关代码,发现有这么一个flutter向原生的pageOnStart方法,类路径flutterProject/flutter_boost/android/src/main/java/com/idlefish/flutterboost/FlutterBoost.java,代码如下

 case "pageOnStart":
                {
                    Map<String, Object> pageInfo = new HashMap<>();

                    try {
                        IContainerRecord record = mManager.getCurrentTopRecord();

                        if (record == null) {
                            record = mManager.getLastGenerateRecord();
                        }

                        if(record != null) {
                            pageInfo.put("name", record.getContainer().getContainerUrl());
                            pageInfo.put("params", record.getContainer().getContainerUrlParams());
                            pageInfo.put("uniqueId", record.uniqueId());  
                        }

                        result.success(pageInfo);
                    } catch (Throwable t) {
                        result.error("no flutter page found!",t.getMessage(),t);
                    }
                }
                break;

看了下代码,应该就是第一个flutter页面的打开逻辑,但是在flutter的demo中并没发现,可能是以前版本留下的

内容来源于网络如有侵权请私信删除
你还没有登录,请先登录注册
  • 还没有人评论,欢迎说说您的想法!