iOS 依賴KVC構(gòu)建App消息派發(fā)服務(wù)

前言

  • 隨著項(xiàng)目的業(yè)務(wù)越來越重,客戶端與服務(wù)端對(duì)實(shí)時(shí)性要求較高的數(shù)據(jù)交互,不能再依賴HTTP輪詢或者可能會(huì)造成數(shù)據(jù)丟失的APNS推送。例如,訂單狀態(tài)的改變、新的運(yùn)營(yíng)消息、同時(shí)存在兩類客戶端(比如餓了么平臺(tái)的送餐員與用戶)需要進(jìn)行頻繁數(shù)據(jù)交互。基于上述原因,項(xiàng)目引入socket通信。通道建立好之后,就將之前的輪詢機(jī)制、依賴APNS推送的部分、還有新需求,移植到sockct通道。

  • 關(guān)于socket的搭建以及服務(wù)端與客戶端的協(xié)議制定,有時(shí)間我單開一篇與大家討論~~

  • 客戶端與服務(wù)端的通道建立好之后所面臨的問題。由于業(yè)務(wù)比較繁重,通道過來的(socket傳遞過來的數(shù)據(jù),以下簡(jiǎn)稱消息)消息,又有好多類型,每種類型又回包含不同的Value,如何讓業(yè)務(wù)層不同的頁面、組件、服務(wù)等準(zhǔn)確的接收到需要的消息,是本篇的重點(diǎn)。

討論

  • 首先,消息中心如何派發(fā)消息隊(duì)列里的消息到最終的接收者。
    • 依賴廣播
      廣播可以讓每一個(gè)監(jiān)聽廣播的接收者都能接收到消息。但是存在一個(gè)互斥的問題,就是,如果想要特定的接收者,接收特定的消息,就得增加特定的通知類型,類型越多,維護(hù)成本越大,且需要消息服務(wù)增加大量的判斷。如果不想增加太多的類型,比如關(guān)于訂單的所有消息,都叫做xxxxxxxOrderNotification,這樣一來,類型少了,但是到了業(yè)務(wù)層,但凡是監(jiān)聽了訂單類型消息的接收者,都得加入“大同小異”的判斷,也是相當(dāng)麻煩的操作。
    • KVO
      如果使用KVO,這就需要消息中心總是根據(jù)業(yè)務(wù)不同,頻繁的變動(dòng)消息的Model且KVO本身就不是太好用的一種機(jī)制。
    • 代理
      如何使用代理?怎樣在delegate回調(diào)處減少大量的判斷、保持更好的擴(kuò)展性、業(yè)務(wù)層代碼更簡(jiǎn)潔?
  • 如何處理多個(gè)接收者對(duì)于某條消息的相應(yīng)優(yōu)先級(jí)。

構(gòu)建過程

image.png

從消息接收者的角度出發(fā)考慮問題
比如一個(gè)訂單服務(wù)中頁面,根據(jù)當(dāng)前訂單ID,只想接收關(guān)于當(dāng)前訂單ID的消息,比如消息結(jié)構(gòu)為:

{
    "msg_body" : {
        "timestamp" : 172831819,
        "type" : "order_status_change",
        "title" : "",
        "sub_title" : "",
        "content" : "",
        "fmt_date" : "07-22 18:22",
        "extra" : {
            "order_no": "TAINIUBILE1234"
        }
    },
    "msg_id" : "00544"
}

如何在訂單服務(wù)中頁面,根據(jù)order_no訂閱該條消息?KVC
首先創(chuàng)建消息Model和消息服務(wù)

@interface YGMessage : YGSBaseModel
@property (nonatomic, assign, getter=isProcessed) BOOL processed; //已被處理過
@property (nonatomic, strong) NSDictionary *msg_body;
@property (nonatomic, copy) NSString *msg_id;
@end

@interface YGMessageCenter : NSObject
+ (instancetype)sharedInstance;
@end

關(guān)于YGMessage的屬性processed的設(shè)計(jì),本人的思路是,有可能同一條消息,被多個(gè)訂閱者所接收,由于消息服務(wù)無法判斷業(yè)務(wù)層的邏輯,比如,訂單詳情頁面處理過的訂單更新的消息,上層接收者可以不做處理等邏輯,可以通過將消息的processed改為YES,去跳過重復(fù)處理,當(dāng)然可以更好的方式去防止重復(fù)處理,

整個(gè)的消息體,可以在消息中心解析為一個(gè)字典,通過[dict valueForKey:@"msg_body.extra.order_no"]就可以拿到order_no,而且根據(jù)value=TAINIUBILE1234派發(fā)到訂單服務(wù)中頁面,由此就可以構(gòu)建一個(gè)消息訂閱條件

typedef struct YGMessageCondition {
    NSString * _Nullable keyPath; //
    _Nullable id value;
} YGMessageCondition;
//提供一個(gè)構(gòu)造函數(shù)
static inline YGMessageCondition YGMessageConditionMake(NSString * _Nullable keyPath, _Nullable id value) {
    YGMessageCondition condition;
    condition.keyPath = keyPath;
    condition.value = value;
    return condition;
}

消息服務(wù)派發(fā)消息時(shí)通過代理
生成代理協(xié)議

@protocol YGMessageCenterDelegate <NSObject>
@optional;

/**
 派發(fā)消息

 @param message 消息
 @param intercept 是否阻斷消息繼續(xù)發(fā)給其他訂閱者
 */
- (void)messageCenterDidReceiveMessage:(YGMessage *)message intercept:(BOOL *)intercept;
@end

每次訂閱消息的時(shí)候,塞一個(gè)Condition給消息服務(wù),YGMessageCenter添加方法:

/**
 添加代理
 
 @param delegate 代理
 所有接收到的消息都會(huì)傳給該delegate(如果消息未被棧頂delegate攔截)
 */
- (void)addDelegate:(id<YGMessageCenterDelegate>)delegate;
/**
 根據(jù)YGMessageCondition添加代理

 @param delegate 代理
 @param condition keyPath-value
 如果condition.keyPath = nil,則所有接收到的消息都會(huì)傳給該delegate(如果消息未被棧頂delegate攔截)
 如果condition.value = nil,則所有接收到的msg.keyPath下有值的消息,都會(huì)傳給該delegate(如果消息未被棧頂delegate攔截)
 */
- (void)addDelegate:(id<YGMessageCenterDelegate>)delegate condition:(YGMessageCondition)condition;

既然有訂閱方法,必然就需要移除訂閱的方法

/**
 移除代理
 
 @param delegate 代理
 */
- (void)removeDelegate:(id<YGMessageCenterDelegate>)delegate;
/**
 根據(jù)YGMessageCondition移除代理

 @param delegate 代理
 @param condition keyPath-value
 如果condition.keyPath = nil,則移除該代理,否則,移除指定的keyPath監(jiān)聽
 如果condition.value = nil,則移除keyPath的所有監(jiān)聽
 */
- (void)removeDelegate:(id<YGMessageCenterDelegate>)delegate condition:(YGMessageCondition)condition;

再看addDelegate:addDelegate:condition:方法的內(nèi)部實(shí)現(xiàn)
首先思考一個(gè)問題,添加的消息的代理肯定是多個(gè),所以服務(wù)內(nèi)部必然有一個(gè)集合去保留這些代理,但是如果直接將代理對(duì)象加入到某個(gè)集合當(dāng)中,很容易會(huì)因?yàn)闃I(yè)務(wù)層童鞋的操作不當(dāng),造成強(qiáng)引用問題?;谶@個(gè)原因我們創(chuàng)建一個(gè)中間件,來弱引用傳入的代理,并保存訂閱條件等其他一些操作,此處命名為YGMessageMonitor消息監(jiān)聽者。

YGMessageMonitor該類設(shè)計(jì)的屬性及方法,下面會(huì)根據(jù)構(gòu)建過程逐步解釋

@interface YGMessageMonitor : NSObject
@property (nonatomic, weak) id<YGMessageCenterDelegate> delegate; //代理
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSMutableArray *> *keyPathValueMaps;
@property (nonatomic, assign) BOOL alwaysRespond; //總是響應(yīng)所有消息
@property (nonatomic, assign, readonly) BOOL isInvalid; //是否無效,業(yè)務(wù)類型為0或者代理為空
- (void)addCondition:(YGMessageCondition)condition;
- (void)removeCondition:(YGMessageCondition)condition;
- (BOOL)respondsToMessage:(YGMessage *)obj;
@end

addDelegate:方法的實(shí)現(xiàn),當(dāng)某個(gè)代理對(duì)象,不攜帶任何condition加入到消息服務(wù)中時(shí)

  • 判空(無須解釋)
  • 獲取弱引用該delegate對(duì)象的中間件
    YGMessageCenter添加
@property (nonatomic, strong) NSMutableArray *monitorArray;
- (YGMessageMonitor *)monitorForDelegate:(id<YGMessageCenterDelegate>)delegate {
    NSMutableArray *invalidArray = [NSMutableArray array];
    YGMessageMonitor *monitor = nil;
    for (YGMessageMonitor *obj in self.monitorArray) {
        if (obj.delegate == delegate) {
            monitor = obj;
        }
        if (obj.delegate == nil) {
            [invalidArray addObject:obj];
        }
    }
    [self.monitorArray removeObjectsInArray:invalidArray];
    return monitor;
}
  • 若獲取到的monitor對(duì)象為空時(shí),創(chuàng)建新的monitor,并若引用delegate
  • 設(shè)置該monitoralwaysRespond屬性為YES,該屬性的定義為,無條件響應(yīng)任何消息,增加該屬性的目的是減少判斷,提高效率
- (void)addDelegate:(id<YGMessageCenterDelegate>)delegate {
    if (delegate == nil) { return ; }
    //創(chuàng)建一個(gè)消息監(jiān)聽者,無條件的delegate,設(shè)置其監(jiān)聽者alwaysRespond為yes
    YGMessageMonitor *monitor = [self monitorForDelegate:delegate];
    if (monitor == nil) {
        monitor = [[YGMessageMonitor alloc] init];
        monitor.delegate = delegate;
        [self.monitorArray addObject:monitor];
    }else {
        //如果存在監(jiān)聽條件,則移除所有的監(jiān)聽條件,釋放內(nèi)存
        [monitor.keyPathValueMaps removeAllObjects];
    }
    monitor.alwaysRespond = YES;
}

addDelegate:condition:方法的實(shí)現(xiàn),當(dāng)某個(gè)代理對(duì)象攜帶condition加入到消息服務(wù)中時(shí)

  • 判空(無須解釋)
  • 判斷condition.keyPath是否為空,若為空調(diào)用addDelegate:方法
  • 獲取弱引用該delegate對(duì)象的中間件
  • 若獲取到的monitor對(duì)象為空時(shí),創(chuàng)建新的monitor,并若引用delegate
  • 判斷moitoralwaysRespond是否為YES,if (monitor.alwaysRespond) { return ;}
  • alwaysRespondNO,則調(diào)用[monitor addCondition:condition];
- (void)addDelegate:(id<YGMessageCenterDelegate>)delegate condition:(YGMessageCondition)condition {
    if (delegate == nil) { return ; }
    //若condition.keypath為空,則設(shè)置該delegate為無條件
    if (condition.keyPath.length == 0) {
        [self addDelegate:delegate];
        return ;
    }
    YGMessageMonitor *monitor = [self monitorForDelegate:delegate];
    if (monitor == nil) {
        monitor = [[YGMessageMonitor alloc] init];
        monitor.delegate = delegate;
        [self.monitorArray addObject:monitor];
    }
    //若該代理已被設(shè)置為無條件監(jiān)聽者,則不再處理監(jiān)聽條件
    if (monitor.alwaysRespond) { return ;}
    [monitor addCondition:condition];
}

接著看YGMessageMonitor addCondition:方法如何實(shí)現(xiàn),考慮以下幾種情況

  • 若傳入的condition.valuenil時(shí),也就表示該消息訂閱者不必匹配keyPath*下具體的值,只要能匹配到存在值,就接收消息派發(fā)
  • 若傳入的condition.value不為 nil 時(shí),則表示該消息訂閱者匹配keyPath下具體的值
  • 若同一個(gè)消息訂閱者,多次傳入condition且擁有不同的keyPath,或者相同的keyPath匹配不同的value

由于消息訂閱者與YGMessageMonitor是一對(duì)一的關(guān)系,所以需要給YGMessageMonitor增加屬性
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSMutableArray *> *keyPathValueMaps;

在調(diào)用addCondition:方法時(shí)首先需要獲取到condition.keyPath對(duì)應(yīng)的value數(shù)組

- (NSMutableArray *)valueArrayForKeyPath:(NSString *)keyPath {
    NSMutableArray *array = self.keyPathValueMaps[keyPath];
    if (array == nil) {
        array = [NSMutableArray array];
        self.keyPathValueMaps[keyPath] = array;
    }
    return array;
}

其次判斷condition.value是否為nil,若為nil則需要有一個(gè)特殊的value來標(biāo)記訂閱者接收該keyPath下所有的存在值的消息
定義常量static NSString * const YGAlwaysRespondValue = @"alwaysRespondValue";,賦值并保存value

- (void)addCondition:(YGMessageCondition)condition {
    //查找監(jiān)聽條件keypath下對(duì)照的value數(shù)組
    NSMutableArray *array = [self valueArrayForKeyPath:condition.keyPath];
    //若value為空,則監(jiān)聽該keypath下存在值的所有消息
    if (condition.value == nil) {
        condition.value = YGAlwaysRespondValue;
    }
    [array addObject:condition.value];
}

另外增加removeCondition:方法

- (void)removeCondition:(YGMessageCondition)condition {
    NSMutableArray *array = self.keyPathValueMaps[condition.keyPath];
    if (array == nil) { return ;}
    if (condition.value == nil) {
        condition.value = YGAlwaysRespondValue;
    }
    [array removeObject:condition.value];
    if (array.count == 0) {
        [self.keyPathValueMaps removeObjectForKey:condition.keyPath];
    }
}

至此消息訂閱已基本實(shí)現(xiàn),下面看消息派發(fā),當(dāng)接收到socket傳遞過來的數(shù)據(jù)時(shí)

{
    "msg_body" : {
        "timestamp" : 172831819,
        "type" : "order_status_change",
        "title" : "",
        "sub_title" : "",
        "content" : "",
        "fmt_date" : "07-22 18:22",
        "extra" : {
            "order_no": "TAINIUBILE1234"
        }
    },
    "msg_id" : "00544"
}

首先解析成YGMessageModel,在解析model之前,可以做排重工作,這部分在源碼中給出,再次不做講解,然后獲取monitorArray中所有響應(yīng)該消息的訂閱者,調(diào)用messageCenterDidReceiveMessage:進(jìn)行派發(fā)

  • 先看如何判斷monitor是否響應(yīng)該消息
    YGMessageMonitor增加方法respondsToMessage:,該方法只是簡(jiǎn)單的實(shí)現(xiàn)了部分判斷條件,可根據(jù)不同業(yè)務(wù)情況做擴(kuò)展,不多做講解
- (BOOL)respondsToMessage:(YGMessage *)message {
    if (self.alwaysRespond) { return YES; }
    NSArray *keyPathArray = self.keyPathValueMaps.allKeys;
    for (NSString *keyPath in keyPathArray) {
        id value = [message valueForKeyPath:keyPath];
        if (value == nil) {
            return NO;
        }
        NSArray *valueArray = self.keyPathValueMaps[keyPath];
        for (id conditionValue in valueArray) {
            if (conditionValue == YGAlwaysRespondValue) {
                return YES;
            }
            if ([value isKindOfClass:[NSString class]] && [conditionValue isKindOfClass:[NSString class]]) {
                if ([value isEqualToString:conditionValue]) {
                    return YES;
                }
            }
            if ([value isKindOfClass:[NSNumber class]] && [conditionValue isKindOfClass:[NSNumber class]]) {
                if ([value isEqualToNumber:conditionValue]) {
                    return YES;
                }
            }
        }
    }
    return NO;
}
  • 在判斷YGMessageMonitor是否響應(yīng)某條消息的時(shí)候,同時(shí)判斷該監(jiān)聽者是否已經(jīng)為無效監(jiān)聽著,YGMessageMonitor重寫isInvalidgetter方法
- (BOOL)isInvalid {
    if (self.delegate == nil) {
        return YES;
    }
    if (self.alwaysRespond) {
        return NO;
    }
    return self.keyPathValueMaps.allKeys.count == 0;
}
  • 最后派發(fā)完消息,移除monitorArray所有無效的monitor
- (void)postMessage:(YGMessage *)message {
    for (YGMessageMonitor *monitor in self.monitorArray.reverseObjectEnumerator) {
        if (monitor.isInvalid) {
            [self.invalidMonitorArray addObject:monitor];
            continue ;
        }
        BOOL responds = [monitor respondsToMessage:message];
        if (!responds) { continue ; }
        if ([monitor.delegate respondsToSelector:@selector(messageCenterDidReceiveMessage:intercept:)]) {
            BOOL intercept = NO;
            [monitor.delegate messageCenterDidReceiveMessage:message intercept:&intercept];
            if (intercept) { break ; }
        }
    }
    [self.monitorArray removeObjectsInArray:self.invalidMonitorArray];
    [self.invalidMonitorArray removeAllObjects];
}

至此,整個(gè)的消息訂閱與派發(fā)功能基本實(shí)現(xiàn),一些完善的邏輯,根據(jù)自身業(yè)務(wù)做不同的處理,在此就不做贅述。若有不明之處,可以評(píng)論溝通。不完善之處,還請(qǐng)慷慨指出。

demo地址

Demo

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

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

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