flutter_boot android和flutter源码阅读(排版后)

版本号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中并没发现,可能是以前版本留下的

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343