sparrow : Flutter 微服务架构开发实践

avatar

​1、为什么要实践 Flutter

在讲Flutter之前我们先来讲一下跨平台技术,大概从2010年至今市面上先后出现了Hybrid、React Native、Weex、Flutter等相关的跨平台技术方案,它们解决的就是研发效率、一致的性能体验、动态化等App应用关心的核心问题,结合哈啰现状,多条业务线并存、本地生活方向的业务发展以及公司倡导开发提效的要求,刚刚提到的核心问题的解决就显得极为迫切。选择Flutter是因为同时运行在两大主流移动操作系统上,其像素级别的还原,保证了不同平台的UI强一致性。(hot reload/attach Debug)一整套机制保证开发的高效。

2、业务特点和技术上的痛点

哈啰出行主App包体随着业务的增加而不端变大,包体大小、动态库过多等架构治理问题迫在眉睫,公司本地生活方向转型发展后续业务可能出现爆发式增长,公司尝试Flutter已经有很长一段时间,目前开发流程管理混乱,各个业务线对Flutter深入程度不统一以及积重难返的native的开发思维,技术栈没有统一,各种代码风格习惯并存,导致Flutter并没有给整个移动业务线有明显的提效的作用,所以为了各个业务的稳健发展,构建Flutter微服务架构统一Flutter开发行为避免无效开发,而且容器化的微架构也能加速业务变现,加快技术的快速转型、降低技术成本、实现多端业务实现的一致性...

3、Flutter混编工程结构

Flutter工程目前有四种类型,app、module、plugin、package

  • app : 标准的Flutter App工程,包含标准的Dart层与Native平台层
  • module : Flutter壳工程,目的就是将所有Flutter组件合并在一起打包,生成aar、pod供app工程引用
  • plugin : 三端混编的Flutter组件,供module引用
  • package : 纯dart端代码,Flutter组件,供module引用

Flutter工程类型的不同取决于编译产物的不同,Flutter混编工程是App Project最终接入Flutter module也就是通过引入App.framework(iOS)、vm/isolate_snapshot_data/instr(Android)生成运行产物 Android App Prolect.png

4、微服务架构

架构总览

image.png 微服务架构的核心是服务独立,经容器包裹之后平层又能相互交互,严格遵循层级关系,从上到下,各司其职,原生开发人员则可以更快适应dart相关开发,开发人员可以先从最熟悉的原生平台层、协议层、Channel 层往上一步一步熟悉开发,直到熟悉 Flutter 代码后,就可以深入 UI开发及相关业务开发。

哈啰内部自身定制一套Flutter Engine(基于1.12.13+hotfix.9),引擎层的中间件服务,如计算包大小、无痕埋点、多媒体资源复用,由于Flutter去除了dart的反射,许多功能需要以定制Flutter Engine在内部实现。

5、Sparrow是什么

sparrow是哈啰出行致力于解决Flutter开发流程管理混乱,各个业务线对Flutter深入程度不统一以及积重难返的native的开发思维,技术栈没有统一,各种代码风格习惯并存而构建的Flutter微服务容器架构。对外暴露Api接口层,管理约束服务层,内部优化引擎层现有逻辑,优化消息传输,提高Flutter内部版本的稳定性。

6、Sparrow做什么

隔离的混合工程体系,规避多端差异 统一标准的业务架构,提供一致的基础服务定义 高效复用基础中间件设施,内聚native基础服务 优化通道传输,提高引擎稳定性,扩展引擎现有功能 Android端动态化支持,iOS端暂不支持动态化

管理公共UI组件,动态列表,bloc以及状态管理

7、Sparrow设计与实现

7.1、sparrow设计总览

image.png

以埋点服务作为样例,业务方作为服务需求发起方发起埋点需求,调用plume提供的容器api接口:

ReportAnalytics.reportAnalytics(categoryId,eventId,params);

plume package内部将函数ReportAnalytics.reportAnalytics()转换为一组执行意图参数信息:path(action:/track/reportAnalytics)、nativeParams(categoryId,eventId,params),通过sparrow模型转换并选择合适的methodChannel发送埋点服务reportAnalytics action,sparrow内部会抉择此服务调用是否为dart服务或者为native基础服务,随后再在native服务转换层利用反射或者编译期注入找到合适的类执行reportAnalytics动作,native服务转换实现服务搜索、传输模型转换等相关操作,核心思路就是把path(action:/track/reportAnalytics)通过native的能力找到合适的执行者,确保不影响现有native基础服务库。 image.png

7.2、sparrow plugin设计

Flutter 有个核心设计思想,便是Everything's a widget,即一切即Widget。在Flutter的世界里,包括views,view controllers,layouts等在内的概念都建立在Widget之上。widget是Flutter功能的抽象描述。sparrow plugin内部有三个概念:页面路由、功能service、动态列表卡片(ListCell、GridCell),对应于开发当中的页面、功能、视图,sparrow的核心也就是接受动态注入的这些页面、功能、视图统一管理,输出相应的执行流程,所以内部通过简单的函数转换把相应的概念转换为对外一直的widget:

/// 服务转换函数定义,通过构建[ServiceSettings]模型,经过ServiceBuilder转换成[Widget]统一管理
typedef ServiceBuilder = Widget Function(ServiceSettings settings);

/// 动态列表Cell卡片转换函数定义,通过构建[CardSettings]模型,CardBuilder[Widget]统一管理
typedef CardBuilder = Widget Function(CardSettings settings);

/// 页面路由转换函数定义,通过构建[RouteSettings]模型,RouteBuilder[Widget]统一管理
typedef RouteBuilder = Widget Function(RouteSettings settings);

sparrow只需要管理ServiceBuilder、CardBuilder、RouteBuilder三种Widget即可,由于这三种Widget是由xxxSettings模型驱动,Builder包含了相关的单一功能所需要的全部信息。

下文重点以ServiceBuilder管理为例,描述以下管理流程:

  • 服务注册(iOS、Android、dart)
  • 函数执行转发
  • 参数模型转换
  • 通道数据mock
  • native端管理搜索Service

7.2.1、服务注册

dart端:

///注册
registerServiceBuilder('action:/bike/getOrderList', 
        (settings) => BikeSerive(serviceSettings: settings));
​
///实现
Future<dynamic> doAction() async{
  if ("getOrderList" == this.serviceSettings.action) {
   /// 执行action逻辑
  }
  return Future.value('callback...');
}

iOS端:

@SparrowMethodMod(PlatformServive,platform)
@Sparrow_export_method(@selector(getUser:))
- (void)getUser:(NSDictionary *)user{
    JYFlutterResult callback = user[kSparrowMethodCallBackKey];
    callback(@"native_callback:用户数据模型。。。");
}

Android端:

@SparrowService("platform")
public class PlatformService implements BaseFlutterService {
    @SparrowMethod("getUser")
    public void getUserInfo(ServiceEntity serviceEntity) {
        serviceEntity
        .getCallback()
        .onSuccess("native_callback:用户数据模型。。。");
    }
}

iOS端的@SparrowMethodMod(PlatformServive,platform)以及Android端的@SparrowService("platform")中的platform属于模块服务别名,用于标识服务所属模块,native端模块服务实现如果出现散落各处,也是通过此别名进行内聚。

dart端使用:

//通过返回值
var orderList = SparrowManager.doAction('action:/bike/getOrderList', actionParams);

//用户行为采集(action:/track/reportAnalytics)
await ReportAnalytics.reportAnalytics(categoryId,eventId,params);

//设备信息(action:/system/getSystemInfo)
var systemInfo = System.getSystemInfo();

7.2.2、函数执行转发

class Analytics {
  static Future<dynamic> reportAnalytics(
      {Map<String, String> params}
      ) async {
    var nativeParams = ...;
    return await SparrowManager
                 .doAction("action:/track/reportAnalytics", nativeParams);
  }
}

sparrow提供函数、url两种方式的服务调用,函数调用解决编译期检查,内部转换成方便管理的url调用,服务初始化采用懒加载形式对公共模型以及上下文进行集中处理。

7.2.3、通道数据mock

var args = {
  'method': 'getUserInfo'
};
​
var res = {
  "code":SparrowCode.Pass.index,
  "msg":"msg",
  "data": {"name":"mock数据"}
};
///更改指定参数的服务调用,返回指定res    
ChannelMockUtil.instance.createMock(args, res);

更改指定参数的服务调用,返回指定res,在sparrow内部通过改写原有通道的methodCallHandler,在改写通道数据流向的同时建立备用通道,保持未mock数据正常传输。

7.2.4、native端管理搜索Service

iOS端的SparrowInvocationConfig管理着一组模块服务,编译器通过注解的形式:

//1、编译期把模块注入到__DATA
#define SparrowMethodDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))
​
#define SparrowMethodMod(name,alias) \
class NSObject; char * k##name##_mod SparrowMethodDATA(SparrowService) = "{ \""#name"\" : \""#alias"\"}";
​
//2、加载image时搜索__DATA注入类的指定方法
#define Sparrow_export_method(method) SPARROW_EXPORT_METHOD_INTERNAL(method,fm_export_method_)
​
#define SPARROW_EXPORT_METHOD_INTERNAL(method, token) \
class NSObject; \
+ (NSString *)FM_CONCAT_WRAPPER(token, __LINE__) { \
    return NSStringFromSelector(method); \
}
  1. 编译期把模块名以及对应的别名注入到 section __DATA
  2. 加载image时搜索section __DATA注入模块类类的打了标记的方法
  3. 为每个模块创建SparrowInvocationConfig,供dart传入数据命中

Android端使用AutoService注解在编译期将SparrowInvocationService标识的类写入到META-INF/services/com.hellobike.sparrow.bases.ISparrowInvocationService 文件中,并在运行时读出,供dart传入数据命中。

7.3、plume package设计

plume的设计初衷是把基础服务从sparrow解耦出来,plume包含了一个native App提供给dart端所有的基础服务,在plume内部对传入数据进行计算加工,封闭服务实现细节。原生app内部通过Module、Service功能划分,并通过pod、maven管理,功能相对集中独立且完整,plume会通过sparrow提供的通道跟这些模块服务一一对应,从而隐藏iOS、Android的实现细节,统一标准的业务架构,提供一致的基础服务定义。

7.3.1、基础服务总览

目前哈啰出行基础服务分为以下项,通过pod、maven管理,功能相对集中独立且完整,plume会通过sparrow提供的通道跟这些服务一一对应,从而隐藏iOS、Android的实现细节,统一标准的业务架构,提供一致的基础服务定义。

7.4、native服务转换层设计

native服务转换层设计核心问题把dart传入path携带的信息二维展开并命中到合适的方法上、通过模型转换解决层与层的依赖问题。 7.4.1 iOS native端设计 服务的实现部分通过硬编码是很难处理的,因此做了一层转换处理。 以网络服务为例子:需要实现一个post函数,供dart层使用:

@Sparrow_export_method(@selector(post:))
- (void)post:(NSDictionary *)dict{
    //实现
}

转换层处理之后:

@Sparrow_export_method(@selector(post:params:success:failure:))
- (void)post:(NSString *)url
      params:(NSDictionary *)params
     success:(SparrowNetworkSuccess)success
     failure:(SparrowNetworkFailure)failure {
     //实现
}

转换层原理:

  • 定义一层协议:
@protocol SparrowNetworkServive <NSObject>
- (void)post:(NSString *)url
      params:(NSDictionary *)params
     success:(SparrowNetworkSuccess)success
     failure:(SparrowNetworkFailure)failure;
@end

需要实现网络层服务,需要继承 SparrowNetworkServive 协议。

  • 创建转发类: SparrowNetworkInvocation 类使用@SparrowServiceProtocol宏和SparrowNetworkServive协议进行绑定 SparrowNetworkInvocation类在invocation函数中显式调用post函数。 容器做了一层强代码约束,降低错误率。
@SparrowServiceProtocol(SparrowNetworkServive,SparrowNetworkInvocation)
@implementation SparrowNetworkInvocation
- (id)invocation:(SEL)action target:(NSObject *)target params:(NSDictionary *)params {
    NSObject<SparrowNetworkServive> *servive = (NSObject<SparrowNetworkServive> *)target;
    if (action == @selector(post:params:success:failure:)) {
        NSString *url = [self _url:params];
        JYFlutterResult result = params[kSparrowMethodCallBackKey];
        [servive post:url
               params:[self _params:params]
              success:[self _successResult:result]
              failure:[self _failureResult:result]];
        return @(0);
    }
}
@end

7.4.2 Android native端设计

使用@SparrowInvocationService("network")标记当前类是转换service,其中的"network"表示对@SparrowService标识的service进行转换;需要实现ISparrowInvocationService接口,以便对转换层接口进行统一。

@Override
public void invoke(Object targetObj, String action, final ServiceEntity serviceEntity){}

invoke即是转换服务类必须实现的方法,其中的targetObj是被转换的service实例,这里需要自定义接口,让该service实例实现,转换层将dart传递过的参数等进行转换后通过该自定义接口调起service对应方法;action是需要执行的动作;serviceEntity对参数、回调、context等进行包装。

// 标识为转换service
@SparrowInvocationService("tangram")
public class SparrowTangramInvocation implements ISparrowInvocationService {
    @Override
    public void invoke(Object targetObj, String action, ServiceEntity serviceEntity) {
        // 被转换的service需要实现自定义接口
        ISparrowTangramService service = (ISparrowTangramService)targetObj;
        Map<String,String> params = serviceEntity.getParams();
        // 根据action判断dart层的调用动作
        if (Actions.getABTestIsEnable.equals(action)) {
            //参数处理
            //调起接口方法
            service.getABTestIsEnable(xx,xx);
        }
    }
}

8、后记

容器技术的出现改变了软件交付的思维,在解决研发效率、一致的性能体验、动态化的同时,sparrow也承载Flutter业务的稳定性指标、安全高效的指标,后续sparrow也就继续深入Android热修复、多媒体资源共享等方面的探索和验证...