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)生成运行产物
4、微服务架构
架构总览
微服务架构的核心是服务独立,经容器包裹之后平层又能相互交互,严格遵循层级关系,从上到下,各司其职,原生开发人员则可以更快适应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设计总览
以埋点服务作为样例,业务方作为服务需求发起方发起埋点需求,调用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基础服务库。
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); \
}
- 编译期把模块名以及对应的别名注入到 section __DATA
- 加载image时搜索section __DATA注入模块类类的打了标记的方法
- 为每个模块创建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热修复、多媒体资源共享等方面的探索和验证...