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.
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.
NetEase Cloud Music Tech Team
Official account of NetEase Cloud Music Tech Team
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.