美團客戶端響應式框架 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 系列。

相關文章
相關標籤/搜索