美团客户端响应式框架 EasyReact 开源啦

前言

EasyReact 是一款基于响应式编程范式的客户端开发框架,开发者可使用此框架轻松地解决客户端的异步问题。php

目前 EasyReact 已在美团和大众点评客户端的部分业务中实践,而且持续迭代了一年多的时间。近日,咱们决定开源这个项目的 iOS Objective-C 语言部分,但愿可以帮助更多的开发者不断探索更普遍的业务场景,也欢迎更多的社区的开发者跟咱们一块儿增强 EasyReact 的功能。Github 的项目地址,参见 user-gold-cdn.xitu.io/2018/7/23/1…git

背景

美团 iOS 客户端团队在业界比较早地使用响应式来解决项目问题,为此咱们引入了 ReactiveCocoa 这个函数响应式框架(相关实践,参考以前的 系列博客)。随着业务的急速扩张和团队拆分变动,ReactiveCocoa 在解决异步问题的同时也带来了新的挑战,总结起来有如下几点:程序员

  1. 高学习门槛
  2. 易出错
  3. 调试困难
  4. 风格不统一

既然响应式编程带来了这么多的麻烦,是否咱们应该摒弃响应式编程,用更通俗易懂的面向对象编程来解决问题呢?这要从移动端开发的特色提及。github

移动端开发特色

客户端程序自己充满异步的场景。客户端的主要逻辑就是从视图中处理控件事件,经过网络获取后端内容再展现到视图上。这其中事件的处理和网络的处理都是异步行为。编程

通常客户端程序发起网络请求后程序会异步的继续执行,等待网络资源的获取。一般咱们还会须要设置必定的标志位和显示一些加载指示器来让视图进行等待。可是当网络进行获取的时候,通知、UI 事件、定时器都对状态产生改变就会致使状态的错乱。咱们是否也遇到过:忙碌指示器没有正确隐藏掉,页面的显示的字段被错误的显示成旧的值,甚至一个页面几个部分信息不一样步的状况?json

单个的问题看似简单,可是客户端飞速发展的今年,不少公司包括美团在内的客户端代码行数早已突破百万。业务逻辑愈发复杂,使得维护状态自己就成了一个大问题。响应式编程正是解决这个问题的一种手段。后端

响应式编程的相关概念

响应式编程是基于数据流动编程的一种编程范式。作过 iOS 客户端开发的同窗必定了解过 KVO 这一系列的 API。 KVO 帮助咱们将属性的变动和变动后的处理分离开,大大简化了咱们的更新逻辑。响应式编程将这一优点体现得更加淋漓尽致,能够简单的理解成一个对象的属性改变后,另一连串对象的属性都随之发生改变。api

响应式的最简单例子莫过于电子表格,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 架构但没有对应的框架支撑,而致使的风格不统1、可维护性差、开发效率低等多种问题。而 MVVM 中最重要的一个功能就是绑定,EasyReact 就是为了让绑定和响应式的代码变得 Easy 起来。

它的目标就是让开发者可以简单的理解响应式编程,而且简单的将响应式编程的优点利用起来。

EasyReact 依赖库介绍

EasyReact 先是基于 Objective-C 开发。而 Objective-C 是一门古老的编程语言,在 2014 年苹果公司推出 Swift 编程语言以后,Objective-C 已经基本再也不更新,而 Swift支持的 Tuple 类型和集合类型自带的 mapfilter 等方法会让代码更清晰易读。 在 EasyReact Objective-C 版本的开发中,咱们还衍生了一些周边库以支持这些新的代码技巧和语法糖。这些周边库现已开源,而且能够独立于 EasyReact 使用。

EasyTuple

EasyTuple 使用宏构造出相似 Swift 的 Tuple 语法。使用 Tuple 可让你在须要传递一个简单的数据架构的时,没必要手动为其建立对应的类,轻松的交给框架解决。

EasySequence

EasySequence 是一个给集合类型扩展的库,能够清晰的表达对一个集合类型的迭代操做,而且经过巧妙的手法可让这些迭代操做使用链式语法拼接起来。同时 EasySequence 也提供了一系列的 线程安全weak 内存管理的集合类型用以补充系统容器没法提供的功能。

EasyFoundation

EasyFoundation 是上述 EasyTupleEasySequence 以及将来底层依赖库的一个统一封装。

用 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 工具观察节点的关系,更好的排查问题。

使用方式以下:

  1. macOS 安装 GraphViz 工具 brew install graphviz

  2. 打印 -[EZRNode graph] 返回的字符串或者 Debug 期间在 lldb 调用 -[EZRNode graph] 获取结果字符串,并输出保存至文件如 test.dot

  3. 使用工具分析生成图像 circo -Tpdf test.dot -o test.pdf && open test.pdf

结果示例:

节点静态图

另外咱们还开发了一个带有录屏而且能够动态查看应用程序中全部节点和边的调试工具,后期也会开源。开发中的工具是这样的:

节点动态图

响应式编程风格上的统一

EasyReact 帮助咱们解决了很多难题,遗憾的是它也不是“银弹”。在实际的项目实施中,咱们发现仅仅经过 EasyReact 仍然很难让你们在开发的过程当中风格上统一块儿来。固然它从写法上要比 ReactiveCocoa 上统一了一些,可是构建数据流仍然有着多种多样的方式。

因此咱们想到经过一个上层的业务框架来统一风格,这也就是后续衍生项目 EasyMVVM 诞生的缘由,不久后咱们也会将 EasyMVVM 进行开源。

EasyReact 和其余框架的对比

EasyReact 从诞生之初,就不可避免要和已有的其余响应式编程框架作对比。下表对几大响应式框架进行了一个大概的对比:

项目 EasyReact ReactiveCocoa ReactiveX
核心概念 图论和面向对象编程 函数式编程 函数式编程和泛型编程
传播可变性
基本变换
组合变换
高阶变换
遍历节点 / 信号
多语言支持 Objective-C (其余语言开源计划中) Objective-C、Swift 大量语言
性能 较快
中文文档支持
调试工具 静态拓扑图展现和动态调试工具(开源计划中) Instrument

性能方面,咱们也和一样是 Objective-C 语言的 ReactiveCocoa 2.5 版本作了相应的 Benchmark。

测试环境

编译平台: macOS High Sierra 10.13.5

IDE: Xcode 9.4.1

真机设备: iPhone X 256G iOS 11.4(15F79)

测试对象

  1. listener、map、filter、flattenMap 等单阶操做
  2. combine、zip、merge 等多点聚合操做
  3. 同步操做

其中测试的规模为:

  • 节点或信号个数 10 个
  • 触发操做次数 1000 次

例如 Listener 方法有 10 个监听者,重复发送值 1000 次。

统计时间单位为 ns。

测试数据

重复上面的实验 10 次,获得数据平均值以下:

name listener map filter flattenMap combine zip merge syncWith
EasyReact 1860665 30285707 7043007 7259761 6234540 63384482 19794457 12359669
ReactiveCocoa 4054261 74416369 45095903 44675757 209096028 143311669 13898969 53619799
RAC:EasyReact 217.89% 245.71% 640.29% 615.39% 3353.83% 226.10% 70.22% 433.83%

性能测试结果

结果总结

ReactiveCocoa 平均耗时是 EasyReact 的 725.41%。

EasyReact 的 Swift 版本即将开源,届时会和 RxSwift 进行 Benchmark 比较。

EasyReact的最佳实践

一般咱们建立一个类,里面会包含不少的属性。在使用 EasyReact 时,咱们一般会把这些属性包装为 EZRNode 并加上一个泛型。如:

// SearchService.h

#import <Foundation/Foundation.h>
#import <EasyReact/EasyReact.h>

@interface 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 stringWithFormat:@"https://en.wikipedia.org/w/api.php?action=query&titles=%@&prop=revisions&rvprop=content&format=json&formatversion=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 errorWithDomain:@"com.example.service.wiki" code:100 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"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 = @"mipmap"; //should print search result
self.service.param.value = @"420v"; // should print error, keyword not found.
复制代码

使用 EasyReact 后,网络请求的参数、结果和错误能够很好地被分离。不须要像命令式的写法那样在网络请求返回的回调中写一堆判断来分离结果和错误。

由于节点的存在先于结果,咱们能对暂时尚未获得的结果构建链接关系,完成整个响应链的构建。响应链构建以后,一旦有了数据,数据便会自动按照咱们预期的构建来传递。

在这个例子中,咱们不须要显式地来调用网络请求,只须要给响应链中的 param 节点赋值,框架就会主动触发网络请求,而且请求完成以后会根据网络返回结果来分离出 result 和 error 供上层业务直接使用。

对于开源,咱们是认真的

EasyReact 项目自立项以来,就励志打形成一个通用的框架,团队也一直以开源的高标准要求本身。整个开发的过程当中咱们始终保证测试覆盖率在一个高的标准上,对于接口的设计也力求完美。在开源的流程,咱们也学习借鉴了 Github 上大量优秀的开源项目,在流程、文档、规范上力求标准化、国际化。

文档

除了 中文 README英文 README 之外,咱们还提供了中文的说明性质文档:

和英文的说明性质文档:

后续帮助理解的文章,也会陆续上传到项目中供你们学习。

另外也为开源的贡献提供了标准的 中文贡献流程英文贡献流程,其中对于 ISSUE 模板、Commit 模板、Pull Requests 模板和 Apache 协议头均有说起。

若是你仍然对 EasyReact 有所不解或者流程代码上有任何问题,能够随时经过提 ISSUE 的方式与咱们联系,咱们都会尽快答复。

行为驱动开发

为了保证 EasyReact 的质量,咱们在开发的过程当中使用 行为驱动开发。当每一个新功能的声明部分肯定后,咱们会先编写大量的测试用例,这些用例模拟使用者的行为。经过模拟使用者的行为,以更加接近使用者的想法,去设计这个新功能的 API。同时大量的测试用例也保证了新的功能完成之时,必定是稳定的。

测试覆盖率

EasyReact 系列立项之时,就以高质量、高标准的开发原则来要求开发组成员执行。开源以后全部项目使用 codecov.io 服务生成对应的测试覆盖率报告,Easy 系列的框架覆盖率均保证在 95% 以上。

name listener
EasyReact
codecov
EasyTuple
codecov
EasySequence
codecov
EasyFoundation
codecov

持续集成

为了保证项目质量,全部的 Easy 系列框架都配有持续集成工具 Travis CI。它确保了每一次提交,每一次 Pull Request 都是可靠的。

展望

目前开源的框架组件只是创建起响应式编程的基石,Easy 系列的初心是为 MVVM 架构提供一个强有力的框架工具。下图是 Easy 系列框架的架构简图:

Archticture

将来开源计划

将来咱们还有提供更多框架能力,开源给你们:

名称 描述
EasyDebugToolBox 动态节点状态调试工具
EasyOperation 基于行为和操做抽象的响应式库
EasyNetwork 响应式的网络访问库
EasyMVVM MVVM 框架标准和相关工具
EasyMVVMCLI EasyMVVM 项目脚手架工具

跨平台与多语言

EasyReact 的设计基于面向对象,因此很容易在各个语言中实现,咱们也正在积极的在 Swift、Java、JavaScript 等主力语言中实现 EasyReact。

另外动态化做为目前行业的趋势,Easy 系列天然不会忽视。在 EasyReact 基于图的架构下,咱们能够很轻松的让一个 Objective-C 的上游节点经过一个特殊的桥接边链接到一个 JavaScript 节点,这样就可让部分的逻辑动态下发过来。

结语

数据传递和异步处理,是大部分业务的核心。EasyReact 从架构上用响应式的方式来很好的解决了这个问题。它有效地组织了数据和数据之间的联系, 让业务的处理流程从命令式编程方式,变成以数据流为核心的响应式编程方式。用先构建数据流关系再响应触发的方法,让业务方更关心业务的本质。使广大开发者从琐碎的命令式编程的状态处理中解放出来,提升了生产力。EasyReact 不只让业务逻辑代码更容易维护,也让出错的概率大大降低。

团队介绍

成威,项目的发起人,负责美团客户端新技术调研。国内函数式编程、响应式编程的爱好者,多年宣传和布道响应式编程实践并取得必定的成绩。 姜沂,项目的主要开发者。 秦宏,项目的主要开发者。 君阳,项目的早期开发者。 思琦,Easy 系列图标设计者,文档和代码翻译者。 志宇,参与了大部分的重构设计。 恩生,文档和代码翻译者。 姝琳,文档和代码翻译者。

招聘

招聘时间~美团平台业务研发中心诚招高级 iOS 工程师、技术专家。

欢迎投递简历到 zangchengwei#meituan.com。一块儿共建 Easy 系列。

相关文章
相关标签/搜索