详解React Native渲染原理

前言

《一篇文章详解React Native初始化和通信机制》中我们详细的介绍了React Native的初始化和通信机制。如果对通信机制不了的的读者可以先去阅读通信机制。

React Native 本质上是以 React 为框架,React Native本质是通过JS(React)的DSL实现业务逻和UI的描述;最终通过调用Native的API实现视图的渲染。所以最终开发出来的页面视图是是纯Native组件。本文会通过源码分析的方式剖析React Native中视图的创建、更新、渲染原理。

JSX

JSX是一个 JavaScript 的语法扩展,可以简单理解为 JavaScript + XML 的语法糖。React虽然不强制要求使用JSX,但官方建议使用,因为JSX可以很好地描述 UI 应该呈现出它应有交互的本质形式。JSX 可能会使人联想到模版语言,但它具有 JavaScript 的全部功能。如下就是一个JSX语句:

const element = <h1>Hello, world!</h1>;

由于JSX是一种语法糖,所以在bundle打包过程中,以上的JSX语法会被Babel转换成普通JS语句,如下:

const element = React.createElement("h1", null, "Hello, world!");

可以通过babel compiler体验在线JSX转换。

React vs ReactNative

如上图,参考自这篇文章。上图呈现了React和ReactNative的大致渲染过程。如果你了解React.js 的渲染过程,那么去理解ReactNative就很容易。蓝色是React具备的能力,黄色是ReactNative特有的能力。虚线框里面的是React和ReactNative通用的部分。不同的是Render,ReactNative的View不是浏览器渲染的,而是Native侧渲染的view。所以ReactNative 可以理解是 React.js 在Native上的一种翻译,为了完成这种React到Native语法的解释,native侧也就必须具备解释这些渲染语法的能力,常见的就是yoga。 因为那个O(n)复杂度的Diff算法是基于 Virtual DOM, 也就是ReactElement在内存中的一种组织形式,所以这一部分也被利用在了ReactNative上。综上,不难看出ReactNative和React的最大的差别在于渲染上的差别。即React使用浏览器进行渲染,而ReactNative使用Native进行渲染。

在上一篇文章中我们说到,JS代码加载完毕后会发送一个通知给RCTRootView。RCTRootView会执行runApplication相关的逻辑:

// RCTRootView.m

- (void)javaScriptDidLoad:(NSNotification *)notification
{
  RCTAssertMainQueue();
  RCTBridge *bridge = notification.userInfo[@"bridge"];
  if (bridge != _contentView.bridge) {
    [self bundleFinishedLoading:bridge];
  }
}

- (void)bundleFinishedLoading:(RCTBridge *)bridge
{
  // 省略创建RCTRootContentView...
  
  [self runApplication:bridge];
  
  // 省略添加一个RCTRootContentView...
}

- (void)runApplication:(RCTBridge *)bridge
{
  NSString *moduleName = _moduleName ?: @""; // 这里是@"NewProject"
  NSDictionary *appParameters = @{
    @"rootTag": _contentView.reactTag,
    @"initialProps": _appProperties ?: @{},
  };

  [bridge enqueueJSCall:@"AppRegistry"
                 method:@"runApplication"
                   args:@[moduleName, appParameters]
             completion:NULL];
}

RCTRootViewrunApplication:方法以_moduleName_contentView.reactTag以及_appProperties为参数调用 JS 侧AppRegistryrunApplication方法。

说到AppRegistry,我们不得不跳到JS侧

在 RN 中,根组件(root components)需要通过AppRegistryregisterComponent方法进行注册。所谓根组件,就是 Native to JS 的入口,Native 在加载 RN bundle 之后可通过AppRegistryrunApplication方法运行指定的根组件,从而进入 RN 的世界。

AppRegistry

注册根组件

// index.js

import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';

// 此处appName需要和Native侧的保持一致,即“NewProject”
AppRegistry.registerComponent(appName, () => App);

/************************************************************************/

// 路径:react-native/Libraries/ReactNative/AppRegistry.js
// 代码有精简...
const AppRegistry = {
  registerComponent(
    appKey: string,
    componentProvider: ComponentProvider,
    section?: boolean,
  ): string {
    let scopedPerformanceLogger = createPerformanceLogger();
    // 以appKey为key注册一个名为run的箭头函数
    runnables[appKey] = {
      componentProvider,
      run: appParameters => {
        // run本质上是调用的renderApplication函数
        renderApplication(
          componentProviderInstrumentationHook(
            componentProvider,
            scopedPerformanceLogger,
          ),
          appParameters.initialProps,
          appParameters.rootTag,
          wrapperComponentProvider && wrapperComponentProvider(appParameters),
          appParameters.fabric,
          showFabricIndicator,
          scopedPerformanceLogger,
        );
      },
    };
    if (section) {
      sections[appKey] = runnables[appKey];
    }
    return appKey;
  },
  
  runApplication(appKey: string, appParameters: any): void {
    // 代码有精简...
    runnables[appKey].run(appParameters);
  }
}

如上,不难看出AppRegistry是一个常量,以key-value的形式存储了若干个函数,包括registerComponent和runApplication。registerComponent中以appKey(此文中是"NewProject")为key向注册表runnables中存储了一个对象。该对象主要包括以run为 key 存储的箭头函数,run中调用了renderApplication方法。所以在Native侧的RCTRootView中调用AppRegistry的runApplication最终会调用到renderApplication。

注意:定义根组件时调用AppRegistry.registerComponent方法的 key 与在RCTRootViewrunApplication:中调用AppRegistry#runApplication时的 key 需要一致(在例子中都是NewProject)。只有appKey保持一致,JS#runApplication才能从注册表runnables中取出箭头函数执行渲染逻辑。

上面说到runApplication最终调用renderApplication,让我们再来看下renderApplication的实现:

// 路径:react-native/Libraries/ReactNative/renderApplication.js

function renderApplication<Props: Object>(
  RootComponent: React.ComponentType<Props>,
  initialProps: Props,
  rootTag: any,
  WrapperComponent?: ?React.ComponentType<*>,
  fabric?: boolean,
  showFabricIndicator?: boolean,
  scopedPerformanceLogger?: IPerformanceLogger,
) {

  const renderable = (
    <PerformanceLoggerContext.Provider
      value={scopedPerformanceLogger ?? GlobalPerformanceLogger}>
      <AppContainer rootTag={rootTag} WrapperComponent={WrapperComponent}>
        <RootComponent {...initialProps} rootTag={rootTag} />
        {fabric === true && showFabricIndicator === true ? (
          <ReactFabricIndicator />
        ) : null}
      </AppContainer>
    </PerformanceLoggerContext.Provider>
  );

  GlobalPerformanceLogger.startTimespan('renderApplication_React_render');
  if (fabric) {
    require('../Renderer/shims/ReactFabric').render(renderable, rootTag);
  } else {
    require('../Renderer/shims/ReactNative').render(renderable, rootTag);
  }
  GlobalPerformanceLogger.stopTimespan('renderApplication_React_render');
}

module.exports = renderApplication;

不难看出,renderApplication最终是调用了ReactFabric或ReactNative的render方法。

注意:值得注意的是,ReactFabric或ReactNative的render的方法并不是直接渲染我们传入的RootComponent,而是在其外面包了一层——AppContainer
AppContainer是一个 React Component,其中封装了Inspector、YellowBox等debug工具。我们最不愿看到的出错时的红色界面也是在该组件中加载的。

下图是renderApplication函数的调用堆栈:


renderApplication调用栈

上述使用chrome远程调试的debug环境下调用到了ReactNative#render方法,我们看下ReactNative的render实现:

// 路径:react-native/Libraries/Renderer/shims/ReactNative.js

import type {ReactNativeType} from './ReactNativeTypes';

let ReactNative;

if (__DEV__) {
  ReactNative = require('../implementations/ReactNativeRenderer-dev');
} else {
  ReactNative = require('../implementations/ReactNativeRenderer-prod');
}

module.exports = (ReactNative: ReactNativeType)

我们看下在dev环境下的render的实现:

// 路径:react-native/Libraries/Renderer/implementations/ReactNativeRenderer-dev.js
 render: function(element, containerTag, callback) {
    var root = roots.get(containerTag);

    if (!root) {
      // TODO (bvaughn): If we decide to keep the wrapper component,
      // We could create a wrapper for containerTag as well to reduce special casing.
      root = createContainer(containerTag, LegacyRoot, false);
      roots.set(containerTag, root);
    }
    updateContainer(element, root, null, callback);

    return getPublicRootInstance(root);
  },

然后经由render调用updateContainer方法,最后经过一系列方法的调用,最终调用到了ReactNativeRenderer-dev

的createInstance,然后createInstance内部调用了UIManager.createView,其调用栈如下:

通过下面调用栈可以看出,dev环境下render之后直到调用createView之前所有的方法调用都发生在ReactNativeRenderer-dev中。prod环境同理。


JS侧调用createView

createView顾名思义就是创建一个真正的view,既然要创建视图,那么肯定是由native侧来实现的。createView把接收4个参数,分别是reactTag、rootTag、viewName、props。下面是native侧对createView的实现:

// RCTUIManager.m
// 代码有精简

RCT_EXPORT_METHOD(createView:(nonnull NSNumber *)reactTag
                  viewName:(NSString *)viewName
                  rootTag:(nonnull NSNumber *)rootTag
                  props:(NSDictionary *)props)
{
  RCTComponentData *componentData = _componentDataByName[viewName];

  // Register shadow view
  RCTShadowView *shadowView = [componentData createShadowViewWithTag:reactTag];
  if (shadowView) {
    [componentData setProps:props forShadowView:shadowView];
    _shadowViewRegistry[reactTag] = shadowView;
    RCTShadowView *rootView = _shadowViewRegistry[rootTag];
    shadowView.rootView = (RCTRootShadowView *)rootView;
  }

  // Dispatch view creation directly to the main thread instead of adding to
  // UIBlocks array. This way, it doesn't get deferred until after layout.
  __block UIView *preliminaryCreatedView = nil;

  void (^createViewBlock)(void) = ^{
    // Do nothing on the second run.
    if (preliminaryCreatedView) {
      return;
    }
    // 创建一个view
    preliminaryCreatedView = [componentData createViewWithTag:reactTag];
    // 将创建的view缓存在_viewRegistry中
    if (preliminaryCreatedView) {
      self->_viewRegistry[reactTag] = preliminaryCreatedView;
    }
  };

  // We cannot guarantee that asynchronously scheduled block will be executed
  // *before* a block is added to the regular mounting process (simply because
  // mounting process can be managed externally while the main queue is
  // locked).
  // So, we positively dispatch it asynchronously and double check inside
  // the regular mounting block.

  RCTExecuteOnMainQueue(createViewBlock);

  [self addUIBlock:^(__unused RCTUIManager *uiManager, __unused NSDictionary<NSNumber *, UIView *> *viewRegistry) {
    createViewBlock();

    if (preliminaryCreatedView) {
      [componentData setProps:props forView:preliminaryCreatedView];
    }
  }];

  [self _shadowView:shadowView didReceiveUpdatedProps:[props allKeys]];
}

上面createView方法涉及到2个类:RCTComponentData、RCTShadowView。此时我们有必要介绍一下这两个类的作用以及和他们相关的一些类。

RCTComponentData

在说RCTComponentData之前,我们有必要先说一下他和其他类的关系,如下图:


上图取材于这篇文章 。通过上面类图可以看出,RCTBridge依赖了RCTModuleData。RCTModuleData依赖(实现)了RCTBridgeModule协议。RCTViewManager、RCTUIManager、NativeModule都实现了RCTBridgeModule协议。且RCTViewManager、RCTUIManager、NativeModule都依赖了RCTBridge。

RCTViewManager:负责管理ReactNative在native侧的view,包括RCTImageView、RCTTextView、RCTBaseTextInputView等。我们native侧封装的用于暴露给JS侧使用的原生视图组件也需要视同RCTViewManager来管理。通常需要自定义一个类继承自RCTViewManager。如下所示:

// RichTextLabelManager.h

#import <React/RCTViewManager.h>

@interface RichTextLabelManager : RCTViewManager

@end


// RichTextLabelManager.m
#import "RichTextLabelManager.h"
#import "RichTextLabel.h"

@implementation RichTextLabelManager

RCT_EXPORT_MODULE()

RCT_EXPORT_VIEW_PROPERTY(richText, NSString)

- (dispatch_queue_t)methodQueue
{
    return dispatch_get_main_queue();
}

- (UIView *)view
{
    RichTextLabel *label = [[RichTextLabel alloc] init];
    label.contentMode = UIViewContentModeRedraw;
    return label;
}

@end

RCTViewManager实现了RCTBridgeModule协议。该协议规定了一些宏和方法,包括常见的RCT_EXPORT_MODULE宏。因为RCTViewManager实现了协议方法,所以通过RCTViewManager及其子类,我们可以按照ReactNative的接口规范将native view暴露给JS。所以,我们在将native view暴露给JS侧使用的时候,通常是自定义一个RCTViewManager子类,然后实现RCTBridgeModule协议的方法。

RCTUIManager:在 JS to Native 的渲染流程中,RCTUIManager起到重要作用:包括 Native View 的创建、布局、移除等操作都是通过RCTUIManager完成的。给RCTUIManager设置bridge的过程中,RCTUIManager会根据RCTViewManager生成一个与之对应的RCTComponentData对象。

下面是RCTUIManager的setBridge:方法实现:

通过RCTUIManager#setBridge:方法实现可知:所有的RCTViewManager都会以RCTComponentData格式储存在RCTUIManager->_componentDataByName中。

// RCTUIManager.m

- (void)setBridge:(RCTBridge *)bridge
{
  _bridge = bridge;
  // 省略若干行代码...

  // 从_bridge获取view managers
  _componentDataByName = [NSMutableDictionary new];
  for (Class moduleClass in _bridge.moduleClasses) {
    if ([moduleClass isSubclassOfClass:[RCTViewManager class]]) {
      RCTComponentData *componentData = [[RCTComponentData alloc] initWithManagerClass:moduleClass
                                                                                bridge:_bridge];
      _componentDataByName[componentData.name] = componentData;
    }
  }

  // 省略若干行代码...
}

RCTUIManager通过RCTComponentData操作RCTViewManager,包括创建组件(createView)、更新组件属性(updateView)等,具体内容后文会详细介绍。

RCTRootView

再说RCTShadowView之前,需要先了解下“真正的”view,RCTView、RCTRootView等。先看一下与之相关的类图:


上图取材于这篇文章 。如上图所示,RCTRootViewView和RCTView都继承自UIView。RCTRootContentView继承自RCTView。RCTRootView持有一个RCTRootContentView。

RCTShadowView继承自NSObject,RCTRootShadowView继承自RCTShadowView.。

让我们来梳理下他们的作用。

RCTRootView&RCTRootContentView

RCTRootView作为一个根视图,是一个ReactNative应用(模块)的入口。上篇文章《一篇文章详解React Native初始化和通信机制》中说过,JSbundle加载完成后发送一个RCTJavaScriptDidLoadNotification通知给RCTRootView。RCTRootView收到通知后创建了RCTRootContentView并作为 subview 添加到RCTRootView上,同时调用了runApplication方法。

RCTRootView#runApplication:方法以_moduleName_contentView.reactTag以及_appProperties为参数调用 JS 模块AppRegistryrunApplication方法。

上面说过,RN root components 都需要通过AppRegistry模块的registerComponent方法进行注册。

RCTShadowView&RCTShadowRootView

在 ReactNative中,每个 UI 组件(view)实例都对应一个RCTShadowView(或其派生类)实例,从上面类图可知,虽然其命名以View结尾,但实质并非 View,而是继承自NSObject。其主要功能是通过facebook-Yoga在子线程(shadow thread)进行布局相关的计算。所以RCTShadowView主要接管了UI视图的布局计算工作。就像UIView接管了CALayer的事件处理工作一样。RCTShadowRootView顾名思义,专门负责RCTRootView的布局计算。

渲染过程

前文已提到,RCTUIManager#createView:viewName:rootTag:props:只是创建了目标 view 并添加到_viewRegistry中(仅此而以)。

从上图可以看到,JS 中的ReactNativeBaseComponent模块在调用RCTUIManagercreateView:viewName:rootTag:props:方法创建目标 view 之后,还会调用RCTUIManagersetChildren:reactTags:方法:

// RCTUIManager.m

RCT_EXPORT_METHOD(setChildren:(nonnull NSNumber *)containerTag
                  reactTags:(NSArray<NSNumber *> *)reactTags)
{
  RCTSetChildren(containerTag, reactTags,
                 (NSDictionary<NSNumber *, id<RCTComponent>> *)_shadowViewRegistry);

  [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry){

    RCTSetChildren(containerTag, reactTags,
                   (NSDictionary<NSNumber *, id<RCTComponent>> *)viewRegistry);
  }];

  [self _shadowViewDidReceiveUpdatedChildren:_shadowViewRegistry[containerTag]];
}

static void RCTSetChildren(NSNumber *containerTag,
                           NSArray<NSNumber *> *reactTags,
                           NSDictionary<NSNumber *, id<RCTComponent>> *registry)
{
  id<RCTComponent> container = registry[containerTag];
  NSInteger index = 0;
  for (NSNumber *reactTag in reactTags) {
    id<RCTComponent> view = registry[reactTag];
    if (view) {
      [container insertReactSubview:view atIndex:index++];
    }
  }
}

如上图源码所示,setChildren:reactTags:分别针对_shadowViewRegistry以及_viewRegistry(在 UIBlock 中完成调用)调用了静态方法:RCTSetChildren

对于shadowView,最终会调用到RCTShadowView#insertReactSubview:atIndex:方法:

// RCTShadowView.m
- (void)insertReactSubview:(RCTShadowView *)subview atIndex:(NSInteger)atIndex
{
  RCTAssert(self.canHaveSubviews, @"Attempt to insert subview inside leaf view.");

  [_reactSubviews insertObject:subview atIndex:atIndex];
  if (![self isYogaLeafNode]) {
    YGNodeInsertChild(_yogaNode, subview.yogaNode, (uint32_t)atIndex);
  }
  subview->_superview = self;
}

在该方法中,做的最核心的事情莫过于在YGNode树中插入相应的子节点。

对于view,最终会调用到UIView+RectinsertReactSubview:atIndex:方法:

// UIView+React.m
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex
{
  // We access the associated object directly here in case someone overrides
  // the `reactSubviews` getter method and returns an immutable array.
  NSMutableArray *subviews = objc_getAssociatedObject(self, @selector(reactSubviews));
  if (!subviews) {
    subviews = [NSMutableArray new];
    objc_setAssociatedObject(self, @selector(reactSubviews), subviews, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  }
  [subviews insertObject:subview atIndex:atIndex];
}

在该方法中,按照层级顺序(index)将subView 添加到AssociatedObject reactSubviews中,还是没有真正添加到视图层级树中!

Flush UI Block

上篇文章《一篇文章详解React Native初始化和通信机制》中说过,为了避免JS to Native的频繁调用,所有的JS to Native的调用都不会立即执行,而是放到一个队列中等待Native调用。而UI操作都是先添加到UIManager->_pendingUIBlocks队列中。React Native执完一次批处理后会触发Native侧Executor的callNativeModule的调用(JS线程)。然后经由RCTCxxBridge调用到RCTUIManager的flushUIBlocksWithCompletion:(shadowQueue)。最后在flushUIBlocksWithCompletion:方法中会切换到主线程更新视图的属性。

下面三张堆栈图完美的展现了触发UI的更新的顺序:

  1. 显示native侧收到JS侧的调用,这个调用最先是RCTObjcExecutor(dev环境)收到的,然后经由JSToNativeBridge转发给RCTInstanceCallback。这些操作都是在JS线程执行的。


    JS线程调用JSToNativeBridge::callNativeModules
  2. RCTInstanceCallback通过成员变量bridge_将调用转发给RCTCxxBridge,然后转发给RCTUIManger。这些操作都是在shadowQueue中执行的。


    shadowQueue中flushUIBlocks
  3. 最后flushUIBlocksWithCompletion:中切换到主线程挨个执行_pendingUIBlocks中的block。


    UI线程更新视图

参考文章

https://zxfcumtcs.github.io/2018/02/03/RNRendering/

https://zhuanlan.zhihu.com/p/32749940

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容