美團(tuán) EasyReact 源碼剖析:圖論與響應(yīng)式編程

前言

18 年 7 月美團(tuán)開源了 EasyReact,告知 iOS 工程師們響應(yīng)式編程和函數(shù)式編程并非不可分離,似乎一出來就想將 ReactiveCocoa 踢出神壇。該框架使用圖論來解決響應(yīng)式編程確實(shí)是一個顛覆性的思想,由于 ReactiveCocoa 的各種弊端讓很多團(tuán)隊(duì)望而卻步,而 EasyReact 的出現(xiàn)無疑讓很多人重拾對響應(yīng)式編程的希望。

官方資料:
美團(tuán)客戶端響應(yīng)式框架 EasyReact 開源啦
EasyReact GitHub

只需要大致看一下官方的介紹,就很容易理解到圖論在響應(yīng)式編程中扮演的角色,不管如何復(fù)雜的響應(yīng)鏈都能通過有向有環(huán)圖來表示,而數(shù)據(jù)的流動依賴深搜廣搜。單從框架的理解難易程度來看,EasyReact 完勝。

本文介紹 EasyReact 的源碼技術(shù)細(xì)節(jié),由于框架依賴庫代碼量較大,所以只會較為抽象的介紹比較核心和重要的部分,并且希望讀者能優(yōu)先閱讀官方資料以降低理解本文的成本。

一、框架整體認(rèn)識

首先,我們需要脫離具體的業(yè)務(wù),從圖論的要素來思考框架的構(gòu)成。

既然是圖,那必然有節(jié)點(diǎn)和邊,框架有兩種節(jié)點(diǎn),一種是EZRNode<T>泛型標(biāo)準(zhǔn)節(jié)點(diǎn),一種是任意對象;框架也有兩種邊,一種EZRTransform可變換的邊,一種是EZRListen監(jiān)聽邊,當(dāng)然邊的衍生類很多并且實(shí)現(xiàn)了數(shù)個協(xié)議。

在控制器中寫這樣一段代碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    EZRMutableNode<NSNumber *> *nodeA = [EZRMutableNode new];
    EZRMutableNode<NSNumber *> *nodeB = [EZRMutableNode new];
    [nodeB linkTo:nodeA];
    [[nodeB listenedBy:self] withBlock:^(NSNumber * _Nullable next) {
        NSLog(@"nodeB 改變:%@", next);
    }];
}

創(chuàng)建兩個可變的節(jié)點(diǎn),并且讓nodeB連接到nodeA,同時讓self作為nodeB的監(jiān)聽者。-linkTo:-listenedBy:都是語法糖暫時不用管具體含義,這段代碼轉(zhuǎn)換為一張圖如下:

邊有兩個很重要的屬性from (強(qiáng)引用)to (弱引用),fromto的方向就是數(shù)據(jù)流動的方向。圖中的an EZRTransforman EZRListen分別是可變邊和監(jiān)聽邊的一個實(shí)例,箭頭的方向表示數(shù)據(jù)流動的方向。當(dāng)執(zhí)行了以下代碼過后:

nodeA.value = @10;

打印:

nodeB 改變:10

@10這個對象通過圖中箭頭的方向依次傳遞,最終由self捕獲到并打印出來。這就是框架的一般邏輯,結(jié)構(gòu)是易懂且清晰的,通過對邊的各種邏輯處理來達(dá)到控制數(shù)據(jù)傳遞的目的。更具體的東西請看官方文檔和源碼。

二、內(nèi)存管理策略

在一個響應(yīng)鏈中,始終是數(shù)據(jù)的消費(fèi)者持有數(shù)據(jù)的提供者。也就是說,數(shù)據(jù)流動的方向往往和強(qiáng)引用方向相反,前面那張圖反過來就是強(qiáng)引用關(guān)系:

self --> an EZRListen --> nodeB --> an EZRTransform --> nodeA

因?yàn)樵跇I(yè)務(wù)中,監(jiān)聽者節(jié)點(diǎn)往往關(guān)系到具體業(yè)務(wù),沒有監(jiān)聽者那么其它節(jié)點(diǎn)就沒有了存在的意義,所以框架的思想是使用監(jiān)聽者來作為結(jié)點(diǎn)的最終強(qiáng)持有者。

下面通過節(jié)點(diǎn)與邊的兩種連接方式驗(yàn)證內(nèi)存管理策略。

監(jiān)聽者連接實(shí)現(xiàn)

[[nodeB listenedBy:self] withBlock:^(NSNumber * _Nullable next) {}];

通過閱讀源碼得知強(qiáng)引用關(guān)系如圖(箭頭表示強(qiáng)引用):

圖中已經(jīng)很明顯了,只要監(jiān)聽者節(jié)點(diǎn)釋放,其它的對象都將不復(fù)存在。而其中的引用關(guān)系恰好能表示實(shí)現(xiàn)監(jiān)聽的數(shù)據(jù)結(jié)構(gòu),使用Dictionary是為了讓監(jiān)聽者能響應(yīng)不同節(jié)點(diǎn)的監(jiān)聽,后面使用Array是為了讓監(jiān)聽者能對同一節(jié)點(diǎn)進(jìn)行多次監(jiān)聽,結(jié)合源碼來看應(yīng)該很容易就理解了。

同時,由于EZRNode的改變要傳遞到監(jiān)聽者節(jié)點(diǎn),所以必然會有必要的反向弱引用,這里就不多說了。

節(jié)點(diǎn)連接實(shí)現(xiàn)

    EZRMutableNode<NSNumber *> *nodeA = [EZRMutableNode new];
    EZRMutableNode<NSNumber *> *nodeB = [EZRMutableNode new];
    [nodeB linkTo:nodeA];

通過閱讀源碼得知強(qiáng)引用關(guān)系如圖(箭頭表示強(qiáng)引用):

實(shí)際上框架的圖結(jié)構(gòu)就是以上兩種連接方式的組合,我們用強(qiáng)引用的關(guān)系來分析它們能清晰的理解框架的內(nèi)存管理策略。

三、數(shù)據(jù)流動帶來的問題

數(shù)據(jù)流動循環(huán)

有這樣一種場景:

圖中箭頭的方向表示數(shù)據(jù)流動的方向,這就是比較典型的有向有環(huán)圖,這種結(jié)構(gòu)會帶來兩個問題:

  1. 形成引用環(huán),無法自動釋放內(nèi)存。
  2. 數(shù)據(jù)流動會陷入無限循環(huán)。

第一個問題實(shí)際上很簡單,如果業(yè)務(wù)中寫了這種結(jié)構(gòu),只需要手動破除循環(huán)引用。把關(guān)注點(diǎn)放到第二問題上,數(shù)據(jù)流動無限循環(huán)將會棧溢出帶來災(zāi)難性的后果,框架是如何避免的呢,官方文檔只說了通過EZRSenderList來避免,下面看看源碼中具體是如何實(shí)現(xiàn)的。

EZRMutableNode節(jié)點(diǎn)中,數(shù)據(jù)傳遞必然會走的方法是:

- (void)next:(nullable id)value from:(EZRSenderList *)senderList context:(nullable id)context {
    ...
    [self _next:value from:senderList context:context];
    ...
}
- (void)_next:(nullable id)value from:(EZRSenderList *)senderList context:(nullable id)context {
    ...
    //賦值
    _value = value;
    ...
    //拼接當(dāng)前節(jié)點(diǎn)
    EZRSenderList *newQueue = [senderList appendNewSender:self];
    //遍歷監(jiān)聽邊發(fā)送數(shù)據(jù)
    for (... item in self.privateListenEdges) {
        [item next:value from:newQueue context:context];
    }
    //遍歷下游可變邊發(fā)送數(shù)據(jù)
    for (... item in self.privateDownstreamTransforms) {
        if (![senderList contains:item.to]) {
             [item next:value from:newQueue context:context];
        }
    }
  ...
}

省去并修改了很多代碼變成了偽代碼,這和源碼是不一致的,便于查看邏輯??梢钥吹綀?zhí)行了兩個for循環(huán),self.privateListenEdges是監(jiān)聽邊集合,self.privateDownstreamTransforms是下游的可變邊集合,它們的元素在構(gòu)建圖的時候已經(jīng)準(zhǔn)備好了,通過遍歷這兩個集合實(shí)現(xiàn)遞歸深搜將數(shù)據(jù)傳遞下去。

EZRSenderList是一個鏈表,可以注意到[senderList appendNewSender:self]代碼,將當(dāng)前節(jié)點(diǎn)拼接進(jìn)鏈表,這個鏈表的生命周期是一次數(shù)據(jù)流動過程。在遍歷下游可變邊的時候有一個判斷:if (![senderList contains:item.to]) {},實(shí)際上這就是阻止無限循環(huán)的核心操作,即若數(shù)據(jù)流動鏈表中包含了當(dāng)前節(jié)點(diǎn),就截?cái)?,避免無限循環(huán)。

nodeA --> nodeB --> nodeC |senderList里面有nodeA,截?cái)鄚 --> nodeA

數(shù)據(jù)流動重入

思考這樣一種場景:

紅色的邊是監(jiān)聽邊,黑色的邊表示可變邊,此處表示nodeA監(jiān)聽了nodeB的變化,當(dāng)nodeB的值變化的時候,會遍歷監(jiān)聽邊發(fā)送數(shù)據(jù),也就是會通知到nodeA。

需要注意的是,節(jié)點(diǎn)只在遍歷下游可變邊時通過EZRSenderList截?cái)嘌h(huán),而在遍歷監(jiān)聽邊時未做處理,這是由于監(jiān)聽邊不會讓to對應(yīng)的節(jié)點(diǎn)繼續(xù)深搜傳遞數(shù)據(jù),而是直接發(fā)送一個通知,所以每一個由業(yè)務(wù)工程師創(chuàng)建的監(jiān)聽都是有意義的。

若出現(xiàn)以下情況:

nodeA --> nodeB [nodeA監(jiān)聽到改變: nodeA --> nodeB (執(zhí)行有限次)] --> nodeC

當(dāng)nodeA值改變,傳遞到nodeB,當(dāng)nodeA監(jiān)聽到nodeB值變化值,nodeA又一次改變自己的值向nodeB傳遞數(shù)據(jù)nodeA --> nodeB,這種情況會導(dǎo)致此次流動的數(shù)據(jù)可能會被更改而不安全。監(jiān)聽回調(diào)的操作邏輯通常是業(yè)務(wù)工程師來寫,在特定的業(yè)務(wù)場景下這種情況是可能出現(xiàn)的。

那么,如何保證一次數(shù)據(jù)流動不可重入,以此保證數(shù)據(jù)安全?

EZRMutableNode.m中,先來看一個至關(guān)重要的類(EZTuple3是元祖,不用糾結(jié)其實(shí)現(xiàn)):

@interface EZRSettingQueue: NSObject
//是否是第一次使用該實(shí)例
@property (nonatomic, assign) BOOL firstSetting;
//隊(duì)列
@property (nonatomic, strong) NSMutableArray<EZTuple3<id, EZRSenderList *, id> *> *queue;
//入隊(duì)
- (void)enqueue:(EZTuple3<id, EZRSenderList *, id> *)tuple;
//出隊(duì)
- (EZTuple3<id, EZRSenderList *, id> *)dequeue;
@end

從 API 看就一目了然,這個類的作用是封裝了一個隊(duì)列,然后有一個屬性firstSetting來判斷是否是第一次使用該實(shí)例,接下來看一個方法:

- (EZRSettingQueue *)currentSettingQueue {
    EZRSettingQueue *settingQueue = [NSThread currentThread].threadDictionary[_settingQueueKey];
    if (settingQueue == nil) {
        settingQueue = [EZRSettingQueue new];
        [NSThread currentThread].threadDictionary[_settingQueueKey] = settingQueue;
    }
    return settingQueue;
}

通過一個線程附帶的 hash 容器,保存一個EZRSettingQueue對象,這個_settingQueueKey是當(dāng)前節(jié)點(diǎn)唯一標(biāo)識。然后接著看下一個方法:

- (void)checkSettingQueue {
    EZRSettingQueue *settingQueue = self.currentSettingQueue;
    if (settingQueue.queue.count) {
        [self settingDequeue];
    } else {
        [NSThread currentThread].threadDictionary[_settingQueueKey] = nil;
    }
}

這個方法判斷了這個線程持有EZRSettingQueue隊(duì)列是否為空,若為空將它從線程字典中剔除,否則執(zhí)行下面方法:

- (void)settingDequeue {
    EZTuple3<id, EZRSenderList *, id> *tuple = [self.currentSettingQueue dequeue];
    [self _next:tuple.first from:tuple.second context:tuple.third];
}

取出隊(duì)列中的元素,并且調(diào)用節(jié)點(diǎn)的數(shù)據(jù)傳送方法-_next...,到這里其實(shí)就可以猜到EZRSettingQueue是用來存儲數(shù)據(jù)流動相關(guān)數(shù)據(jù)的。那么,我們來看數(shù)據(jù)流動流程里面是如何調(diào)用這些方法的:

- (void)next:(nullable id)value from:(EZRSenderList *)senderList context:(nullable id)context {
    EZRSettingQueue *settingQueue = self.currentSettingQueue;
    if EZR_LikelyYES(settingQueue.firstSetting) {
        settingQueue.firstSetting = NO;
        [self _next:value from:senderList context:context];
    } else {
        [settingQueue enqueue:EZTuple(value, senderList, context)];
    }
}
- (void)_next:(nullable id)value from:(EZRSenderList *)senderList context:(nullable id)context {
    ...lock {
        _value = value;
    }
    ...
    //深搜發(fā)送數(shù)據(jù)
    ...
    [self checkSettingQueue];
}

可以看到,在深搜發(fā)送數(shù)據(jù)完畢之后,會調(diào)用-checkSettingQueue方法。

情況一:深搜完成之前不會再次進(jìn)入-next:...方法,那么-checkSettingQueue會將線程字典里面的隊(duì)列清空,那么 if (settingQueue.firstSetting)這個判斷將始終為true,這種情況下發(fā)現(xiàn)EZRSettingQueue并沒有起到作用。

情況二:深搜的過程中,再次進(jìn)入了當(dāng)前節(jié)點(diǎn)的-next:...方法,這時if (settingQueue.firstSetting)判斷就為false了,那么就會將發(fā)送數(shù)據(jù)必備的參數(shù)入隊(duì)到EZRSettingQueue隊(duì)列中。當(dāng)深搜發(fā)送數(shù)據(jù)完成過后,調(diào)用-checkSettingQueue方法執(zhí)行在隊(duì)列中的任務(wù)。如此,通過避免同一個節(jié)點(diǎn)的-next:...重入來保證一次數(shù)據(jù)流動過程的安全。當(dāng)然,有可能數(shù)據(jù)流動會無限循環(huán)仍然導(dǎo)致棧溢出,但這屬于業(yè)務(wù)工程師“指定”的邏輯。

值得注意的是,情況二的分析是建立在同一線程的。延遲執(zhí)行隊(duì)列EZRSettingQueue是放在線程字典中的,意味著在同一線程一次數(shù)據(jù)流動是不可重復(fù)進(jìn)入的,而不同線程的重復(fù)進(jìn)入不做處理(因?yàn)椴煌€程擁有不同的??臻g,不會相互影響)。而對于多線程情況,-_next:...方法中對_value = value就行了加鎖操作,保證全局變量的安全,同時避免同一線程的重入也恰巧避免了重復(fù)獲取鎖導(dǎo)致的死鎖。

這確實(shí)是一個非常巧妙且令人興奮的技巧。

四、邊的變換

EZRTransform有很多衍生類,每一個都對應(yīng)一種變換。什么叫變換呢?也就是在數(shù)據(jù)傳到EZRTransform的時候,EZRTransform對數(shù)據(jù)進(jìn)行處理,然后再按照特定的邏輯繼續(xù)發(fā)送。

EasyReact 自帶有非常多的變換處理,比如map、filter、scanmerge等,可以到 GitHub 查看其使用,也可以直接查看源碼,大多數(shù)的變換的實(shí)現(xiàn)都是很簡單易懂的,筆者這里只列舉并解析幾個稍微比較復(fù)雜的實(shí)現(xiàn)(主要是通過結(jié)構(gòu)圖來解析,最好是對照源碼理解)。

combine

響應(yīng)式編程經(jīng)常會使用 a := b + c 來舉例,意圖是當(dāng) b 或者 c 的值發(fā)生變化的時候,a 會保持兩者的加和。那么在響應(yīng)式庫 EasyReact 中,我們是怎樣體現(xiàn)的呢?就是通過 EZRCombine-mapEach 操作:

EZRMutableNode<NSNumber *> *nodeA = [EZRMutableNode value:@1];
EZRMutableNode<NSNumber *> *nodeB = [EZRMutableNode value:@2];
EZRNode<NSNumber *> *nodeC = [EZRCombine(nodeA, nodeB) mapEach:^NSNumber *(NSNumber *a, NSNumber *b) {
  return @(a.integerValue + b.integerValue);
}];

nodeC.value;           // <- 1 + 2 = 3
nodeA.value = @4;
nodeC.value;           // <- 4 + 2 = 6
nodeB.value = @6;
nodeC.value;           // <- 4 + 6 = 10

上面是官方的描述和例子,實(shí)際上 combine 操作就是nodeC的值始終等于nodeA + nodeB。

combine

實(shí)現(xiàn) combine 的邊叫做EZRCombineTransform,同時有一個EZRCombineTransformGroup作為處理器,它持有了所有相關(guān)的邊,當(dāng)數(shù)據(jù)經(jīng)過EZRCombineTransform時,交由處理器將所有邊的值相加,然后繼續(xù)發(fā)送。

zip

拉鏈操作是這樣的一種操作:它將多個節(jié)點(diǎn)作為上游,所有的節(jié)點(diǎn)的第一個值放在一個元組里,所有的節(jié)點(diǎn)的第二個值放在一個元組里……以此類推,以這些元組作為值的就是下游。它就好像拉鏈一樣一個扣著一個:

EZRMutableNode<NSNumber *> *nodeA = [EZRMutableNode value:@1];
EZRMutableNode<NSNumber *> *nodeB = [EZRMutableNode value:@2];
EZRNode<EZTuple2<NSNumber *, NSNumber *> *> *nodeC = [nodeA zip:nodeB];

[[nodeC listenedBy:self] withBlock:^(EZTuple2<NSNumber *, NSNumber *> *tuple) {
  NSLog(@"接收到 %@", tuple);
}];
nodeA.value = @3;
nodeA.value = @4;
nodeB.value = @5;
nodeA.value = @6;
nodeB.value = @7;
/* 打印如下:
接收到 <EZTuple2: 0x60800002b140>(
  first = 1;
  second = 2;
  last = 2;
)
接收到 <EZTuple2: 0x60800002ac40>(
  first = 3;
  second = 5;
  last = 5;
)
接收到 <EZTuple2: 0x600000231ee0>(
  first = 4;
  second = 7;
  last = 7;
)
 */
zip

zip 的數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)和 combine 如出一轍,不同的是,每一個EZRZipTransform都維護(hù)了一個新值的隊(duì)列,當(dāng)數(shù)據(jù)流動時,EZRZipTransformGroup會讀取每一個邊對應(yīng)隊(duì)列的頂部元素(同時出隊(duì)),若某一個邊的隊(duì)列未讀取到新值則停止數(shù)據(jù)傳播。

switch

switch-case-default 變換是通過給出的 block 將每個上游的值代入,求出唯一標(biāo)識符,再分離這些標(biāo)識符的一種操作。我們舉例一個分離劇本的例子:

EZRMutableNode<NSString *> *node = [EZRMutableNode new];
EZRNode<EZRSwitchedNodeTuple<NSString *> *> *nodes = [node switch:^id<NSCopying> _Nonnull(NSString * _Nullable next) {
  NSArray<NSString *> *components = [next componentsSeparatedByString:@":"];
  return components.count > 1 ? components.firstObject: nil;
}];
EZRNode<NSString *> *liLeiSaid = [nodes case:@"李雷"];
EZRNode<NSString *> *hanMeimeiSaid = [nodes case:@"韓梅梅"];
EZRNode<NSString *> *aside = [nodes default];
[[liLeiSaid listenedBy:self] withBlock:^(NSString *next) {
  NSLog(@"李雷節(jié)點(diǎn)接到臺詞: %@", next);
}];
[[hanMeimeiSaid listenedBy:self] withBlock:^(NSString *next) {
  NSLog(@"韓梅梅節(jié)點(diǎn)接到臺詞: %@", next);
}];
[[aside listenedBy:self] withBlock:^(NSString *next) {
  NSLog(@"旁白節(jié)點(diǎn)接到臺詞: %@", next);
}];
node.value = @"在一個寧靜的下午";
node.value = @"李雷:大家好,我叫李雷。";
node.value = @"韓梅梅:大家好,我叫韓梅梅。";
node.value = @"李雷:你好韓梅梅。";
node.value = @"韓梅梅:你好李雷。";
node.value = @"于是他們幸福的在一起了";
/* 打印如下:
旁白節(jié)點(diǎn)接到臺詞: 在一個寧靜的下午
李雷節(jié)點(diǎn)接到臺詞: 李雷:大家好,我叫李雷。
韓梅梅節(jié)點(diǎn)接到臺詞: 韓梅梅:大家好,我叫韓梅梅。
李雷節(jié)點(diǎn)接到臺詞: 李雷:你好韓梅梅。
韓梅梅節(jié)點(diǎn)接到臺詞: 韓梅梅:你好李雷。
旁白節(jié)點(diǎn)接到臺詞: 于是他們幸福的在一起了
 */

分支的實(shí)現(xiàn)幾乎是最復(fù)雜的了,node首先通過EZRSwitchMapTransform邊連接一個nodes下游節(jié)點(diǎn),并且初始化一個分支劃分規(guī)則 (block);然后nodes節(jié)點(diǎn)分別通過EZRCaseTransform邊連接liLeiSaid、hanMeimeiSaidaside下游節(jié)點(diǎn),并且每一個下游節(jié)點(diǎn)存儲了一個匹配分支的key(也就是例子中的“李雷”、“韓梅梅”等)。

當(dāng)node發(fā)送數(shù)據(jù)過來時,由EZRSwitchMapTransform通過分支劃分規(guī)則處理數(shù)據(jù),然后將每一個分支節(jié)點(diǎn)通過 hash 容器裝起來,也就是圖中的藍(lán)色節(jié)點(diǎn)case node,這個例子發(fā)送的數(shù)個消息最終會創(chuàng)建三個分支;在創(chuàng)建分支完成過后,EZRSwitchMapTransform向下游繼續(xù)發(fā)送數(shù)據(jù),在數(shù)據(jù)到達(dá)EZRCaseTransform時,該邊會監(jiān)聽對應(yīng)的case node(當(dāng)然前提是匹配)而不會繼續(xù)向下游發(fā)送數(shù)據(jù);然后EZRSwitchMapTransform會繼續(xù)改變對應(yīng)case node的值,由此EZRCaseTransform就接收到了數(shù)據(jù)改變的通知,最終發(fā)送給下游節(jié)點(diǎn),即這里的liLeiSaid、hanMeimeiSaidaside

case node 中間節(jié)點(diǎn)的意義

貌似沒有case node節(jié)點(diǎn)也能實(shí)現(xiàn) switch 功能,經(jīng)過筆者思考,猜測作者此處設(shè)計(jì)的深意:由EZRSwitchMapTransform預(yù)處理得到key和最終需要傳遞的數(shù)據(jù)value;而EZRCaseTransform只需關(guān)心key是否對應(yīng),若對應(yīng)才去監(jiān)聽對應(yīng)的case node。如此做法有兩點(diǎn)意義:

  1. 可以避免EZRCaseTransform接收到與它不匹配的value,也可以避免連接在nodes節(jié)點(diǎn)的非EZRCaseTransform邊接收到value,由此保證value的安全。
  2. EZRCaseTransform想要取消對 switch 分支數(shù)據(jù)的接收,而又要繼續(xù)保持上游邊的結(jié)構(gòu),可以直接取消對case node的監(jiān)聽(雖然框架沒有這個功能)。

五、代碼細(xì)節(jié)及優(yōu)化

在源碼的閱讀中,發(fā)現(xiàn)了幾個有意思的代碼技巧。

自動解鎖

- (void)_next:(nullable id)value from:(EZRSenderList *)senderList context:(nullable id)context {
    {
        EZR_SCOPELOCK(_valueLock);
        _value = value;
    }
    ...
}

EZR_SCOPELOCK()宏的出場率相當(dāng)高,直接查看實(shí)現(xiàn):

#define EZR_SCOPELOCK(LOCK) /
EZR_LOCK(LOCK);  /
EZR_LOCK_TYPE EZR_CONCAT(auto_lock_, __LINE__) /
__attribute__((cleanup(EZR_unlock), unused)) = LOCK

可以看到先是對傳進(jìn)來的鎖進(jìn)行加鎖操作,后面關(guān)鍵的有句代碼:

__attribute__((cleanup(AnyFUNC), unused))

這句代碼加在局部變量后面,將會在局部變量作用域結(jié)束之前調(diào)用AnyFUNC方法。那么此處的目的很簡單,看一眼這里的EZR_unlock干了什么:

static inline void EZR_unlock(EZR_LOCK_TYPE *lock) {
    EZR_UNLOCK(*lock);
}

具體的宏可以看源碼,此處只是做了一個解鎖操作,由此就實(shí)現(xiàn)了自動解鎖功能。這就是為什么要用大括號把加鎖的代碼包起來,可以理解為限定加鎖的臨界區(qū)。

雖然少寫句代碼的意義不大,但是卻比較炫。

分支預(yù)測

經(jīng)常會看到類似的代碼:

if EZR_LikelyNO(value == EZREmpty.empty) {
    ...
}

EZR_LikelyNO系列宏出場率也是極高的:

#define EZR_Likely(x)       (__builtin_expect(!!(x), 1))
#define EZR_Unlikely(x)     (__builtin_expect(!!(x), 0))
#define EZR_LikelyYES(x)    (__builtin_expect(x, YES))
#define EZR_LikelyNO(x)     (__builtin_expect(x, NO))

可以看到實(shí)際上就是__builtin_expect()函數(shù)的宏,!!(x)是為了把非 0 變量變?yōu)?1 。

我們知道 CPU 有流水線執(zhí)行能力,當(dāng)處理分支程序時,判斷成功過后可能會產(chǎn)生指令的跳轉(zhuǎn),打斷 CPU 對指令的處理,并且直到判斷完成這個過程中,CPU 可能流水執(zhí)行了大量的無用邏輯,浪費(fèi)了時鐘周期。

簡單分析一下:

1 讀取指令 | 執(zhí)行指令 | 輸出結(jié)果   (判斷指令)
2           讀取指令 | 執(zhí)行指令 | 輸出結(jié)果
3                     讀取指令 | 執(zhí)行指令 | 輸出結(jié)果

假設(shè)一條指令的執(zhí)行分為三個階段,若這里是一個分支語句判斷,第 1 行是判斷指令,在判斷指令輸出結(jié)果時,下面兩條指令已經(jīng)在執(zhí)行中了,而判斷結(jié)構(gòu)是走另外一個分支,這就必然需要跳轉(zhuǎn)指令,而放棄 2、3 條指令的執(zhí)行或結(jié)果。

那么怎樣保證盡量不跳轉(zhuǎn)指令呢?

答案就是分支預(yù)測,通過工程師對業(yè)務(wù)的理解,告知編譯器哪個分支概率更大,比如:

if (__builtin_expect(someValue, NO)) {
    //為真代碼
} else {
    //為假代碼
}

那么在編譯后,可執(zhí)行文件中“為假代碼”轉(zhuǎn)換的指令將會靠前,優(yōu)先執(zhí)行。

后語

EasyReact 將圖論與響應(yīng)式編程結(jié)合起來表現(xiàn)非常好,將各種復(fù)雜邏輯都用相同的思維處理,不管從理解上還是使用上都非常具有親和性。

不過 EasyReact 作為美團(tuán)組件庫中的一個組件來說是很合適的,但是如果作為一個獨(dú)立的框架來說卻顯得有點(diǎn)臃腫了。

作為一個普通的開發(fā)者,可能更多的想如何高效且快捷的做一個框架,畢竟少有團(tuán)隊(duì)擁有美團(tuán)的技術(shù)實(shí)力。比如框架依賴了 EasySequence,這個東西對于 EasyReact 來說沒有太大意義,弱引用容器也可以用NSPointerArray替代;EasyTuple 元祖的實(shí)現(xiàn)有些復(fù)雜了,如果是個人框架的話建議使用 C++ 的 tuple;隊(duì)列、鏈表等數(shù)據(jù)結(jié)構(gòu)也不需自己實(shí)現(xiàn),隊(duì)列可以用 C++ 的queue,鏈表用 Objective-C 數(shù)組或 C 數(shù)組來表示也更加輕量。

這種從公司剝離的框架總是會有很多限制,比如公司的代碼規(guī)范、類庫使用規(guī)范,肯定遠(yuǎn)不及個人框架的自由和隨性。

在 EasyReact 中也體會到了一些設(shè)計(jì)思維,從代碼質(zhì)量來說確實(shí)是上乘的,閱讀過程中非常的流暢,很多看起來簡單的實(shí)現(xiàn),細(xì)想過后能發(fā)現(xiàn)令人驚喜的作用。

整體來說,收獲頗豐,給美團(tuán)技術(shù)團(tuán)隊(duì)點(diǎn)個贊。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,688評論 1 32
  • http://liuxing.info/2017/06/30/Spring%20AMQP%E4%B8%AD%E6%...
    sherlock_6981閱讀 16,211評論 2 11
  • 冬未春初 終于迎來了今冬的第一場雨 昨夜 雨聲陣陣,寒風(fēng)瑟瑟 冷氣逼人,讓人感覺冷是從心中鉆出來的! 這樣的夜 最...
    流年時光_fe16閱讀 338評論 2 2
  • 數(shù)獨(dú)(すうどく,Sūdoku)是一種運(yùn)用紙、筆進(jìn)行演算的邏輯游戲。玩家需要根據(jù)9×9盤面上的已知數(shù)字,推理出所有剩...
    娟子閱讀 3,487評論 3 1
  • 昊霖寶寶2018.4.21踐行~ 1.英文~已經(jīng)可以把字母歌唱下來,唱完還問媽媽“驚不驚訝?”。 磨耳朵鵝媽媽念讀...
    smile_曉倩閱讀 276評論 1 0

友情鏈接更多精彩內(nèi)容