关于前端:⚡️-React-Native-启动速度优化Native-篇内含源码分析

11次阅读

共计 18592 个字符,预计需要花费 47 分钟才能阅读完成。

Web 开发有一个经典问题:「浏览器中从输出 URL 到页面渲染的这个过程中都产生了什么?

据我考据这个问题起码有 十年 历史了。在突飞猛进学不动的前端圈子里,这个问题能始终被问,就是因为因为它是个十分好的问题,波及十分多的知识点,平时做一些性能优化,都能够从这个问题登程,剖析性能瓶颈,而后隔靴搔痒进行优化。

不过明天咱们不谈 Web 的性能优化,只是借助刚刚的那个那个经典问题的剖析思路,从 React Native 的启动到页面的第一次渲染实现,联合 React Native 的源码和 1.0 的新架构,一一剖析 React Native 的启动性能优化之路

如果你喜爱我的文章,心愿点赞👍 珍藏 📁 评论 💬 三连反对一下,谢谢你,这对我真的很重要!

浏览揭示
1. 文章中的源码内容为 RN 0.64 版本
2. 源码剖析内容波及 Objective-CJavaC++JavaScript 四门语言,我尽量讲得通俗易懂一些,若切实不了解能够间接看论断

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 性能优化指南》,我从 渲染 图片 动画 长列表 等方向介绍了 RN 渲染优化的常见套路,感兴趣的读者能够返回查看,我这里就不多介绍了。

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

提醒 :React Native 初始化时,有可能多个工作 并行执行,所以上图只能示意 React Native 初始化的大抵流程,并不和理论代码的执行时序一一对应。

1. 降级 React Native

想晋升 React Native 利用的性能,最一劳永逸的办法就是 降级 RN 的大版本 了。咱们的利用从 0.59 降级到 0.62 之后,咱们的 APP 没有做任何的性能优化工作,启动工夫间接缩短了 1/2。当 React Native 的新架构公布后,启动速度和渲染速度都会大大增强。

当然,RN 的版本升级并不容易(横跨 iOS Android JS 三端,兼容破坏性更新),我之前写过一篇《React Native 降级指南(0.59 -> 0.62)》的文章,如果有降级想法的老铁能够浏览参考一下。

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;

从头文件看出:

  • 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 办法来看看做了些什么。

// RTCxxBridge.mm

- (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 多线程 的一些知识点,用文字描述大抵是如下的流程:

  1. 初始化 js 线程 _jsThread
  2. 在主线程上注册所有 native modules
  3. 筹备 jsNative 之间的桥和 js 运行环境
  4. 在 JS 线程上创立音讯队列 RCTMessageThread,初始化 _reactInstance
  5. 在 JS 线程上加载 JS Bundle
  6. 等下面的事件全副做完后,执行 JS 代码

其实下面的六个点都能够深挖上来,然而本节波及到的源码内容到这里就能够了,感兴趣的读者能够联合我最初给出的参考资料和 React Native 源码深挖摸索一下。

Android 源码剖析

1.MainActivity.java & MainApplication.java

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

MainActivity 继承自 ReactActivityReactActivity 又继承自 AppCompatActivity

// 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);
  }
}

ReactApplication 接口很简略,要求咱们创立一个 ReactNativeHost 对象:

public interface ReactApplication {ReactNativeHost getReactNativeHost();
}

从下面的剖析咱们能够看出所有指向了 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 都要占用肯定的内存,所以什么时候实例化,实例化几个容器,池的大小限度,什么时候革除容器,都须要联合业务进行实际和摸索。

3.Native Modules 绑定

iOS 源码剖析

iOS 的 Native Modules 有 3 块儿内容,大头是两头的 _initializeModules 函数:

// RCTCxxBridge.mm

- (void)start {
  // 初始化 RCTBridge 时调用 initWithBundleURL_moduleProvider_launchOptions 中的 moduleProvider 返回的 native modules
  [self registerExtraModules];
  
  // 注册所有的自定义 Native Module
  (void)[self _initializeModules:RCTGetModuleClasses() withDispatchGroup:prepareBridge lazilyDiscovered:NO];
  
  // 初始化所有懒加载的 native module,只有用 Chrome debug 时才会调用
  [self registerExtraLazyModules];
}

咱们看看 _initializeModules 函数做了什么:

// RCTCxxBridge.mm

- (NSArray<RCTModuleData *> *)_initializeModules:(NSArray<Class> *)modules
                               withDispatchGroup:(dispatch_group_t)dispatchGroup
                                lazilyDiscovered:(BOOL)lazilyDiscovered
{for (RCTModuleData *moduleData in _moduleDataByID) {if (moduleData.hasInstance && (!moduleData.requiresMainQueueSetup || RCTIsMainQueue())) {
        // Modules that were pre-initialized should ideally be set up before
        // bridge init has finished, otherwise the caller may try to access the
        // module directly rather than via `[bridge moduleForClass:]`, which won't
        // trigger the lazy initialization process. If the module cannot safely be
        // set up on the current thread, it will instead be async dispatched
        // to the main thread to be set up in _prepareModulesWithDispatchGroup:.
        (void)[moduleData instance];
      }
    }
    _moduleSetupComplete = YES;
    [self _prepareModulesWithDispatchGroup:dispatchGroup];
}

依据 _initializeModules_prepareModulesWithDispatchGroup 的正文,能够看出 iOS 在 JS Bundle 加载的过程中(在 JSThead 线程 进行),同时在 主线程 初始化所有的 Native Modules。

联合后面的源码剖析,咱们能够看出 React Native iOS 容器初始化的时候,会 初始化所有的 Native Modules,若 Native Modules 比拟多,就会影响 Android RN 容器的启动工夫。

Android 源码剖析

对于 Native Modules 的注册,其实在 MainApplication.java 这个入口文件里曾经给出了线索:

// MainApplication.java

protected List<ReactPackage> getPackages() {@SuppressWarnings("UnnecessaryLocalVariable")
  List<ReactPackage> packages = new PackageList(this).getPackages();
  // Packages that cannot be autolinked yet can be added manually here, for example:
  // packages.add(new MyReactNativePackage());
  return packages;
}

因为 0.60 之后 React Native 启用了 auto link,装置的第三方 Native Modules 都在 PackageList 里,所以咱们只有 getPackages() 一下就能获取 auto link 的 Modules。

源码里,在 ReactInstanceManager.java 这个文件中,会运行 createReactContext() 创立 ReactContext,这外面有一步就是注册 nativeModules 的注册表:

// ReactInstanceManager.java

private ReactApplicationContext createReactContext(
  JavaScriptExecutor jsExecutor, 
  JSBundleLoader jsBundleLoader) {
  
  // 注册 nativeModules 注册表
  NativeModuleRegistry nativeModuleRegistry = processPackages(reactContext, mPackages, false);
}

依据函数调用,咱们追踪到 processPackages() 这个函数里,利用一个 for 循环把 mPackages 里的 Native Modules 全副退出注册表:

// ReactInstanceManager.java

private NativeModuleRegistry processPackages(
    ReactApplicationContext reactContext,
    List<ReactPackage> packages,
    boolean checkAndUpdatePackageMembership) {
  // 创立 JavaModule 注册表 Builder,用来创立 JavaModule 注册表,// JavaModule 注册表将所有的 JavaModule 注册到 CatalystInstance 中
  NativeModuleRegistryBuilder nativeModuleRegistryBuilder =
      new NativeModuleRegistryBuilder(reactContext, this);

  // 给 mPackages 加锁
  // mPackages 类型为 List<ReactPackage>,与 MainApplication.java 里的 packages 对应
  synchronized (mPackages) {for (ReactPackage reactPackage : packages) {
      try {
        // 循环解决咱们在 Application 里注入的 ReactPackage,解决的过程就是把各自的 Module 增加到对应的注册表中
        processPackage(reactPackage, nativeModuleRegistryBuilder);
      } finally {Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
      }
    }
  }

  NativeModuleRegistry nativeModuleRegistry;
  try {
    // 生成 Java Module 注册表
    nativeModuleRegistry = nativeModuleRegistryBuilder.build();} finally {Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
    ReactMarker.logMarker(BUILD_NATIVE_MODULE_REGISTRY_END);
  }

  return nativeModuleRegistry;
}

最初调用 processPackage() 进行真正的注册:

// ReactInstanceManager.java

private void processPackage(
    ReactPackage reactPackage,
    NativeModuleRegistryBuilder nativeModuleRegistryBuilder
) {nativeModuleRegistryBuilder.processPackage(reactPackage);
}

从下面的流程能够看出,Android 注册 Native Modules 的时候是 同步全量注册 的,若 Native Modules 比拟多,就会影响 Android RN 容器的启动工夫。

优化倡议

说实话,Native Modules 全量绑定在现有的架构里是无解的:不论这个 Native Methods 你有没有用到,容器启动时先全副初始化一遍。在新的 RN 架构里,TurboModules 会解决这个问题(本文下一大节会介绍)。

如果非要说优化,其实还有个思路,你不是全量初始化吗,那我让 Native Modules 的数量缩小不就行了?新架构里有一步叫做 Lean Core,就是精简 React Native 外围,把一些性能 / 组件从 RN 的主工程项目里移出去(例如 WebView 组件),交给社区保护,你想用的时候再独自下载集成。

这样做的益处次要有几点:

  • 外围更加精简,RN 维护者有更多的精力保护次要性能
  • 减小 Native Modules 的绑定耗时和多余的 JS 加载工夫,包体积的减小,对初始化性能更敌对(咱们降级 RN 版本到 0.62 后初始化速度晋升一倍,根本都是 Lean Core 的功绩)
  • 放慢迭代速度,优化开发体验等

当初 Lean Core 的工作根本曾经实现,更多探讨可见官网 issues 讨论区,咱们只有同步降级 React Native 版本就能够享受 Lean Core 的成绩。

4.RN 新架构如何优化启动性能

React Native 新架构曾经跳票快两年了,每次问进度,官网回复都是“别催了别催了在做了在做了”。

我集体去年期待了一整年,然而啥都没等到,所以 RN 啥时候更新到 1.0.0 版本,我曾经不在乎了。尽管 RN 官网始终在鸽,然而不得不说他们的新架构还是有些货色的,市面上存在对于 RN 新架构的文章和视频我根本都看了一遍,所以集体对新架构还是有个整体的认知。

因为新架构还没有正式放出,所以具体细节上必定还存在一些差别,具体执行细节还是要等 React Native 官网为准。

JSI

JSI 的全名是 JavaScript Interface,一个用 C++ 写的框架,作用是 反对 JS 间接调用 Native 办法,而不是当初通过 Bridge 异步通信。

JS 间接调用 Native 如何了解呢?咱们举一个最简略的例子。在浏览器上调用 setTimeout document.getElementById 这类 API 的时候,其实就是在 JS 侧间接调用 Native Code,咱们能够在浏览器控制台里验证一下:

比如说我执行了一条命令:

let el = document.createElement('div')

变量 el 持有的不是一个 JS 对象,而是一个在 C++ 中被实例化的对象。对于 el 持有的这个对象咱们再设置一下相干属性:

el.setAttribute('width', 100)

这时候其实是 JS 同步调用 C++ 中的 setWidth 办法,扭转这个元素的宽度。

React Native 新架构中的 JSI,次要就是起这个作用的,借助 JSI,咱们能够用 JS 间接取得 C++ 对象的援用(Host Objects),进而间接管制 UI,间接调用 Native Modules 的办法,省去 bridge 异步通信的开销。

上面咱们举个小例子,来看一下 Java/OC 如何借助 JSI 向 JS 裸露同步调用的办法。

#pragma once

#include <string>
#include <unordered_map>

#include <jsi/jsi.h>

// SampleJSIObject 继承自 HostObject,示意这个一个裸露给 JS 的对象
// 对于 JS 来说,JS 能够间接同步调用这个对象上的属性和办法
class JSI_EXPORT SampleJSIObject : public facebook::jsi::HostObject {

public: 

// 第一步
// 将 window.__SampleJSIObject 裸露给 JavaScript
// 这是一个动态函数,个别在利用初始化时从 ObjC/Java 中调用
static void SampleJSIObject::install(jsi::Runtime &runtime) {runtime.global().setProperty(
      runtime,
      "__sampleJSIObject",
      jsi::Function::createFromHostFunction(
          runtime,
          jsi::PropNameID::forAscii(runtime, "__SampleJSIObject"),
          1,
          [binding](jsi::Runtime& rt, const jsi::Value& thisVal, const jsi::Value* args, size_t count) {
            // 返回调用 window.__SampleJSIObject 时失去的内容
            return std::make_shared<SampleJSIObject>();}));
}

// 相似于 getter,每次 JS 拜访这个对象的时候,都要通过这个办法,作用相似于一个包装器
// 比如说咱们调用 window.__sampleJSIObject.method1(),这个办法就会被调用
jsi::Value TurboModule::get(jsi::Runtime& runtime, const jsi::PropNameID& propName) {
  // 调用办法名
  // 比如说调用 window.__sampleJSIObject.method1() 时,propNameUtf8 就是 method1
  std::string propNameUtf8 = propName.utf8(runtime);

  return jsi::Function::createFromHostFunction(
    runtime,
    propName,
    argCount,
    [](facebook::jsi::Runtime &rt, const facebook::jsi::Value &thisVal, const facebook::jsi::Value *args, size_t count) {if (propNameUtf8 == 'method1') {// 调用 method1 时,相干的函数解决逻辑}
    });
}
  
std::vector<PropNameID> getPropertyNames(Runtime& rt){}}

下面的例子比拟简短,想要深刻理解 JSI,能够看《React Native JSI Challenge》这篇文章或间接浏览源码。

TurboModules

通过后面的源码剖析,咱们能够得悉,现有架构里,Native 初始化时会 全量加载 native modules,随着业务的迭代,native modules 只会越来越多,这里的耗时会越来越长。

TurboModules 就能够一次性解决这个问题。在新架构里,native modules 是 懒加载 的,也就是说只有你调用相应的 native modules 时才会初始化加载,这样就解决了初始化全量加载耗时较长的问题。

TurboModules 的调用门路大略是这样的:

  1. 先用 JSI 创立一个顶层的「Native Modules Proxy」,称之为 global.__turboModuleProxy
  2. 拜访一个 Native Modules,比如说要拜访 SampleTurboModule,咱们先在 JavaScript 侧执行 require('NativeSampleTurboModule')
  3. 在 NativeSampleTurboModule.js 这个文件里,咱们先调用 TurboModuleRegistry.getEnforcing(),而后就会调用 global.__turboModuleProxy("SampleTurboModule")
  4. 调用 global.__turboModuleProxy 的时候,就会调用第一步 JSI 裸露的 Native 办法,这时候 C++ 层通过传入的字符串 “SampleTurboModule”,找到 ObjC/Java 的实现,最初返回一个对应的 JSI 对象
  5. 当初咱们失去了 SampleTurboModule 的 JSI 对象,就能够用 JavaScript 同步调用 JSI 对象上的属性和办法

通过下面的步骤,咱们能够看到借助 TurboModules,Native Modules 只有 首次调用 的时候才会加载,这样就彻底干掉 React Native 容器初始化时 全量加载 Native Modules 时的工夫;同时咱们能够借助 JSI 实现 JS 和 Native 的同步调用,耗时更少,效率更高。

总结

本文次要从 Native 的角度登程,从源码剖析 React Native 现有架构 的启动流程,总结了几个 Native 层的性能优化点;最初又简略介绍了一下 React Native 的 新架构。下一篇文章我会解说如何从 JavaScript 动手,优化 React Native 的启动速度。


如果你喜爱我的文章,心愿点赞👍 珍藏 📁 评论 💬 三连反对一下,谢谢你,这对我真的很重要!

欢送大家关注我的微信公众号:卤蛋实验室,目前专一前端技术,对图形学也有一些渺小钻研。

原文链接 👉 ⚡️ React Native 启动速度优化——Native 篇(内含源码剖析):更新更及时,浏览体验更佳

参考

React Native 性能优化指南

React Native 降级指南(0.59 -> 0.62)

Chain React 2019 – Ram Narasimhan – Performance in React Native

React Native’s new architecture – Glossary of terms

React Native JSI Challenge

RFC0002: Turbo Modules ™

ReactNative 与 iOS 原生通信原理解析 - 初始化

React Native iOS 源码解析

ReactNative 源码篇:源码初识

如何用 React Native 预加载计划解决白屏问题


正文完
 0