HarmonyOS 鸿蒙Next React Native 启动速度优化——Native 篇(内含源码分析)

发布于 1周前 作者 zlyuanteng 来自 鸿蒙OS

HarmonyOS 鸿蒙Next React Native 启动速度优化——Native 篇(内含源码分析)

0. React Native 启动流程

React Native 作为一个 Web 前端友好的混合开发框架,启动时可以大致分为两个部分:

  • Native 容器的运行
  • JavaScript 代码的运行

其中 Native 容器启动在现有架构(版本号小于 1.0.0)里:大致可以分为 3 个部分:

  • Native 容器初始化
  • Native Modules 的全量绑定
  • JSEngine 的初始化

容器初始化后,舞台就交给了 JavaScript,流程可以细分为 2 个部分:

  • JavaScript 代码的加载、解析和执行
  • JS Component 的构建

最后 JS Thread 把计算好的布局信息发送到 Native 端,计算 Shadow Tree,最后由 UI Thread 进行布局和渲染。

上面的几个步骤,我画了一张图,下面我以这张图为目录,从左向右介绍各个步骤的优化方向:

![图示]

提示:React Native 初始化时,有可能多个任务并行执行,所以上图只能表示 React Native 初始化的大致流程,并不和实际代码的执行时序一一对应。

1. 升级 React Native

想提升 React Native 应用的性能,最一劳永逸的方法就是升级 RN 的大版本了。我们的应用从 0.59 升级到 0.62 之后,我们的 APP 没有做任何的性能优化工作,启动时间直接缩短了 1/2。当 React Native 的新架构发布后,启动速度和渲染速度都会大大加强。

2. Native 容器初始化

![图示]

容器的初始化肯定是从 APP 的入口文件开始分析,下面我会挑选一些关键代码,梳理一下初始化的流程。

iOS 源码分析

1. AppDelegate.m

AppDelegate.m 是 iOS 的入口文件,代码非常精简,主要内容如下所示:

// AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  // 1.初始化一个 RCTBridge 实现加载 jsbundle 的方法
  RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];

  // 2.利用 RCTBridge 初始化一个 RCTRootView
  RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
                                                    moduleName:@"RN64"
                                           initialProperties:nil];

  // 3.初始化 UIViewController
  self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  UIViewController *rootViewController = [UIViewController new];

  // 4.将 RCTRootView 赋值给 UIViewController 的 view
  rootViewController.view = rootView;
  self.window.rootViewController = rootViewController;
  [self.window makeKeyAndVisible];
  return YES;
}

总的来看入口文件就做了三件事:

  • 初始化一个 RCTBridge 实现加载 jsbundle 的方法
  • 利用 RCTBridge 初始化一个 RCTRootView
  • RCTRootView 赋值给 UIViewController 的 view 实现 UI 的挂载

从入口源码我们可以发现,所有的初始化工作都指向 RCTRootView,所以接下来我们看看 RCTRootView 干了些啥。

2. RCTRootView

我们先看一下 RCTRootView 的头文件,删繁就简,我们只看我们关注的一些方法:

// RCTRootView.h
@interface RCTRootView : UIView
// AppDelegate.m 中用到的初始化方法
- (instancetype)initWithBridge:(RCTBridge *)bridge
                    moduleName:(NSString *)moduleName
            initialProperties:(nullable NSDictionary *)initialProperties NS_DESIGNATED_INITIALIZER;
@end

从头文件看出:

  • RCTRootView 继承自 UIView,所以它本质上就是一个 UI 组件;
  • RCTRootView 调用 initWithBridge 初始化时要传入一个已经初始化的 RCTBridge

RCTRootView.m 文件里,initWithBridge 初始化时会监听一系列的 JS 加载监听函数,监听到 JS Bundle 文件加载结束后,就会调用 JS 里的 AppRegistry.runApplication(),启动 RN 应用。

分析到这里,我们发现 RCTRootView.m 只是实现了对 RCTBridge 的各种事件监听,并不是初始化的核心,所以我们就又要转到 RCTBridge 这个文件上去。

3. RCTBridge.m

RCTBridge.m 里,初始化的调用路径有些长,全贴源码有些长,总之最后调用的是 (void)setUp,核心代码如下:

- (Class)bridgeClass
{
  return [RCTCxxBridge class];
}

- (void)setUp {
  // 获取bridgeClass 默认是 RCTCxxBridge
  Class bridgeClass = self.bridgeClass;
  // 初始化 RTCxxBridge
  self.batchedBridge = [[bridgeClass alloc] initWithParentBridge:self];
  // 启动 RTCxxBridge
  [self.batchedBridge start];
}

我们可以看到,RCTBridge 的初始化又指向了 RTCxxBridge

4. RTCxxBridge.mm

RTCxxBridge 可以说是 React Native 初始化的核心,我查阅了一些资料,貌似 RTCxxBridge 曾用名为 RCTBatchedBridge,所以可以粗暴的把这两个类当成一回事儿。

因为在 RCTBridge 里调用了 RTCxxBridgestart 方法,我们就从 start 方法来看看做了些什么。

- (void)start {
  // 1.初始化 JSThread,后续所有的 js 代码都在这个线程里面执行
  _jsThread = [[NSThread alloc] initWithTarget:[self class] selector:@selector(runRunLoop) object:nil];
  [_jsThread start];

  // 创建并行队列
  dispatch_group_t prepareBridge = dispatch_group_create();

  // 2.注册所有的 native modules
  [self registerExtraModules];
  (void)[self _initializeModules:RCTGetModuleClasses() withDispatchGroup:prepareBridge lazilyDiscovered:NO];

  // 3.初始化 JSExecutorFactory 实例
  std::shared_ptr<JSExecutorFactory> executorFactory;

  // 4.初始化底层 Instance 实例,也就是 _reactInstance
  dispatch_group_enter(prepareBridge);
  [self ensureOnJavaScriptThread:^{
    [weakSelf _initializeBridge:executorFactory];
    dispatch_group_leave(prepareBridge);
  }];

  // 5.加载 js 代码
  dispatch_group_enter(prepareBridge);
  __block NSData *sourceCode;
  [self
    loadSource:^(NSError *error, RCTSource *source) {
      if (error) {
        [weakSelf handleError:error];
      }
      sourceCode = source.data;
      dispatch_group_leave(prepareBridge);
    }
    onProgress:^(RCTLoadingProgress *progressData) {}
  ];

  // 6.等待 native moudle 和 JS 代码加载完毕后就执行 JS
  dispatch_group_notify(prepareBridge, dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
    RCTCxxBridge *strongSelf = weakSelf;
    if (sourceCode && strongSelf.loading) {
      [strongSelf executeSourceCode:sourceCode sync:NO];
    }
  });
}

上面代码比较长,里面用到了 GCD 多线程的一些知识点,用文字描述大致是如下的流程:

  • 初始化 js 线程 _jsThread
  • 在主线程上注册所有 native modules
  • 准备 jsNative 之间的桥和 js 运行环境
  • 在 JS 线程上创建消息队列 RCTMessageThread,初始化 _reactInstance
  • 在 JS 线程上加载 JS Bundle
  • 等上面的事情全部做完后,执行 JS 代码

其实上面的六个点都可以深挖下去,但是本节涉及到的源码内容到这里就可以了,感兴趣的读者可以结合我最后给出的参考资料和 React Native 源码深挖探索一下。

Android 源码分析

1. MainActivity.java & MainApplication.java

和 iOS 一样,启动流程我们先从入口文件开始分析,我们先看 MainActivity.java

// MainActivity.java
public class MainActivity extends ReactActivity {
  // 返回组件名,和 js 入口注册名字一致
  @Override
  protected String getMainComponentName() {
    return "rn_performance_demo";
  }
}

我们再从 Android 的入口文件 MainApplication.java 开始分析:

// MainApplication.java
public class MainApplication extends Application implements ReactApplication {
  private final ReactNativeHost mReactNativeHost =
    new ReactNativeHost(this) {
      // 返回 app 需要的 ReactPackage,添加需要加载的模块,这个地方就是我们在项目中添加依赖包时需要添加第三方 package 的地方
      @Override
      protected List<ReactPackage> getPackages() {
        @SuppressWarnings("UnnecessaryLocalVariable")
        List<ReactPackage> packages = new PackageList(this).getPackages();
        return packages;
      }

      // js bundle 入口文件,设置为 index.js
      @Override
      protected String getJSMainModuleName() {
        return "index";
      }
    };

  @Override
  public ReactNativeHost getReactNativeHost() {
    return mReactNativeHost;
  }

  @Override
  public void onCreate() {
    super.onCreate();
    // SoLoader:加载C++底层库
    SoLoader.init(this, /* native exopackage */ false);
  }
}

从上面的分析我们可以看出一切指向了 ReactNativeHost 这个类,下面我们就看一下它。

2. ReactNativeHost.java

ReactNativeHost 主要的工作就是创建了 ReactInstanceManager:

public abstract class ReactNativeHost {
  protected ReactInstanceManager createReactInstanceManager() {
    ReactMarker.logMarker(ReactMarkerConstants.BUILD_REACT_INSTANCE_MANAGER_START);
    ReactInstanceManagerBuilder builder =
      ReactInstanceManager.builder()
      .setApplication(mApplication)
      // JSMainModulePath 相当于应用首页的 js Bundle,可以传递 url 从服务器拉取 js Bundle
      // 当然这个只在 dev 模式下可以使用
      .setJSMainModulePath(getJSMainModuleName())
      // 是否开启 dev 模式
      .setUseDeveloperSupport(getUseDeveloperSupport())
      // 红盒的回调
      .setRedBoxHandler(getRedBoxHandler())
      .setJavaScriptExecutorFactory(getJavaScriptExecutorFactory())
      .setUIImplementationProvider(getUIImplementationProvider())
      .setJSIModulesPackage(getJSIModulePackage())
      .setInitialLifecycleState(LifecycleState.BEFORE_CREATE);

    // 添加 ReactPackage
    for (ReactPackage reactPackage : getPackages()) {
      builder.addPackage(reactPackage);
    }

    // 获取 js Bundle 的加载路径
    String jsBundleFile = getJSBundleFile();
    if (jsBundleFile != null) {
      builder.setJSBundleFile(jsBundleFile);
    } else {
      builder.setBundleAssetName(Assertions.assertNotNull(getBundleAssetName()));
    }
    ReactInstanceManager reactInstanceManager = builder.build();
    return reactInstanceManager;
  }
}

3. ReactActivityDelegate.java

我们再回到 ReactActivity,它自己并没有做什么事情,所有的功能都由它的委托类 ReactActivityDelegate 来完成,所以我们直接看 ReactActivityDelegate 是怎么实现的:

public class ReactActivityDelegate {
  protected void onCreate(Bundle savedInstanceState) {
    String mainComponentName = getMainComponentName();
    mReactDelegate =
      new ReactDelegate(
        getPlainActivity(), getReactNativeHost(), mainComponentName, getLaunchOptions()) {
          @Override
          protected ReactRootView createRootView() {
            return ReactActivityDelegate.this.createRootView();
          }
        };
    if (mMainComponentName != null) {
      // 载入 app 页面
      loadApp(mainComponentName);
    }
  }

  protected void loadApp(String appKey) {
    mReactDelegate.loadApp(appKey);
    // Activity 的 setContentView() 方法
    getPlainActivity().setContentView(mReactDelegate.getReactRootView());
  }
}

onCreate() 的时候又实例化了一个 ReactDelegate,我们再看看它的实现。

4. ReactDelegate.java

ReactDelegate.java 里,我没看见它做了两件事:

  • 创建 ReactRootView 作为根视图
  • 调用 getReactNativeHost().getReactInstanceManager() 启动 RN 应用
public class ReactDelegate {
  public void loadApp(String appKey) {
    if (mReactRootView != null) {
      throw new IllegalStateException("Cannot loadApp while app is already running.");
    }
    // 创建 ReactRootView 作为根视图
    mReactRootView = createRootView();
    // 启动 RN 应用
    mReactRootView.startReactApplication(
      getReactNativeHost().getReactInstanceManager(), appKey, mLaunchOptions);
  }
}

基础的启动流程本节涉及到的源码内容到这里就可以了,感兴趣的读者可以结合我最后给出的参考资料和 React Native 源码深挖探索一下。

优化建议

对于 React Native 为主体的应用,APP 启动后就要立马初始化 RN 容器,基本上没有什么优化思路;但是 Native 为主的混合开发 APP 却有招:

既然初始化耗时最长,我们在正式进入 React Native 容器前提前初始化不就好了?

这个方法非常的常见,因为很多 H5 容器也是这样做的。正式进入 WebView 网页前,先做一个 WebView 容器池,提前初始化 WebView,进入 H5 容器后,直接加载数据渲染,以达到网页秒开的效果。

RN 容器池这个概念看着很玄乎,其实就是一个 Mapkey 为 RN 页面的 componentName(即 AppRegistry.registerComponent(appName, Component) 中传入的 appName),value 就是一个已经实例化的 RCTRootView/ReactRootView

APP 启动后找个触发时机提前初始化,进入 RN 容器前先读容器池,如果有匹配的容器,直接拿来用即可,没有匹配的再重新初始化。

写两个很简单的案例,iOS 可以如下图所示,构建 RN 容器池:

@property (nonatomic, strong) NSMutableDictionary<NSString *, RCTRootView *> *rootViewRool;

// 容器池
-(NSMutableDictionary<NSString *, RCTRootView *> *)rootViewRool {
  if (!_rootViewRool) {
    _rootViewRool = @{}.mutableCopy;
  }
  return _rootViewRool;
}

// 缓存 RCTRootView
-(void)cacheRootView:(NSString *)componentName path:(NSString *)path props:(NSDictionary *)props bridge:(RCTBridge *)bridge {
  // 初始化
  RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
                                                      moduleName:componentName
                                             initialProperties:props];
  // 实例化后要加载到屏幕的最下面,否则不能触发视图渲染
  [[UIApplication sharedApplication].keyWindow.rootViewController.view insertSubview:rootView atIndex:0];
  rootView.frame = [UIScreen mainScreen].bounds;

  // 把缓存好的 RCTRootView 放到容器池中
  NSString *key = [NSString stringWithFormat:@"%@_%@", componentName, path];
  self.rootViewRool[key] = rootView;
}

// 读取容器
-(RCTRootView *)getRootView:(NSString *)componentName path:(NSString *)path props:(NSDictionary *)props bridge:(RCTBridge *)bridge {
  NSString *key = [NSString stringWithFormat:@"%@_%@", componentName, path];
  RCTRootView *rootView = self.rootViewRool[key];
  if (rootView) {
    return rootView;
  }
  // 兜底逻辑
  return [[RCTRootView alloc] initWithBridge:bridge moduleName:componentName initialProperties:props];
}

Android 如下构建 RN 容器池:

private HashMap<String, ReactRootView> rootViewPool = new HashMap<>();

// 创建容器
private ReactRootView createRootView(String componentName, String path, Bundle props, Context context) {
  ReactInstanceManager bridgeInstance = ((ReactApplication) application).getReactNativeHost().getReactInstanceManager();
  ReactRootView rootView = new ReactRootView(context);
  if(props == null) {
    props = new Bundle();
  }
  props.putString("path", path);
  rootView.startReactApplication(bridgeInstance, componentName, props);
  return rootView;
}

// 缓存容器
public void cahceRootView(String componentName, String path, Bundle props, Context context) {
  ReactRootView rootView = createRootView(componentName, path, props, context);
  String key = componentName + "_" + path;
  // 把缓存好的 RCTRootView 放到容器池中
  rootViewPool.put(key, rootView);
}

// 读取容器
public ReactRootView getRootView(String componentName, String path, Bundle props, Context context) {
  String key = componentName + "_" + path;
  ReactRootView rootView = rootViewPool.get(key);
  if (rootView != null) {
    rootView.setAppProperties(newProps);
    rootViewPool.remove(key);
    return rootView;
  }
  // 兜底逻辑
  return createRootView(componentName, path, props, context);
}

当然,由于每次 RCTRootView/ReactRootView 都要占用一定的内存,所以什么时候实例化,实例化几个容器,池的大小限制,什么时候清除容器,都需要结合业务进行实践和摸索。


更多关于HarmonyOS 鸿蒙Next React Native 启动速度优化——Native 篇(内含源码分析)的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html

6 回复

眼睛看了,但脑袋不会,嘻嘻

更多关于HarmonyOS 鸿蒙Next React Native 启动速度优化——Native 篇(内含源码分析)的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


你们太厉害啦,我可不行

快速上手有办法么

这个我真没研究过,无奈

膜拜呀,请接受我的膝盖

针对帖子标题“HarmonyOS 鸿蒙Next React Native 启动速度优化——Native 篇(内含源码分析)”,以下是对该问题的专业回答:

在HarmonyOS鸿蒙系统中,针对React Native应用的启动速度优化,尤其是在Native层面的优化,主要涉及到以下几个方面:

  1. 减少初始化时间:检查并优化React Native框架及依赖库的初始化流程,减少不必要的操作和资源加载,以缩短应用启动时的准备时间。

  2. 优化代码和资源加载:对React Native应用的代码和资源进行精细化管理和优化,如按需加载、代码拆分等,以减少启动时的I/O操作。

  3. 并行处理:在Native层面实现启动流程中的并行处理,如异步加载资源、后台初始化组件等,以提高整体启动效率。

  4. 源码分析:深入分析React Native及HarmonyOS鸿蒙系统的源码,找出影响启动速度的关键因素,并针对性地进行优化。

  5. 性能监控:利用HarmonyOS提供的性能监控工具,对React Native应用的启动过程进行实时监控和分析,以便及时发现并解决问题。

在进行上述优化时,开发者需要深入理解React Native及HarmonyOS的底层机制,并结合实际应用场景进行针对性的调整。如果问题依旧没法解决请联系官网客服,官网地址是:https://www.itying.com/category-93-b0.html

回到顶部