Mobile Development 14 min read

How NetEase Cloud Music Achieved Seamless RN Upgrades with a Dual‑Dynamic‑Library Gray Release

This article details NetEase Cloud Music's engineering solution for gradually upgrading its iOS React Native version using a dual dynamic‑library gray‑release strategy, covering background, challenges, symbol handling, code modifications, and practical issues to enable zero‑impact, data‑driven rollouts.

NetEase Cloud Music Tech Team
NetEase Cloud Music Tech Team
NetEase Cloud Music Tech Team
How NetEase Cloud Music Achieved Seamless RN Upgrades with a Dual‑Dynamic‑Library Gray Release

Background

NetEase Cloud Music has over 100 business modules built with React Native (RN), accounting for more than 30% of its iOS codebase. Upgrading to a new RN version (0.70) is critical for stability, and TestFlight no longer supports unlimited distribution via email deletion, necessitating a non‑intrusive gray‑release mechanism.

Problem Statement and Challenges

To perform a progressive upgrade, two RN versions must coexist, controlled by an AB test that routes the C group to the old version and the T group to the new one. The main challenges are:

Static linking cannot handle duplicate symbols, making manual symbol renaming impractical for a large codebase.

Dynamic linking requires loading separate RN dynamic libraries at runtime, but static linking still fails when business code references RN symbols.

Proposed Dual‑Dynamic‑Library Solution

The team introduced an intermediate layer called NEReactNative that declares all RN symbols needed by business code, ensuring static linking succeeds while remaining invisible to the business layer. At runtime, the appropriate RN dynamic library is loaded based on AB test results, and placeholder symbols are bound to real symbols.

Architecture diagram
Architecture diagram

Symbol Retrieval

A utility class collects addresses of global variables, functions, and class symbols when building the new and old RN dynamic libraries.

@interface NEReactNativeDynamicFramework : NSObject
+ (Class _Nullable)getClass:(NSString *)name;
+ (void *_Nullable)getSymbol:(NSString *)name;
@end

@implementation NEReactNativeDynamicFramework
static NSMutableDictionary<NSString *, NSValue> *symbols;
static NSMutableDictionary<NSString *, NSValue> *classes;
+ (void)prepare {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        symbols = [NSMutableDictionary dictionary];
        classes = [NSMutableDictionary dictionary];
        // TODO: populate symbols and classes
    });
}
+ (Class _Nullable)getClass:(NSString *)name {
    [self prepare];
    return (__bridge Class)[classes[name] pointerValue];
}
+ (void *_Nullable)getSymbol:(NSString *)name {
    [self prepare];
    return [symbols[name] pointerValue];
}
@end

Global symbols are declared with extern to let the linker bind them automatically.

#define INCLUDE_SYMBOL(NAME) \
    do { \
        __attribute__((visibility("hidden"))) extern void NAME; \
        symbols[@(#NAME)] = [NSValue valueWithPointer:&NAME]; \
    } while (0)

INCLUDE_SYMBOL(RCTJavaScriptDidLoadNotification);
INCLUDE_SYMBOL(RCTBridgeModuleNameForClass);

Global Variable/Function Replacement

Placeholder variables are defined and later replaced with real addresses:

#define NE_VAR_SYMBOL_DECLARE(NAME) \
    extern void * NAME; \
    void * NAME;

#define NE_VAR_SYMBOL_LOAD(NAME) \
    NAME = *(void **)[NEReactNativeDynamicFramework getSymbol:@(#NAME)];

NE_VAR_SYMBOL_DECLARE(RCTJavaScriptDidLoadNotification)

@implementation NEReactNativeGlobalSymbolLoader (variables)
+ (void)loadGlobalVariables {
    NE_VAR_SYMBOL_LOAD(RCTJavaScriptDidLoadNotification);
}
@end

Functions use a naked assembly stub that jumps to the real implementation:

#if __x86_64__
#define _JMP_TO(PTR) __asm__ volatile("JMP *%0" : : "r"(PTR));
#elif __arm64__
#define _JMP_TO(PTR) __asm__ volatile("BR %0" : : "r"(PTR));
#endif

#define NE_FUN_SYMBOL_DECLARE(NAME) \
    static void *SYM_##NAME = NULL; \
    FOUNDATION_EXPORT void NAME(void); \
    __attribute__((naked)) void NAME(void) { \
        _JMP_TO(SYM_##NAME); \
    }

#define NE_FUN_SYMBOL_LOAD(NAME) \
    SYM_##NAME = [NEReactNativeDynamicFramework getSymbol:@(#NAME)];

NE_FUN_SYMBOL_DECLARE(RCTBridgeModuleNameForClass)

@implementation NEReactNativeGlobalSymbolLoader (functions)
+ (void)loadGlobalFunctions {
    NE_FUN_SYMBOL_LOAD(RCTBridgeModuleNameForClass);
}
@end

Class Symbol Binding

Placeholder Objective‑C classes are created and at runtime swapped with real classes. The process handles:

Method forwarding for class methods.

Overriding +alloc, -init, and +new to instantiate real objects.

Injecting Category methods and Protocol lists into the real class.

Special handling for subclasses to ensure overridden methods call the subclass implementation rather than the base class.

When a subclass object is created, a proxy subclass inheriting from the real class is generated, and the proxy object is stored in brokerObject. Method calls are forwarded appropriately, preserving super semantics.

Subclass proxy diagram
Subclass proxy diagram

Implementation Issues Encountered

Direct Instance Variable Access

Some UIKit methods (e.g., -addSubview:) and RN classes ( RCTShadowView) directly access instance variables, which the method‑forwarding approach cannot intercept. The team swizzled these methods to replace the passed‑in object with the real class instance.

API Differences Between RN Versions

New RN versions expose functions such as RCTPLLabelForTag that older versions lack. The solution adds bridge functions that jump to the new implementation when available and guards calls with version checks to avoid crashes on older RN builds.

Conclusion

The intermediate layer enables a zero‑impact, data‑driven RN version switch. Business code remains unchanged, and the upgrade proceeds via AB testing: traffic is gradually shifted, metrics are collected, issues are resolved, and the new RN 0.70 version is eventually rolled out to all users.

AB testingiOSgray-releasedynamic linkingReact NativeSymbol Binding
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

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.