乐趣区

关于objective-c:心遇iOS端会话页性能优化-ReactiveObjC实践篇

本文作者:尚尧

一、背景

心遇作为一款社交产品,音讯会话页必然是用户使用量最大的页面之一,因此会话页的用户体验将尤为重要。同时,心遇有着陌生人社交属性,用户的会话量动辄上万,会话页也面临着较大的性能挑战。因而,会话页的性能优化既是重点,也是难点。

本文将举例会话页已知的性能问题,剖析实现弊病,最初通过引入 ReactiveObjC 来更优雅的解决问题。

二、ReactiveObjC 简介

ReactiveObjC 是一个基于响应式编程 (Reactive Programming) 范式的开源框架了,它联合了函数式编程、观察者模式、事件流解决等多种编程思维,从而让开发者更加高效地解决异步事件和数据流。其外围思路是将事件形象成一个个信号,再依据需要对信号进行组合操作,最初订阅解决信号。通过应用 ReactiveObjC,写法上由命令式改为申明式,使得代码的逻辑变得更紧凑清晰。

三、实际

场景一:会话数据源解决存在的问题

问题剖析

心遇会话页如图所示:

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

局部实现代码如下:

// 会话变更的 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 解决。

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 类型,外面是各个子信号上一次触发的值。

到目前为止,曾经将扩散的逻辑集中到了 combineSignal 的订阅回调里。然而性能问题仍旧没有解决。解决性能问题最不便的操作就是将耗时操作放到子线程中,而 ReactiveObjC 提供的 flattenMap 函数能让这一异步操作的实现更为优雅。

通过龙珠图不难发现,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 吐出。残缺信号龙珠图如下:

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

问题剖析

因为业务隔离,会话的业务数据(比方用户材料)须要申请业务接口去获取。

对于这段业务数据的获取逻辑,心遇是通过 BusinessBinder 去实现的,结构图如下:

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

  • 过滤出内存池中没有业务数据的会话,尝试从 DB 中获取数据并加载到内存池。
  • 过滤出没有申请过业务数据的会话,批量申请数据,在接口回调中更新内存池并缓存。

业务层在刷新时,通过 id 从内存池中获取对应的业务数据:

局部实现代码如下:

- (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 的读取操作以及接口的拉取操作其实能够了解为同一行为,即数据获取。因而能够将这一行为形象成一个异步信号,信号的类型为业务数据数组。触发该信号的机会为会话数据源变更。龙珠图如下:

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

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

当初,每当会话变更或业务数据获取到,都会触发组合后的新信号 Combine Signal。最初,通过 flattenMap 异步获取 DataCache 数据并更新内存池,生成后果信号 Result Signal

至此,最终信号 Result Signal 即为业务数据数据获取结束并更新内存池后的信号。下层业务通过订阅该信号即可获取到业务数据获取结束的机会。残缺的龙珠图如下:

四、小结

上述场景对于 ReactiveObjC 的应用只不过是冰山一角。它的弱小之处在于通过它能够将任意的事件形象成信号,同时它又提供了大量的操作符去转换信号,从而最终失去你想要的信号。

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

五、参考文献

[1] https://github.com/ReactiveCocoa/ReactiveObjC

[2] https://reactivex.io/documentation/operators.html

本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!

退出移动版