前言
EasyReact 是基于响应式编程范式的客户端开发框架,开发者可以轻松解决客户端的异步问题。
目前 EasyReact 已经在美团和大众点评客户端的一些业务中实践了一年多。最近,我们决定开源项目 iOS Objective-C 语言部分希望帮助更多的开发者不断探索更广泛的业务场景,也欢迎更多的社区开发者加强 EasyReact 的功能。
GitHub 项目地址见 https://github.com/meituan/EasyReact。
背景
美团 iOS 客户端团队在行业早期使用响应式来解决项目问题。因此,我们引入了 ReactiveCocoa 函数响应框架(相关实践,参考之前的系列博客)。随着业务的快速扩张和团队的拆分和变化,ReactiveCocoa 在解决异步问题的同时,也带来了新的挑战,总结如下:
高学习门槛容易出错,调试困难,风格不统一既然响应编程带来了这么多麻烦,我们应该放弃响应编程,用更容易理解的目标编程来解决问题吗?这从移动开发的特点开始。
开发移动端的特点
客户端程序本身充满了异步场景。客户端的主要逻辑是从视图中处理控件事件,通过网络获取后端内容,然后显示在视图上。事件和网络的处理是异步行为。
一般客户端程序启动网络请求后,程序将继续异步执行,等待网络资源的获取。通常,我们还需要设置一定的标志位置,并显示一些加载指示器来等待视图。但当网络获取时,通知,UI 事件和定时器都会改变状态,导致状态混乱。我们有没有遇到过:忙碌的指示器没有正确隐藏,页面显示的字段被错误地显示为旧值,甚至一个页面的几个部分的信息不同步?
单个问题看似简单,但如今,随着客户端的快速发展,包括美团在内的许多公司的代码行数已经超过了100万。业务逻辑越来越复杂,维护状态本身就成了一个大问题。响应编程是解决这个问题的一种手段。
响应式编程的相关概念
响应编程是一种基于数据流编程的编程范式。iOS 客户开发的学生必须了解 KVO 这一系列 API。KVO 帮助我们分离属性的变化和变化后的处理,大大简化了我们的更新逻辑。响应编程更生动地反映了这一优势,可以简单地理解为,在一个对象的属性发生变化后,另一系列对象的属性发生了变化。
最简单的例子是电子表格,Excel 和 Numbers 中单元格公式就是一个响应的例子。我们只需要关心单元格和单元格之间的关系,而不需要关心当单元格发生变化时需要如何处理其他单元格。程序的写作提前到事件发生,因此响应编程是一种声明编程。它帮助我们更加关注数据流的关系,而不是数据变化时的处理。
简单的响应编程,如电子表格中的公式和 KVO 很容易理解,但为了 Objective-C 支持语言中的响应特性,ReactiveCocoa 利用函数响应编程实现响应编程框架。函数编程是学习路径陡峭的主要原因。在函数编程的世界里,一切都很复杂。这些复杂的概念,如 Immutable、Side effect、High-order Function、Functor、Applicative、Monad 等等,让很多开发者望而却步。
防不胜防的错误
函数编程主要使用高级函数来解决问题Objective-C 在语言中使用Block 主要处理。Objective-C 使用自动引用计数(ARC)为了管理内存,一旦出现循环引用,程序员需要主动打破循环引用。Block 闭包捕获变量最容易形成循环引用。weakify-strongify 会导致早期释放,无脑不使用 weakify-strongify 会引起循环引用。即使是老手在使用过程中也难免会出错。
另外,ReactiveCocoa 框架为了方便开发者更快地使用响应编程,Hook 了很多 Cocoa 框架中的功能,如 KVO、Notification Center、Perform Selector。其它框架一旦在 Hook 在与之冲突的过程中,后续问题的调查变得非常困难。
调试难度
使用高级函数进行函数响应编程也带来了另一个问题,即由大量嵌套闭包函数引起的调用栈深度问题。ReactiveCocoa 2.5 版本中,简单的 5 次变换,其调用栈深度甚至达到 50 层(见下图)。
ReactiveCocoa 的调用栈
仔细观察调用栈,我们发现整个调用栈的内容非常相似,很难发现问题。
此外,异步场景给调试增加了新的难度。很多时候,数据的变化是由其他队列发送的,我们甚至无法在调用栈中追溯数据变化的来源。
风格差异化
很多人在业内使用 FRP 框架解决 MVVM 结构中的绑定问题。在业务实践中,许多操作非常相似,可以泛化,这意味着脚手架工具可以自动生成。
但目前业内知名框架还没有提供相应的工具,最佳实践无法模板传递。这导致了 MVVM 和响应式编程,大家有了各自不同的理解。
EasyReact的初心
EasyReact 的诞生,其初衷是解决 iOS 工程实现 MVVM 架构但没有相应的框架支撑,导致风格不统一、可维护性差、开发效率低等问题。MVVM 绑定是最重要的功能之一,EasyReact 是为了使绑定和响应代码 Easy 起来。
其目标是让开发者简单地理解响应编程,并简单地利用响应编程的优势。
EasyReact依赖库介绍
EasyReact 以 为基础Objective-C 开发。而 Objective-C 是一种古老的编程语言,在 2014 苹果公司推出 Swift 编程语言之后,Objective-C 基本不再更新, Swift支持的 Tuple 类型和 ** 类型** p、filter 等方 ** 使代码更清晰易读。
在 EasyReact Objective-C在 版本的开发中,我们还衍生了一些周边库来支持这些新的代码技能和语法糖。这些周边库现在已经开源,可以独立于 EasyReact 使用。
EasyTuple
EasyTuple 使用宏构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构制构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构构Swift 的 Tuple 语法。使用 Tuple ,当需要传输一个简单的数据架构时,不需要手动创建相应的类别,就可以很容易地交给框架。
EasySequence
EasySequence 是给 的** 类型扩展的库可以清晰地表达给 ** 类型的迭代操作,这些迭代操作可以巧妙的技术用链式语法拼接。EasySequence 还提供了一系列 线程安全 和 weak 内存管理 ** 类型用于补充系统容器无法提供的功能。
EasyFoundation
EasyFoundation 是上述 EasyTuple 和 EasySequence 以及未来底层依赖库的统一包装。
用EasyReact解决以前的问题
EasyReact 因业务需要而诞生。首要任务是解决业务中的问题。让我们来看看这些问题是否已经解决了:
响应式编程的学习门槛
根据之前的分析,简单的响应编程并不是特别难以理解,而函数编程是导致高学习门槛的原因。EasyReact 使用众所周知的面向对象编程进行设计,想要理解代码,比函数编程容易得多。
此外,响应编程是基于数据流,流程将产生一个向前的流动网络图。在函数编程中,网络图是通过闭包捕获建立的,不利于图片的搜索和遍历。EasyReact 选择在框架中使用图的数据结构,将数据流向网络图抽象成有环图的节点和边缘。这样,框架可以在运行过程中随时查询节点和边缘之间的关系。详见 框架概述。
另外,已经熟悉 ReactiveCocoa对于 的学生,我们基本上实现了 的数据流动操作ReactiveCocoa API。详细内容可以参见 基本操作。更多的功能可以向我们提功能的 ISSUE,欢迎您提 Pull Request 共同建设 EasyReact。
避免无意中的错误
前面提到过 ReactiveCocoa 容易引起循环引用或提前释放的问题, EasyReact 如何解决这个问题?EasyReact 中的节点、边缘和监听者不使用闭包捕获,因此在转换和订阅中消除副作用(转换 block 或订阅 block 中闭包捕获),EasyReact 内存可自动管理。详见 内存管理。
除内存问题外,ReactiveCocoa 中的 Hook Cocoa 框架问题,在 EasyReact 通过回避处理。EasyReact 在整个计划中整个计划中最基本的数据流驱动部分,所以它本身就是 Cocoa 和 CocoaTouch 框架与系统 无关,在一定程度上避免了API 和其 Hook 造成冲突。这并不是指 Easy 系列不解决相应的部分,而是 Easy 系列希望以更标准化和约束的方式解决同样的问题,后续 Easy 系列其他开源项目将有更多这些具体需求的解决方案。
EasyReact 的调试
EasyReact 利用对象的持有关系和方法调用来实现响应中的数据流,便于在调用栈信息中找到数据传输关系。EasyReact 中,与前 进行ReactiveCocoa 同样的 5 次简单变换,其调用栈只有 15 层(见下图)。
EasyReact 的调用栈
观察后不难发现,调用栈的顺序恰好是变换行为。这是因为我们将每个操作定义为一个边的类型,使调用栈可以通过类名进行简单的分析。
为了方便调试,我们提供了 - [EZRNode graph] 方法。这种方法可以通过调用任何节点获得一段 GraphViz 程序的 DotDSL 描述字符串,开发者可以通过 GraphViz 观察节点之间的关系,更好地调查问题。
使用方法如下:
** cOS 安装 GraphViz 工具 brew install graphviz打印 -[EZRNode graph] 返回的字符串或 Debug 期间在 lldb 调用 -[EZRNode graph]获取结果字符串,输出并保存到文件中,如 test.dot用工具分析生成图像circo -Tpdf test.dot -o test.pdf && open test.pdf结果示例:
节点静态图
此外,我们还开发了一种带有录音屏幕的调试工具,可以动态查看应用程序中的所有节点和边缘,并将在后期开源。开发工具如下:
节点动态图
响应式编程风格的统一
EasyReact 帮助我们解决了许多问题。不幸的是,它不是银弹。在实际项目实施中,我们发现只有 EasyReact ,在发展风格上仍然很难统一每个人。当然,它的写作方法比 要好ReactiveCocoa 统一了很多,但是构建数据流的方式还是很多的。
因此,我们认为通过上层业务框架统一风格,即后续衍生项目 EasyMVVM 出生的原因,我们很快就会 EasyMVVM 开源。
EasyReact与其它框架相比EasyReact 自诞生以来,不可避免地要与其他现有的响应编程框架进行比较。下表对几个响应框架进行了一般比较:
在性能方面,我们也是 Objective-C 语言的 ReactiveCocoa 2.5 版本对应 Bench ** rk。
测试环境
** cOS High Sierra 10.13.5IDE:Xcode 9.4.1真机设备:iPhone X 256G iOS 11.4(15F79)测试对象
listener、 ** p、filter、flattenMap 等单阶操作combine、zip、merge 多点聚合操作同步操作作其中测试的规模为:
节点或信号个数 10 个触发操作次数 1000 次例如 Listener 方法有 10 个监听者,重复发送值 1000 次。
统计时间单位为 ns。
测试数据
重复上面的实验 10 次,得到数据平均值如下:
结果总结
ReactiveCocoa 平均耗时是 EasyReact 的 725.41%。
EasyReact 的 Swift 版本即将开源,届时会和 RxSwift 进行 Bench ** rk 比较。
EasyReact的最佳实践通常我们创建一个类,里面会包含很多的属性。在使用 EasyReact 时,我们通常会把这些属性包装为 EZRNode 并加上一个泛型。如:
// SearchService.h#import <Foundation/Foundation.h>#import <EasyReact/EasyReact.h>@inte ** ce SearchService : NSObject@property (nonatomic, readonly, strong) EZRMutableNode<NSString *> *param;@property (nonatomic, readonly, strong) EZRNode<NSDictionary *> *result;@property (nonatomic, readonly, strong) EZRNode<NSError *> *error;@end这段代码展示了如何创建一个 WiKi查询服务,该服务接收一个 param 参数,查询后会返回 result 或者 error。以下是实现部分:
// SearchService.m@implementation SearchService- (instancetype)init { if (self = [super init]) { _param = [EZRMutableNode new]; EZRNode *resultNode = [_param flattenMap:^EZRNode * _Nullable(NSString * _Nullable searchParam) { NSString *queryKeyWord = [searchParam stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; NSURL *url = [NSURL URLWithString:[NSString stringWithFor ** t:@"https://en. ** .org/w/api.php?action=query&titles=%@&prop=revisions&rvprop=content&for ** t=json&for ** tversion=2", queryKeyWord]]; EZRMutableNode *returnedNode = [EZRMutableNode new]; [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { if (error) { returnedNode.value = error; } else { NSError *serializationError; NSDictionary *resultDictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:&serializationError]; if (serializationError) { returnedNode.value = serializationError; } else if (!([resultDictionary[@"query"][@"pages"] count] && !resultDictionary[@"query"][@"pages"][0][@"missing"])) { NSError *notFoundError = [NSError errorWithDo ** in:@"com.example.service.wiki" code:100 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFor ** t:@"keyword '%@' not found.", searchParam]}]; returnedNode.value = notFoundError; } else { returnedNode.value = resultDictionary; } } }]; return returnedNode; }]; EZRIFResult *resultAnalysedNode = [resultNode if:^BOOL(id _Nullable next) { return [next isKindOfClass:NSDictionary.class]; }]; _result = resultAnalysedNode.thenNode; _error = resultAnalysedNode.elseNode; } return self;}@end在调用时,我们只需要通过 listenedBy 方法关注节点的变化:
self.service = [SearchService new];[[self.service.result listenedBy:self] withBlock:^(NSDictionary * _Nullable next) { NSLog(@"Result: %@", next);}];[[self.service.error listenedBy:self] withBlock:^(NSError * _Nullable next) { NSLog(@"Error: %@", next);}];self.service.param.value = @"mip ** p"; //should print search resultself.service.param.value = @"420v"; // should print error, keyword not found.使用 EasyReact 后,网络请求的参数、结果和错误可以很好地被分离。不需要像命令式的写法那样,在网络请求返回的回调中写一堆判断来分离结果和错误。
因为节点的存在先于结果,我们能对暂时还没有得到的结果构建连接关系,完成整个响应链的构建。响应链构建之后,一旦有了数据,数据便会自动按照我们预期的构建来传递。
在这个例子中,我们不需要显式地来调用网络请求,只需要给响应链中的 param 节点赋值,框架就会主动触发网络请求,并且请求完成之后会根据网络返回结果来分离出 result 和 error 供上层业务直接使用。
对于开源,我们是认真的
EasyReact 项目自立项以来,就励志打造成一个通用的框架,团队也一直以开源的高标准要求自己。整个开发的过程中我们始终保证测试覆盖率在一个高的标准上,对于接口的设计也力求完美。在开源的流程,我们也学习借鉴了 GitHub 上大量优秀的开源项目,在流程、文档、规范上力求标准化、国际化。
文档
除了 中文 README 和 英文 README 以外,我们还提供了中文的说明性质文档:
框架概述基本操作内存管理和英文的说明性质文档:
Framework OverviewBasic OperationsMemory Management后续帮助理解的文章,也会陆续上传到项目中供大家学习。
另外也为开源的贡献提供了标准的 中文贡献流程 和 英文贡献流程,其中对于 ISSUE 模板、Commit 模板、Pull Requests 模板和 Apache 协议头均有提及。
如果你仍然对 EasyReact 有所不解或者流程代码上有任何问题,可以随时通过提 ISSUE 的方式与我们联系,我们都会尽快答复。
行为驱动开发
为了保证 EasyReact 的质量,我们在开发的过程中使用 行为驱动开发。当每个新功能的声明部分确定后,我们会先编写大量的测试用例,这些用例模拟使用者的行为。通过模拟使用者的行为,以更加接近使用者的想法,去设计这个新功能的 API。同时大量的测试用例也保证了新的功能完成之时,一定是稳定的。
测试覆盖率
EasyReact 系列立项之时,就以高质量、高标准的开发原则来要求开发组成员执行。开源之后所有项目使用 codecov.io 服务生成对应的测试覆盖率报告,Easy 系列的框架覆盖率均保证在 95% 以上。
持续集成
为了保证项目质量,所有的 Easy 系列框架都配有持续集成工具 Travis CI。它确保了每一次提交,每一次 Pull Request 都是可靠的。
展望
目前开源的框架组件只是建立起响应式编程的基石,Easy 系列的初心是为 MVVM 架构提供一个强有力的框架工具。下图是 Easy 系列框架的架构简图:
未来开源计划
未来我们还有提供更多框架能力,开源给大家:
名称描述EasyDebugToolBox动态节点状态调试工具EasyOperation基于行为和操作抽象的响应式库EasyNetwork响应式的网络访问库EasyMVVMMVVM 框架标准和相关工具EasyMVVMCLIEasyMVVM 项目脚手架工具
跨平台与多语言EasyReact 的设计基于面向对象,所以很容易在各个语言中实现。我们也正在积极的在 Swift、Java、JavaScript 等主力语言中实现 EasyReact。
另外动态化作为目前行业的趋势,Easy 系列自然不会忽视。在 EasyReact 基于图的架构下,我们可以很轻松的让一个 Objective-C 的上游节点,通过一个特殊的桥接边连接到一个 JavaScript 节点,这样就可以让部分的逻辑动态下发过来。
结语
数据传递和异步处理,是大部分业务的核心。EasyReact 从架构上用响应式的方式来很好地解决了这个问题。它有效地组织了数据和数据之间的联系,让业务的处理流程从命令式编程方式,变成以数据流为核心的响应式编程方式。用先构建数据流关系再响应触发的方法,让业务方更关心业务的本质。使广大开发者从琐碎的命令式编程的状态处理中解放出来,提高了生产力。EasyReact 不仅让业务逻辑代码更容易维护,也让出错的几率大大下降。
团队简介
成威,项目的发起人,负责美团客户端新技术调研。国内函数式编程、响应式编程的爱好者,多年宣传和布道响应式编程实践并取得一定的成绩。
姜沂,项目的主要开发者。
秦宏,项目的主要开发者。
君阳,项目的早期开发者。
思琦,Easy 系列图标设计者,文档和代码翻译者。
志宇,参与了大部分的重构设计。
恩生,文档和代码翻译者。
姝琳,文档和代码翻译者。
如果你想近距离与我们的作者沟通、交流,请来GitChat,点击“免费预订”即可参与读者交流,报名链接。
招聘信息
美团平台业务研发中心诚招高级 iOS 工程师、技术专家。欢迎投递简历到 zangchengwei#http://meituan.com。一起写 Easy 系列。
也许你还想看
用Vue.js开发微信小程序:开源框架mpvue解析
Android热更新方案Robust开源,新增自动化补丁工具
Shield:支撑美团点评品类最丰富业务的移动端模块化框架开源了
http://weixin.qq.com/r/9HVSSg3EOFBHrUkp9yDm (二维码自动识别)
扫码咨询与免费使用
申请免费使用