本文作者:尚尧

一、布景

心遇作为一款交际产品,音讯会话页必定是用户运用量最大的页面之一,因而会话页的用户体验将尤为重要。一起,心遇有着陌生人交际特点,用户的会话量动辄上万,会话页也面临着较大的功能挑战。因而,会话页的功能优化既是要点,也是难点。

本文将举例会话页已知的功能问题,剖析完结坏处,最终经过引入 ReactiveObjC 来更高雅的解决问题。

二、 ReactiveObjC 简介

ReactiveObjC 是一个基于响应式编程 (Reactive Programming) 范式的开源结构了,它结合了函数式编程、观察者形式、事情流处理等多种编程思维,然后让开发者更加高效地处理异步事情和数据流。其核心思路是将事情笼统成一个个信号,再依据需求对信号进行组合操作,最终订阅处理信号。经过运用 ReactiveObjC ,写法上由命令式改为声明式,使得代码的逻辑变得更紧凑明晰。

三、实践

场景一:会话数据源处理存在的问题

问题剖析

心遇会话页如图所示:

心遇iOS端会话页性能优化 — ReactiveObjC实践篇

会话页的数据源来源于 DataSourceDataSource 保护着一个有序的会话数组,内部监听着各种事情,比方会话更新、会话草稿更新、置顶会话改变等等。当触发事情后, DataSource 或许会从头绑定会话外显音讯、过滤、排序会话数组,最终告诉最上层事务侧改写页面。结构图如下:

心遇iOS端会话页性能优化 — ReactiveObjC实践篇

部分完结代码如下:

// 会话改变的IM回调
- (void)didUpdateRecentSession:(NIMRecentSession *)recentSession {
   // 更新会话的外显音讯 
   [recentSession updateLastMessage];
   // 过滤非自己宗族的会话
   [self filterFamilyRecentSession];
   // 从头排序
   [self customSortRecentSessions];
   // 告诉观察者数据改变
   [self dispatchObservers];
}
// 置顶数据改变
- (void)stickTopInfoDidUpdate:(NSArray *)infos {
   self.stickTopInfos = infos;
   [self customSortRecentSessions];
   [self dispatchObservers];
}
// 草稿箱改变
- (void)dartDidUpdate {
   [self customSortRecentSessions];
   [self dispatchObservers];
}
// 宗族数据改变
- (void)familyInfoDidUpdate {
   [self filterFamilyRecentSession];
   [self customSortRecentSessions];
   [self dispatchObservers];
}

这儿需求解说的是 [recentSession updateLastMessage] 的调用。因为心遇的事务需求,部分音讯是不需求外显到会话页的。因而当收到一条新音讯时,需求从头更新该会话的外显音讯。外显音讯的更新逻辑如下:

  • 第1步、经过 IMSDK 的接口同步获取会话最新的音讯列表
  • 第2步、倒叙遍历音讯数组,找到最新的可外显的音讯
  • 第3步、更新会话的外显音讯

其中,因为第一步的音讯列表获取是同步 DB 操作,因而有阻塞当时线程的危险。当频频接收到新音讯时,或许会引起严峻掉帧的问题。

一起, filterFamilyRecentSessioncustomSortRecentSessions 办法在内部会遍历会话数组,尽管时刻复杂度是 O(n) ,但是当会话量大且回调进入频频时,也会有一定的功能问题。

而在写法上,这儿很多选用托付的办法,逻辑涣散在各个回调中,可读性较差。一起每个回调中的逻辑又是相似的,代码冗余。

总结一下问题要害点:

  • 主线程存在很多的耗功能操作,形成卡顿。

  • 事情回调多,逻辑涣散,可读性差,欠好保护。

解决计划

解决计划:

  • 将各种事情回调笼统成信号,进行 combine 组合操作,解决逻辑涣散问题。

  • 将耗功能操作移到子线程中,并笼统成异步信号,解决卡顿问题。

  • 对组合信号运用 flattenMap 操作符,内部回来异步信号,终究生成成果信号供事务运用。

下面将按照计划,经过 ReactiveObjC 来一步步解决问题。

首要按照其核心思维,将上述的事情笼统成信号。以 familyInfoDidUpdate 回调为例,能够经过库供给的 - (RACSignal<RACTuple *> *)rac_signalForSelector:(SEL)selector 办法将托付办法转化成信号。当然,更好的做法是宗族材料管理类直接供给一个信号给外部运用,这样外部就不需求再去封装信号了。

RACSignal <RACTuple *> *familyInfoUpdateSingal = [self rac_signalForSelector:@selector(familyInfoDidUpdate)];

再以会话数组为例,考虑到外显音讯的更新是个耗时操作,因而先不处理,将源数据的改变先封装成信号 originalRecentSessionSignal

- (void)didUpdateRecentSession:(NIMRecentSession *)recentSession {
   NSArray *recentSessions = [self addRecentSession:recentSession];
   self.recentSessions = recentSessions;
}
RACSignal <NSArray <NIMRecentSession *> *> *originalRecentSessionSignal = RACObserve(self, recentSessions);

现在,一切的回调事情都现已抽成信号了。因为这些信号均会触发过滤、排序等一系列操作,因而能够将信号进行组合 combine 处理。

心遇iOS端会话页性能优化 — ReactiveObjC实践篇

RACSignal <RACTuple *> *familyInfoUpdateSingal = [self rac_signalForSelector:@selector(familyInfoDidUpdate)];
RACSignal <NSArray <NIMRecentSession *> *> *originalRecentSessionSignal = RACObserve(self, recentSessions);
...
RACSignal <RACTuple *> *combineSignal = [RACSignal combineLatest:@[originalRecentSessionSignal, stickTopInfoSignal, familyInfoUpdateSingal, stickTopInfoSignal, draftSignal, ...]];
[combineSignal subscribeNext:^(RACTuple * _Nullable value) {
       // 响应信号
       // 更新外显音讯、过滤、排序等操作
}];

combine 后的新信号 combineSignal 将会在任一回调事情触发时,告诉信号的订阅者。一起该信号的类型为 RACTuple 类型,里面是各个子信号上一次触发的值。

心遇iOS端会话页性能优化 — ReactiveObjC实践篇

到目前为止,现已将涣散的逻辑会集到了 combineSignal 的订阅回调里。但是功能问题依旧没有解决。解决功能问题最方便的操作就是将耗时操作放到子线程中,而 ReactiveObjC 供给的 flattenMap 函数能让这一异步操作的完结更为高雅。

心遇iOS端会话页性能优化 — ReactiveObjC实践篇

经过龙珠图不难发现, flattenMap 能够将一个原始信号 A 经过信号 B 转化成一个 新类型的信号 C 。在上面的例子中, combineSignal 作为原始信号 A ,异步处理数据信号作为信号 B ,终究转化成了成果信号 C ,即 recentSessionSignal 。具体代码如下:

RACSignal <NSArray <NIMRecentSession *> *> *recentSessionSignal = [[combineSignal flattenMap:^__kindof RACSignal * _Nullable(RACTuple * _Nullable value) {
   // 从tuple中拿出最新数据,传入
   return [[self flattenSignal:orignalRecentSessions stickTopInfo:stickTopInfo] deliverOnMainThread];
}];
- (RACSignal *)flattenSignal:(NSArray *)orignalRecentSessions stickTopInfo:(NSDictionary *)stickTopInfo {
   RACSignal *signal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
       dispatch_async(self.sessionBindQueue, ^{
           //  先处理:更新外显音讯、过滤排序
           NSArray *recentSessions = ...
           //  后吐出终究成果
           [subscriber sendNext:recentSessions];
           [subscriber sendCompleted];
       });
       return nil;
   }];
   return signal;
}

至此,该场景下的问题已优化结束。再简单总结下信号链路:每逢任一事情回调,都会触发信号,然后派发到子线程处理成果,终究经过成果信号 recentSessionSignal 吐出。完整信号龙珠图如下:

心遇iOS端会话页性能优化 — ReactiveObjC实践篇

场景二:会话事务数据处理存在的问题

问题剖析

因为事务隔离,会话的事务数据(比方用户材料)需求恳求事务接口去获取。

对于这段事务数据的获取逻辑,心遇是经过 BusinessBinder 去完结的,结构图如下:

心遇iOS端会话页性能优化 — ReactiveObjC实践篇

BusinessBinder 监听着数据源改变的回调,在回调内部做两件事:

  • 过滤出内存池中没有事务数据的会话,尝试从 DB 中获取数据并加载到内存池。

  • 过滤出没有恳求过事务数据的会话,批量恳求数据,在接口回调中更新内存池并缓存

事务层在改写时,经过 id 从内存池中获取对应的事务数据:

心遇iOS端会话页性能优化 — ReactiveObjC实践篇

部分完结代码如下:

- (void)recentSessionDidUpdate:(NSArray *)recentSessions {
   // 尝试从DB中加载没有内存池中没有的Data
   NSArray *unloadRecentSessions = [recentSessions bk_select:^BOOL(id obj) {
       return ![MemoryCache dataWithKey:obj.session.sessionId];
   }];
   for (recentSession in unloadRecentSessions) {
       Data *data = [DBCache dataWithKey:recentSession.session.sessionId];
       [MemoryCache cache:data forKey:recentSession.session.sessionId];
   }
   // 批量拉取未恳求过的Data
   NSArray *unfetchRecentSessionIds = [[recentSessions bk_select:^BOOL(id obj) {
       return obj.isFetch;
   }] bk_map:^id(id obj) {
       return obj.session.sessionId;
   }];
   [self fetchData:unfetchRecentSessionIds ];
}
- (void)dataDidFetch:(NSArray *)datas {
   // 在接口响应回调中缓存
   for (data in datas) {
       [MemoryCache cache:data forKey:data.id];
       [DataCache cache:data forKey:data.id];
   }
}

因为和场景一相似,这儿不做过多剖析。简单总结下问题要害点:

  • DataCache 的读写操作以及多处遍历操作均在主线程执行,存在功能问题。

解决计划

因为场景二中的操作符在场景一中已具体介绍过,因而场景二会越过介绍直接运用。场景二的核心思路和一相似:

  • 将耗时操作异步处理,并笼统成信号。

  • 将源信号、中心信号组合、操作,终究生成符合预期的成果信号。

首要, DataCache 的读取操作以及接口的拉取操作其实能够理解为同一行为,即数据获取。因而能够将这一行为笼统成一个异步信号,信号的类型为事务数据数组。触发该信号的机遇为会话数据源改变。龙珠图如下:

心遇iOS端会话页性能优化 — ReactiveObjC实践篇

图中的新信号 Data Signal 即为事务数据获取信号。该信号由场景一中的 Sessions Signal 经过 flattenMap 操作符改变而来,在 flattenMap 内部去异步读取 DataCache ,恳求接口。因为或许存在DB无数据或接口未获取到数据的状况,因而能够给 Data Signal 进行一次 filter 操作,过滤掉数据为空状况。

心遇iOS端会话页性能优化 — ReactiveObjC实践篇

其次按照上述剖析的逻辑,当会话改变时,会从 DataCache 中获取数据并更新内存池;当事务数据获取届时,也需求更新内存池。因而,能够将 Sessions SignalData Signal' 进行组合操作。

心遇iOS端会话页性能优化 — ReactiveObjC实践篇

现在,每逢会话改变或事务数据获取到,都会触发组合后的新信号 Combine Signal 。最终,经过 flattenMap 异步获取 DataCache 数据并更新内存池,生成成果信号 Result Signal

心遇iOS端会话页性能优化 — ReactiveObjC实践篇

至此,终究信号 Result Signal 即为事务数据数据获取结束并更新内存池后的信号。上层事务经过订阅该信号即可获取到事务数据获取结束的机遇。完整的龙珠图如下:

心遇iOS端会话页性能优化 — ReactiveObjC实践篇

四、小结

上述场景对于 ReactiveObjC 的运用只不过是冰山一角。它的强大之处在于经过它能够将任意的事情笼统成信号,一起它又供给了很多的操作符去转化信号,然后终究得到你想要的信号。

不可否认,诸如此类的结构的学习曲线是较陡的。但当真正理解了响应式编程思维并熟练运用后,开发效率必定会事半功倍。

五、参考文献

[1] github.com/ReactiveCoc…

[2] reactivex.io/documentati…

本文发布自网易云音乐技能团队,文章未经授权制止任何形式的转载。我们常年招收各类技能岗位,如果你准备换作业,又刚好喜欢云音乐,那就加入我们 grp.music-fe(at)corp.netease.com!