Flutter Fair原理篇之Fair逻辑动态化通信实现

Fair 逻辑动态化,是对一期布局动态化的增强。为了实现逻辑动态化,我们当时考虑了多种方案,方案主要集中在这三个方面,一种是对google提供的JIT进行裁切,第二种是自定义解析引擎,第三种借助js的能力。

Flutter Fair原理篇之Fair逻辑动态化通信实现_第1张图片

下面主要讲一下几方面:

  • 架构的标准化
  • 通信协议的实现
  • js文件的加载与释放
  • 数据的绑定
  • 消息的分发
  • 第三方扩展(用户根据需要扩展更精彩的功能)

一、架构的标准化

当我们生成了布局文件和逻辑文件,接下来要做的是如何建立他们之间的联系,确切的说是如何建立两者之间的通信。为了方便资源的控制与分配,fair是对每一个逻辑js通过唯一的key确认,然后通过key-value的形式将逻辑文件临时储存在一个全局的集合中。因这个key在生成js的时候是不确定的,所以我们对key统一定义名称为#FairKey# ,在js发送到native侧之前,由dart侧全局替换该值为具体的key。这个key非常重要,在消息通信的时候,需要通过key获取具体的通信对象,js侧dart侧都是,原理如下图。

Flutter Fair原理篇之Fair逻辑动态化通信实现_第2张图片

下面是生成js文件的格式:

//该对象用于全局管理各个js对象
let GLOBAL = {};


...


//'#FairKey#'会在发送之前全局统一替换
GLOBAL['#FairKey#']={
        ...
    //转换之后的js内容,包含对应的变量和方法
        ...
}

二、通信协议的实现

js与布局文件的通信,本质上就是js与dart之间的通信,因为两者都是以native平台做依托,所以需要native作为消息的转发器,负责消息的分发。对于dart与native之间的通信,我们使用的是官方提供的message-channel与dart:FFI。message-channel主要有、BasicMessageChannel、MethodChannel、EventChannel,该通道主要用于异步通信,dart:FFI是官方提供的直接调用native c/c++代码的工具,主要用于同步通信。对于native与js之间的通信,我们则可以用注入方法的形式建立联系,native侧注入本地方法,那么js则可以调用该方法发送消息并获取结果值,而如果是js提供本地方法, 那native侧可以执行js中的方法获取js发送的结果。

2.1 格式定义

为了方便数据的统一处理,需要规定数据格式,数据的处理逻辑主要集中在js侧和dart侧,native侧只负责数据的转发,以及js的加载和释放。

{
    pageName:"对应的调用页名称,也就是js侧的#FairKey#",
    funcType:"调用类型,method,variable等",
    args:{
        //用户携带的参数,交由js侧处理
    }
}

2.2 通道创建

对于通道的创建,目前主要是用了三种,对于js的加载以及释放用的是MethodChannel,对于js-native之间的通信采用的是BasicMessageChannel和dart:FFI。之所以采用两种通道的原因,也是为了分离加载释放和消息发送的流程。

2.3 各侧接口定义

dart侧与js侧的联系,主要是涉及到同步和异步通信,所以在设计接口方法的时候也是做了区分。

Flutter Fair原理篇之Fair逻辑动态化通信实现_第3张图片

为了抹平平台的差异性,对于js侧的通信通过的是方法的注入,native侧只负责消息的转发,不做太多的逻辑,所以js侧只注入了一个方法,逻辑的处理交给js侧处理,方法会根据消息的字段type来执行具体的操作。

Flutter Fair原理篇之Fair逻辑动态化通信实现_第4张图片

三、JS文件的加载与释放

我们通过对dsl布局的解析生成widget树,用于UI的展示,对于里面的逻辑,我们放到js中去处理。所以我们需要涉及到js的加载,用于数据绑定,当页面销毁之后我们对应的也需要释放掉js,降低对内存的消耗,同时需要防止出现重复加载问题。

3.1 js文件的加载流程

  1. js文件的加载主要分为以下几步:
  2. 读取本地或者网络的js数据
  3. 对js数据包装成固定格式的json字符串
  4. 通过method-channel调用native端的加载方法
  5. native端js引擎加载成功之后返回消息通知dart端,js加载完成。

Flutter Fair原理篇之Fair逻辑动态化通信实现_第5张图片

3.2 js文件的释放流程

当页面销毁之后,需要销毁js文件,消耗的步骤如下:

  1. 当widget的onDispose回调被执行的时候,调用dart侧的释放方法
  2. dart通过method-channel调用native端的释放方法
  3. native调用js侧方法,js会根据pageName获取制定的js对象,并将相关对象从集合中移除,后续回收就交给js引擎处理了
  4. js移除成功之后,native侧移除相关js-Object,完成释放
  5. 释放成功之后通知dart侧,释放过程完成。

Flutter Fair原理篇之Fair逻辑动态化通信实现_第6张图片

四、数据的绑定

当js加载完成之后,接下来的工作就是需要做数据的绑定了。对于数据的绑定,绑定的是变量和方法,本文只写方法的调用,对于调用方式会在接下来的文章进行分享。在js加载完成之后,dart端会调用getBindVariableAndFunc方法获取js侧的数据,数据包包括js侧方法名称、变量名称、变量名对应的值。每一个FairWidget都会有一个固定的key,在js侧也会有一个对应的key,dart给js侧发送消息的时候,会通过key获取js侧的对象,然后进行相关操作。dart中的布局被解析成给各个节点,对于里面的js逻辑则会解析成js,js代码中会对应具体的变量以及方法,所以我们要做的是建立dart与js之间的通信,用于获取变量值,执行相关方法等。所有的数据都是在js侧处理,只有js调用setState的方法的时候才会将数据发送到dart侧,刷新页面数据。

获取js侧的数据格式如下:

{
    "func":["方法A名","方法B名"],
    "variable":{
        "变量a名":"变量a值",
        "变量b名":"变量b值",
        "变量c名":"变量c值",
           ... 
    }
}

我们获取到这些数据之后就会将数据绑定到RuntimeFairDelegate中去,当实际调用的时候从里面根据名称取出来对应数据绑定就可以了。

五、消息分发

消息的分发主要是指dart-js两者消息能够正确的发送和接收,而保证消息能够正确的接收发送,是通过FairKey来确定的。消息的发送分两部分,一部分是js发送消息给dart,当用js侧调用setState和调用invokeFlutterCommonChannel的时候会发送消息给dart侧,其中invokeFlutterCommonChannel是native侧注入的方法,setState是js侧注入的方法,是对invokeFlutterCommonChannel的包装;另一部分是dart发送消息给js侧,例如获取js侧绑定数据、调用js侧方法的时候,通过Runtime中的方法即可通信。对于dart侧方法的调用,会有同步调用和异步调用。异步调用的实现方式是通过message-channel,同步则通过dart:FFI的形式。 

5.1 通信过程

Flutter Fair原理篇之Fair逻辑动态化通信实现_第7张图片

js通信dart(js发送消息到dart主要是异步通信)通过调用js侧invokeFultterChanne方法,将消息发送到native侧,native再对消息转发至dart侧。

5.2 Dart侧消息分发

当dart侧接收到js发送的消息的时候,dart侧会对消息分类,然后发送到正确的目标。对与dart侧消息的接收主要是在message handler的中分发消息,具体分发过程是根据消息的funcName字段作区分,来自js侧的消息目前只有两种,一种是js侧拓展发送过来的消息,第二种是setState发送过来的消息。当funcName名称为invokePlugin的时候是用户拓展模块发来的消息,对setState发送过来的消息,会根据pageName分发到指定的FairWidget中去。

Flutter Fair原理篇之Fair逻辑动态化通信实现_第8张图片

六、三方拓展

js转dart的时候,有些功能是js不支持的,例如dart端用的通信是Dio,权限调用,拍照,相册选择,那么js转换的时候是有问题的,会因为没有找到对应的类而报错,js只做逻辑处理所以对于这些功能需要用户自定义封装,下面是封装基本流程:

6.1 封装Dart侧

封装dart侧,需要继承IFairPlugin接口,并实现里面的getRegisterMethods方法,如下所示。

//实现IFairPlugin的getRegisterMethods方法,暴露出对应js侧的方法
class FairPhotoSelector extends IFairPlugin {
  Future getPhoto(dynamic map) async {
    //具体的逻辑实现,
    return Future.value();
  }
  @override
  Map getRegisterMethods() {
    var functions = {};
    //用户需要注册方法,这个方法与js侧对应
    functions.putIfAbsent('getPhoto', () => getPhoto);
    return functions;
  }
}

6.2 封装JS侧

js侧的封装比较简单,只需要调用invokeFlutterCommonChannel方法传递给Native,然后再传递给Dart侧,如下所示。

let FairPhotoSelectorCallback = {};
let FairPhotoSelector = function () {
        return {
        getPhoto: function (req) {
            //调用改方法,将包装的消息发送到dart侧
            invokeFlutterCommonChannel(JSON.stringify(reqMap), function (resp) {
                //处理dart端返回的请求结果
                    ...
                //消费完之后及时移除回调
                FairPhotoSelectorCallback[respCallId] = null;
            });
        },


    };
};

6.3 注册

在fair_basic_config.json中注册我们自定义封装。

// fair_basic_config.json中注册
{
  "plugin": {
      ...
    "fair_photo": "assets/plugin/sample_fair_photo_selector.js"
  }
}
//main函数中注册用户拓展
void main() {
  WidgetsFlutterBinding.ensureInitialized();
  FairApp.runApplication(
    _getApp(),
    plugins: {
        ...
      'FairPhotoSelector': FairPhotoSelector(),
    },
  );
}

6.4 基本使用

完成上述操作之后,接下来我们就可以使用自定义的封装插件,如下。

//开始使用
FairPhotoSelector().getPhoto({
  //pageName为固定格式,方便转换成js之后替换相关值
  'pageName': '#FairKey#',
  'args': {
    'type': 'album',
    'success': (resp) {
      picUrl = resp;
      setState(() {});
    },
    'failure': () {
      //用户获取图片失败
    },
  }
});

基本原理就是js将js侧参数包装,通过invokeFlutterCommonChannel方法通知到native侧,native侧再通过messagechannel将消息发送到dart侧,dart侧处理完之后返回相关数据到js侧,js侧接收dart返回的数据,然后执行接下来的逻辑。消息的发送也是通过自定义的消息通道发送,dart会根据消息中的func的值判断,如果是值为invokePlugin,那么则会通过FairPluginDispatcher分发给指定拓展类,其工作流程图如下。

Flutter Fair原理篇之Fair逻辑动态化通信实现_第9张图片

你可能感兴趣的:(flutter)