原创:架构部-移动端  哈啰技术团队

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)生成运行产物

微服务架构的核心是服务独立,经容器包裹之后平层又能相互交互,严格遵循层级关系,从上到下,各司其职,原生开发人员则可以更快适应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设计总览


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:
1/// 服务转换函数定义,通过构建[ServiceSettings]模型,经过ServiceBuilder转换成[Widget]统一管理
2typedef ServiceBuilder = Widget Function(ServiceSettings settings); 3
4/// 动态列表Cell卡片转换函数定义,通过构建[CardSettings]模型,CardBuilder[Widget]统一管理
5typedef CardBuilder = Widget Function(CardSettings settings); 6
7/// 页面路由转换函数定义,通过构建[RouteSettings]模型,RouteBuilder[Widget]统一管理
8typedef RouteBuilder = Widget Function(RouteSettings settings);

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

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

  • 服务注册(iOS、Android、dart)

  • 函数执行转发

  • 参数模型转换

  • 通道数据mock

  • native端管理搜索Service

7.2.1、服务注册

dart端:
1///注册 
2registerServiceBuilder('action:/bike/getOrderList',
3        (settings) => BikeSerive(serviceSettings: settings)); 
4
5 ///实现
6 Future<dynamicdoAction(async{ 7 if ("getOrderList" == this.serviceSettings.action) {
8    /// 执行action逻辑
9   }
10 return Future.value('callback...');
11 }
iOS端:
1@SparrowMethodMod(PlatformServive,platform)
2 @Sparrow_export_method(@selector(getUser:)) 
3- (void)getUser:(NSDictionary *)user
4{ JYFlutterResult callback = user[kSparrowMethodCallBackKey];
5  callback(@"native_callback:用户数据模型。。。");
6 }

Android端:

1@SparrowService("platform")
2public class PlatformService implements BaseFlutterService { 3@SparrowMethod("getUser")
4 public void getUserInfo(ServiceEntity serviceEntity) { 5serviceEntity 6.getCallback() 7.onSuccess("native_callback:用户数据模型。。。");
8 }
9}
        iOS端的@SparrowMethodMod(PlatformServive,platform)以及Android端的@SparrowService("platform")中的platform属于模块服务别名,用于标识服务所属模块,native端模块服务实现如果出现散落各处,也是通过此别名进行内聚。

dart端使用:

1//通过返回值
2var orderList = SparrowManager.doAction('action:/bike/getOrderList', actionParams);
3
4//用户行为采集(action:/track/reportAnalytics)await 5ReportAnalytics.reportAnalytics(categoryId,eventId,params);
6
7 //设备信息(action:/system/getSystemInfo)
8var systemInfo = System.getSystemInfo();

7.2.2、函数执行转发

1class Analytics {
2static Future<dynamic> reportAnalytics(
3 {Map<String, String> params}
4 ) async {
5 var nativeParams = ...;
6 return await SparrowManager
7                 .doAction("action:/track/reportAnalytics", nativeParams);
8 }
9}

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

7.2.3、通道数据mock

1var args = {
2 'method': 'getUserInfo'
3};
4
5 var res = {
6 "code":SparrowCode.Pass.index,
7 "msg":"msg",
8 "data": {"name":"mock数据"}
9};
10///更改指定参数的服务调用,返回指定res
11 ChannelMockUtil.instance.createMock(args, res);

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

7.2.4、native端管理搜索Service

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

1//1、编译期把模块注入到__DATA
2#define SparrowMethodDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))
3
4 #define SparrowMethodMod(name,alias) \
5class NSObject; char * k##name##_mod SparrowMethodDATA(SparrowService) = "{ \""#name"\" : \""#alias"\"}";
6
7 //2、加载image时搜索__DATA注入类的指定方法
8#define Sparrow_export_method(method) SPARROW_EXPORT_METHOD_INTERNAL(method,fm_export_method_)
9
10 #define SPARROW_EXPORT_METHOD_INTERNAL(method, token) \ 11classNSObject; \
12+ (NSString *)FM_CONCAT_WRAPPER(token, __LINE__) { \
13 return NSStringFromSelector(method); \
14}
  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层使用:

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

转换层处理之后:

1@Sparrow_export_method(@selector(post:params:success:failure:))
2- (void)post:(NSString *)url
3 params:(NSDictionary *)params
4 success:(SparrowNetworkSuccess)success
5 failure:(SparrowNetworkFailure)failure {
6 //实现
7}

转换层原理:

  • 定义一层协议:

@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

1@SparrowServiceProtocol(SparrowNetworkServive,SparrowNetworkInvocation)
2@implementation SparrowNetworkInvocation
3- (id)invocation:(SEL)action target:(NSObject *)target params:(
4NSDictionary *)params { NSObject<SparrowNetworkServive> *servive = (NSObject<SparrowNetworkServive> *)target;
5 if (action == @selector(post:params:success:failure:)) {
6 NSString *url = [self _url:params];
7 JYFlutterResult result = params[kSparrowMethodCallBackKey];
8 [servive post:url
9 params:[self _params:params]
10 success:[self _successResult:result]
11 failure:[self _failureResult:result]];
12 return @(0);
13 }
14}
15@end

7.4.2 Android native端设计

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

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

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

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

8后记

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