Mobile Development 12 min read

Optimizing Chat Session Page Performance with ReactiveObjC in iOS

The article demonstrates how to boost iOS chat session page performance by refactoring heavy data‑source and business‑data processing into ReactiveObjC signals, using combineLatest and flattenMap to shift intensive tasks off the main thread, improve readability, and eliminate frame drops.

NetEase Cloud Music Tech Team
NetEase Cloud Music Tech Team
NetEase Cloud Music Tech Team
Optimizing Chat Session Page Performance with ReactiveObjC in iOS

As a social product, the message session page is one of the most frequently used pages in Xinyu, making user experience crucial. With stranger social networking features, users often have thousands of sessions, presenting significant performance challenges. This article explores known performance issues in the session page, analyzes implementation drawbacks, and presents elegant solutions using ReactiveObjC.

ReactiveObjC Overview

ReactiveObjC is an open-source framework based on Reactive Programming paradigm, combining functional programming, observer pattern, and event stream processing. Its core idea is to abstract events into signals, combine and transform them based on requirements, and finally subscribe to process them. Using ReactiveObjC changes imperative code to declarative style, making logic more compact and clear.

Practice - Scenario 1: Session Data Source Processing Issues

Problem Analysis

The session page data comes from DataSource, which maintains an ordered session array and listens to various events such as session updates, draft updates, and sticky-top changes. When events trigger, DataSource may rebind display messages, filter and sort sessions, then notify the upper business layer to refresh the page.

Key implementation code:

// IM callback for session updates
- (void)didUpdateRecentSession:(NIMRecentSession *)recentSession {
// Update session's display message
[recentSession updateLastMessage];
// Filter non-family sessions
[self filterFamilyRecentSession];
// Re-sort
[self customSortRecentSessions];
// Notify observers of data changes
[self dispatchObservers];
}

The display message update logic involves: 1) Synchronously fetching the latest message list via IMSDK interface; 2) Traversing messages in reverse to find the latest displayable message; 3) Updating the session's display message. Since step 1 involves synchronous DB operations, it risks blocking the current thread and may cause severe frame drops when receiving frequent new messages.

Additionally, filterFamilyRecentSession and customSortRecentSessions iterate through the session array with O(n) complexity, causing performance issues with large session volumes and frequent callbacks.

Summary of problems:

Main thread performs many performance-intensive operations, causing stuttering.

Multiple event callbacks with scattered logic, poor readability, and difficult maintenance.

Solution

The solution involves: 1) Abstracting various event callbacks into signals and using combine operations to solve scattered logic; 2) Moving performance-intensive operations to background threads and abstracting them as async signals; 3) Using flattenMap operator on combined signals to return async signals internally, generating final result signals for business use.

First, abstract events into signals. Using familyInfoDidUpdate callback as example:

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

For session array, since display message update is time-consuming, wrap source data changes into a signal:

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

Now combine all callback events:

RACSignal <RACTuple *> *combineSignal = [RACSignal combineLatest:@[originalRecentSessionSignal, stickTopInfoSignal, familyInfoUpdateSingal, stickTopInfoSignal, draftSignal, ...]];
[combineSignal subscribeNext:^(RACTuple * _Nullable value) {
// Respond to signal
// Update display messages, filter, sort, etc.
}];

To solve performance issues, use flattenMap to move time-consuming operations to background threads:

RACSignal <NSArray <NIMRecentSession *> *> *recentSessionSignal = [[combineSignal flattenMap:^__kindof RACSignal * _Nullable(RACTuple * _Nullable value) {
// Extract latest data from 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, ^{
// First: update display messages, filter and sort
NSArray *recentSessions = ...
// Then output final result
[subscriber sendNext:recentSessions];
[subscriber sendCompleted];
});
return nil;
}];
return signal;
}

Practice - Scenario 2: Session Business Data Processing Issues

Problem Analysis

Due to business isolation, session business data (such as user profiles) needs to be fetched via business interfaces. Xinyu uses BusinessBinder to handle this. BusinessBinder listens to data source change callbacks and: 1) Filters sessions without business data in memory pool and tries to load from DB; 2) Filters sessions without business data requests and batch requests data, updating and caching in memory pool via interface callbacks.

Key problems: DataCache read/write operations and multiple traversal operations execute on main thread, causing performance issues.

Solution

Similar to Scenario 1: 1) Move time-consuming operations to async processing and abstract as signals; 2) Combine source signals and intermediate signals to generate expected result signals.

DataCache read operations and interface fetch operations can be abstracted as the same behavior - data fetching. Abstract this as an async signal triggered when session data source changes. The new Data Signal is transformed from Sessions Signal via flattenMap operator, which asynchronously reads DataCache and requests interfaces. Apply filter to remove empty data cases.

Combine Sessions Signal and Data Signal'. Whenever session changes or business data is fetched, the combined signal triggers. Finally, use flattenMap to asynchronously fetch DataCache data and update memory pool, generating Result Signal.

Summary

The above scenarios only scratch the surface of ReactiveObjC's capabilities. Its power lies in abstracting any event into signals and providing numerous operators to transform signals to achieve desired results. The learning curve is steep, but once mastered, development efficiency will be greatly improved.

Mobile DevelopmentReactive Programmingsignal processingAsync OperationsiOS performance optimizationReactiveObjC
NetEase Cloud Music Tech Team
Written by

NetEase Cloud Music Tech Team

Official account of NetEase Cloud Music Tech Team

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.