ReactNative源码分析 - 渲染原理

1.ReactNative源码分析 - 概述
2.ReactNative源码分析 - JavaScriptCore C语言篇
3.ReactNative源码分析 - 启动流程
4.ReactNative源码分析 - 通信机制
5.ReactNative源码分析 - 渲染原理

  • 一、前言
  • 二、React简介
  • 三、原生端与渲染流程相关的类
    • 1.原生端UI组件体系
    • 2.原生端UI管理者体系
  • 四、原生控件信息如何传递到JS端
    • 1.RCTViewManager定义导出属性
    • 2.RCTUIManager收集原生控件信息
    • 3.UIManager获取原生控件信息
    • 4.原生控件信息处理、获取
  • 五、渲染流程
    • 1.注册JS组件
    • 2.运行JS组件,执行渲染
    • 3.获取React计算结果
    • 4.驱动Native执行渲染流程
  • 六、JS端调用原生控件导出函数
  • 七、结语

一、前言

  • 1.本文主要分析JS端业务层编写的React组件最终如何渲染为原生控件。渲染原理的完整流程梳理起来有点困难,难在此处开始与React衔接,而React又是一个非常庞大的框架。本文对React不过多展开讨论,首先笔者并不擅长React,没有从源码层面完整梳理过它的工作流程,再者这个流程分析起来又是一个系列的文章。本文以React最终产生的结果(组件信息JSON数据)为基础,分析它如何驱动原生端进行UI绘制。
  • 2.ReactNative以React的模式开发,最终通过底层Bridge驱动原生端进行相应操作。从UI渲染原理层面分析,大致流程是:
    • 承载业务逻辑的React组件最终生成描述原生控件信息的JSON数据,通过底层Bridge传递到原生端,驱动原模块绘制原生UI控件;
    • 用户行为触发原生UI控件交互事件/回调事件从原生端传递到JS端以处理业务逻辑,业务逻辑处理结果通常表现为原生控件信息的更新,新的信息数据会再次传递到原生端,驱动原生模块进行新一轮UI更新……
  • 3.本文很多逻辑是基于通信机制(eg:原生模块信息导出流程、Native&JS通信),本文假设你已了解这些知识。

二、React简介

  • JSX是JavaScript的语法扩展,用来书写声明式UI。最终会通过babel 转码,转化常规JavaScript代码。
import React from 'react';
import {View, TouchableOpacity} from 'react-native';

export default class TestRender extends React.Component {
    render() {
     return  (
        <View style={{ backgroundColor: 'white', paddingTop: 64,}} testID="white">
            <View style={{ backgroundColor: "yellow",width: 50, height: 50}} testID="white-yellow"/>
            <View 
                style={{backgroundColor: 'green',width: 100, height: 100,justifyContent:"center",alignItems:"center"}} testID="white-green"
            >
                <View style={{backgroundColor: 'red', width: 50, height: 50,}} testID="white-green-red"></View>
            </View>
            <TouchableOpacity
                style={{backgroundColor: 'blue', width: 50, height: 50}}
                testID="white-blue"
                onPress={() => console.log("onPress") }
            />
        </View>
    );
    }
}

上述JSX转码后变成如下代码。每个React元素转化为React.createElement函数调用,最终返回一个描述组件信息的JS对象,其中嵌套关系表示父子组件关系。

export default class TestRender extends React.Component {
    render() {
      return 
        React.createElement(View, {
            style: {
                backgroundColor: 'white',
                paddingTop: 64
            },
            testID: "white"
        }, React.createElement(View, {
                style: {
                    backgroundColor: "yellow",
                    width: 50,
                    height: 50
                },
                testID: "white-yellow"
            }),
            React.createElement(View, {
                style: {
                    backgroundColor: 'green',
                    width: 100,
                    height: 100,
                    justifyContent: "center",
                    alignItems: "center"
                },
                testID: "white-green"
            }, React.createElement(View, {
                    style: {
                        backgroundColor: 'red',
                        width: 50,
                        height: 50
                    },
                    testID: "white-green-red"
                })), 
            React.createElement(TouchableOpacity, {
                style: {
                    backgroundColor: 'blue',
                    width: 50,
                    height: 50
                },
                testID: "white-blue",
                onPress: () => console.log("onPress")})
        );
    }
  }
const element = <h1>Hello, world</h1>;
  • ReactComponent即React组件是独立、可复用的UI模块,它可同时包含UI渲染、业务逻辑。组件是由元素构成的,React应用由一系列的React组件组合起来的。
    关于React的源码分析,可以参考React源码解析(一):组件的实现与挂载,结合React源码分析React.createElement最终返回JS对象的过程。

三、原生端与渲染流程相关的类

1.原生端UI组件体系

View体系.jpg

原生端与UI渲染相关的视图类体系如图所示,ReactNative封装了一系列原生控件,把控件信息导出到JS端并且包装为对应的组件供JS业务层使用,我们暂且把前者称为控件,后者称为组件,两者在Native端、JS端一一对应。

  • RCTComponent协议定义了一套标准接口,作为视图树的逻辑结点。RCTShadowViewUIView遵守该协议,使得对这两者组成的树View Tree、ShadowView Tree的操作更统一。
  • ReactNative封装的原生控件(RCTView、RCTTextView、RCTImageView……)最终都继承自UIView,即默认实现了RCTComponent协议;
    RCTShadowView可理解为布局结点,js端传递到原生端的控件信息,与布局相关的会赋值给它,并通过它驱动yoga计算出组件的布局信息(frame、center);
    总的来说:在ReactNative渲染过程中,ShadowView与UIView是对应关系,每个UIView都有一个与之对应的ShadowView。UIView是最终渲染出来的视图、ShadowView是它的布局结点,负责为它计算布局信息。渲染过程中存在两棵树:控件树、布局结点树,两者的结点一一对应,互为镜子。
  • RCTRootView根视图是ReactNative暴露给原生端使用的控件,可以像普通UIView一样使用。主要负责运行js端通过AppRegistry.registerComponent注册的根组件。混合开发应用中通常是存在多个RCTRootView,共用同一个底层Bridge。
  • RCTRootContentView作为RCTRootView的子视图,RCTRootView只是负责运行JS组件,后续JS端驱动创建的控件绘制到RootContentView上。
  • RCTRootContentViewRCTRootShadowView是对应关系。

2.原生端UI管理者体系

Manager体系.jpg

原生端与UI渲染相关的管理者类体系如图所示

  • RCTUIManager、RCTViewManager都是原生模块,由于懒加载机制他们实际上是单例,最终都通过底层Bridge把模块信息导出到JS端使用。

    • RCTViewManager与导出控件一一对应,负责管理对应的原生控件,包括定义要导出的控件属性、接口;接收通过RCT_CUSTOM_VIEW_PROPERTY导出的属性赋值;创建控件、创建布局结点……
    • RCTUIManager是渲染流程中的集大成者,它接收JS端发送过来的指令,执行UI控件创建/移除、属性设置/更新、动画、调用原生控件导出函数等。
      RCTViewManager创建原生控件并提供给RCTUIManager,RCTUIManager则会反过来委托它们在需要的时候去设置和更新视图的属性,甚至调用控件导出函数。
  • RCTComponentData包装每一个RCTViewManager,RCTUIManager通过它来:

    • 1.创建原生控件、布局结点;
    • 2.设置控件属性、布局属性;
    • 3.获取控件导出属性集合。

四、原生控件信息如何传递到JS端

这里以RCTWKWebView为例分析原生控件信息导出流程,理解RCTWKWebView的封装,你一定可以封装自定义原生控件供JS端使用

1.RCTViewManager定义导出属性

RCTViewManager控件属性导出宏生成的对应函数,用于后续获取导出属性信息

  • RCT_EXPORT_VIEW_PROPERTY生成属性名、属性类型获取函数。后续通过Runtime获取以propConfig_为前缀的函数并调用以获取这些信息。
  • RCT_EXPORT_SHADOW_PROPERTY生成布局属性名、属性类型获取函数。后序通过Runtime获取以propConfigShadow_为前缀的函数并调用以获取这些信息。
  • RCT_CUSTOM_VIEW_PROPERTY生成属性名、属性类型获取函数,并生成属性setter
// RCTWKWebViewManager.m
RCT_EXPORT_VIEW_PROPERTY(source, NSDictionary)
RCT_EXPORT_VIEW_PROPERTY(onLoadingStart, RCTDirectEventBlock)
RCT_CUSTOM_VIEW_PROPERTY(bounces, BOOL, RCTWKWebView) {
  view.bounces = json == nil ? true : [RCTConvert BOOL: json];
}
// 生成属性type keypath获取函数
+ (NSArray<NSString *> *)propConfig_source { 
    return @[@"NSDictionary"]; 
}
+ (NSArray<NSString *> *)propConfig_onLoadingStart { 
    return @[@"RCTDirectEventBlock"]; 
}

+ (NSArray<NSString *> *)propConfig_bounces { 
    return @[@"BOOL", @"__custom__"]; 
} 
- (void)set_bounces:(id)json forView:(RCTWKWebView *)view withDefaultView:(RCTWKWebView *)defaultView {
    view.bounces = json == ((void *)0) ? 1 : [RCTConvert BOOL: json];
}
// RCTViewManager.m
RCT_EXPORT_SHADOW_PROPERTY(marginTop, YGValue)
+ (NSArray<NSString *> *)propConfigShadow_marginTop { 
    return @[@"YGValue"]; 
}

2.RCTUIManager收集原生控件信息

  • RCTUIManager实例化后执行setBridge:,会收集所有的原生控件管理者RCTViewManager,并创建对应的RCTComponentData
// RCTUIManager.m
- (void)setBridge:(RCTBridge *)bridge
{
  _bridge = bridge;
  ...
  // 收集RCTViewManager
  _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;
    }
  }
}
  • RCTComponentData包装了RCTViewManager,因此它可获取原生控件的一切导出信息。
    RCTViewManager通过宏导导出属性/布局属性,分别生成的导出函数前缀propConfig_propConfigShadow_
    函数viewConfig使用Runtime遍历ViewManager所有类函数以获取导出属性信息,收集以propConfig开头的函数,截取函数名字符串_后面的内容获取属性名;调用函数获取属性类型。
// RCTComponentData.m

// 获取控件导出属性
- (NSDictionary<NSString *, id> *)viewConfig
{
  NSMutableArray<NSString *> *bubblingEvents = [NSMutableArray new];
  NSMutableArray<NSString *> *directEvents = [NSMutableArray new];
  
  // Runtime获取属性/布局属性导出宏生成的函数,函数前缀`propConfig`
  unsigned int count = 0;
  NSMutableDictionary *propTypes = [NSMutableDictionary new];
  Method *methods = class_copyMethodList(object_getClass(_managerClass), &count);
  for (unsigned int i = 0; i < count; i++) {
    SEL selector = method_getName(methods[i]);
    const char *selectorName = sel_getName(selector);
    
    // 过滤无propConfig前缀的函数
    if (strncmp(selectorName, "propConfig", strlen("propConfig")) != 0) {
      continue;
    }

    // 控件导出的属性/布局属性生成的函数分别是propConfig_* propConfigShadow_* ,定位'_'的位置,进而获取属性名
    const char *underscorePos = strchr(selectorName + strlen("propConfig"), '_');
    if (!underscorePos) {
      continue;
    }
    NSString *name = @(underscorePos + 1);
    
    // 调用函数获取属性类型
    NSString *type = ((NSArray<NSString *> *(*)(id, SEL))objc_msgSend)(_managerClass, selector)[0];
    
    // 回调类型属性,用BOOL代替
    if ([type isEqualToString:@"RCTBubblingEventBlock"]) {
      [bubblingEvents addObject:RCTNormalizeInputEventName(name)];
      propTypes[name] = @"BOOL";
    } else if ([type isEqualToString:@"RCTDirectEventBlock"]) {
      [directEvents addObject:RCTNormalizeInputEventName(name)];
      propTypes[name] = @"BOOL";
    } else {
      propTypes[name] = type;
    }
  }
  free(methods);
  
  Class superClass = [_managerClass superclass];
  
  return @{
    @"propTypes": propTypes,
    @"directEvents": directEvents,
    @"bubblingEvents": bubblingEvents,
    @"baseModuleName": superClass == [NSObject class] ? (id)kCFNull : moduleNameForClass(superClass),
  };
}

例子RCTWKWebViewManager返回结果如下
propTypes存放所有导出属性的属性名、类型;
directEvents、bubblingEvents存放回调类型;
baseModuleName表示控件基类,用于在JS端添加基类导出属性以得到完整属性表。

{
     "propTypes": {
        "allowsInlineMediaPlayback": "BOOL",
        "automaticallyAdjustContentInsets":"BOOL",
        "bounces" : "BOOL",
        "contentInset": "UIEdgeInsets",
        ...
        "onMessage":""BOOL,
        "onShouldStartLoadWithRequest":"BOOL"
     },
     "directEvents": [
        "topLoadingStart",
        "topLoadingFinish",
        "topLoadingError",
        "topMessage",
        "topShouldStartLoadWithRequest"
     ],
     "bubblingEvents": [],
     "baseModuleName": "RCTViewManager"
   }
  • RCTUIManager原生模块导出常量constantsToExport用来导出所有原生控件属性信息。
    这里遍历所有原生控件以收集属性信息,最终加工为一个包含所有原生控件属性信息的集合,导出到JS端。
// 收集所有导出供JS端使用的原生控件的信息
- (NSDictionary<NSString *, id> *)constantsToExport
{
  return [self getConstants];
}

- (NSDictionary<NSString *, id> *)getConstants
{
  NSMutableDictionary<NSString *, NSDictionary *> *constants = [NSMutableDictionary new];
  NSMutableDictionary<NSString *, NSDictionary *> *directEvents = [NSMutableDictionary new];
  NSMutableDictionary<NSString *, NSDictionary *> *bubblingEvents = [NSMutableDictionary new];
  
  [_componentDataByName enumerateKeysAndObjectsUsingBlock:^(NSString *name, RCTComponentData *componentData, __unused BOOL *stop) {
    NSMutableDictionary<NSString *, id> *moduleConstants = moduleConstantsForComponent(directEvents, bubblingEvents, componentData);
    constants[name] = moduleConstants;
  }];
  return constants;
}

static NSMutableDictionary<NSString *, id> *moduleConstantsForComponent(
    NSMutableDictionary<NSString *, NSDictionary *> *directEvents,
    NSMutableDictionary<NSString *, NSDictionary *> *bubblingEvents,
    RCTComponentData *componentData) {
  NSMutableDictionary<NSString *, id> *moduleConstants = [NSMutableDictionary new];
 
  NSMutableDictionary<NSString *, NSDictionary *> *bubblingEventTypes = [NSMutableDictionary new];
  NSMutableDictionary<NSString *, NSDictionary *> *directEventTypes = [NSMutableDictionary new];

  moduleConstants[@"Manager"] = RCTBridgeModuleNameForClass(componentData.managerClass);

  NSDictionary<NSString *, id> *viewConfig = [componentData viewConfig];
  moduleConstants[@"NativeProps"] = viewConfig[@"propTypes"];
  moduleConstants[@"baseModuleName"] = viewConfig[@"baseModuleName"];
  moduleConstants[@"bubblingEventTypes"] = bubblingEventTypes;
  moduleConstants[@"directEventTypes"] = directEventTypes;

  for (NSString *eventName in viewConfig[@"directEvents"]) {
    if (!directEvents[eventName]) {
      directEvents[eventName] = @{
                                  @"registrationName": [eventName stringByReplacingCharactersInRange:(NSRange){0, 3} withString:@"on"],
                                  };
    }
    directEventTypes[eventName] = directEvents[eventName];
  }

  // Add bubbling events
  for (NSString *eventName in viewConfig[@"bubblingEvents"]) {
    if (!bubblingEvents[eventName]) {
      NSString *bubbleName = [eventName stringByReplacingCharactersInRange:(NSRange){0, 3} withString:@"on"];
      bubblingEvents[eventName] = @{
                                    @"phasedRegistrationNames": @{
                                        @"bubbled": bubbleName,
                                        @"captured": [bubbleName stringByAppendingString:@"Capture"],
                                        }
                                    };
    }
    bubblingEventTypes[eventName] = bubblingEvents[eventName];
  }

  return moduleConstants;
}

最终生成一张表包含所有原生控件导出属性信息<原生控件名:导出信息>,格式如下。作为RCTUIManager原生模块常量导出到JS端,原生模块信息导出流程详解通信机制

{
   "RCTWKWebView": {
     "Manager": "WKWebViewManager",
     "NativeProps" : {
         "allowsInlineMediaPlayback" :"BOOL",
         ...
         "onLoadingError" : "BOOL",
         "onLoadingFinish" : "",
         "onLoadingStart" : "BOOL",
         "onMessage" : "BOOL",
         "source" : "NSDictionary",
      },
      "baseModuleName" : "RCTView",
      "bubblingEventTypes" : { };
      "directEventTypes" {
         "topLoadingError" : { "registrationName" : "onLoadingError",},
         "topLoadingFinish" : { "registrationName" : "onLoadingFinish",},
         ...
       },
    },
   
   "RCTView": {...},
   
   ...
   }

3.UIManager获取原生控件信息

  • 注:
    通常情况下:ReactNative原生模块(特别是逻辑模块)在JS端会包装为对应的JS模块,例如RCTUIManager在JS端包装为UIManager。 这样做有诸多好处,首先Native、JS端一一对应,其次是更清晰的模块化,还是可以缓存原生模块信息。

原生控件管理者ViewManager通常不会包装,原因是原生模块信息几乎都通过UIManager获取,不会直接使用。但原生控件信息导出到JS端,会包装为对应组件,例如RCTWKWebView在JS端会包装为WebView.ios.js,这么做的好处同上。

  • UIManager实际上就做一件事情:加载所有原生控件信息并缓存,供调用者使用。
    上文分析过RCTUIManager导出常量是所有原生控件信息,根据通信机制:原生模块信息如何导入到JS端,可知相应的JS端模块UIManager能获取到这些原生控件信息。UIManager.js脚本运行时,会遍历UIManager模块信息,找出原生控件信息(判断是否有Manager属性),并且进行加工、缓存。
    加工的过程很简单:获取相应ViewManager并把导出函数getter、常量getter添加到原生控件信息中。至此,JS端缓存的原生控件信息相对完整,包含原生控件的导出属性、函数、常量、管理者ViewManager、基类。
    UIManager把原生控件信息都命名为ViewManagerConfig,即原生控件管理者信息,其实是就是原生控件信息。
// UIManager.js

const {UIManager} = NativeModules;
// 存放所有原生控件信息 { viewName: viewConfig }
const viewManagerConfigs = {};

// 获取控件信息
UIManager.getViewManagerConfig = function(viewManagerName: string) {
  const config = viewManagerConfigs[viewManagerName];
  if (config) {
    return config;
  }

  // 防御作用,获取不到viewConfig时,使用同步函数获取试图获取
  if (UIManager.lazilyLoadView && !triedLoadingConfig.has(viewManagerName)) {
    ...
  }
  return viewManagerConfigs[viewManagerName];
};

// 原生控件配置信息viewConfig
function lazifyViewManagerConfig(viewName) {
  const viewConfig = UIManager[viewName];
  if (viewConfig.Manager) {
    // viewConfig存入组件信息表viewManagerConfigs
    viewManagerConfigs[viewName] = viewConfig;

    // 定义取值函数getter,用于获取Constants,即ViewManager导出常量
    defineLazyObjectProperty(viewConfig, 'Constants', {
      get: () => {
        const viewManager = NativeModules[viewConfig.Manager];
        const constants = {};
        viewManager &&
          Object.keys(viewManager).forEach(key => {
            const value = viewManager[key];
            if (typeof value !== 'function') {
              constants[key] = value;
            }
          });
        return constants;
      },
    });
    
    // 定义取值函数,用于获取Commands,即ViewManager导出函数(索引)
    defineLazyObjectProperty(viewConfig, 'Commands', {
      get: () => {
        const viewManager = NativeModules[viewConfig.Manager];
        const commands = {};
        let index = 0;
        viewManager &&
          Object.keys(viewManager).forEach(key => {
            const value = viewManager[key];
            if (typeof value === 'function') {
              commands[key] = index++;
            }
          });
        return commands;
      },
    });
  }
}

// 遍历UIManager模块信息,获取、加工组件信息,并缓存
if (Platform.OS === 'ios') {
  Object.keys(UIManager).forEach(viewName => {
    lazifyViewManagerConfig(viewName);
  });
}

// 导出UIManager对象,单例
module.exports = UIManager;

最终原生控件信息存放在viewManagerConfigs,格式如下

{
    RCTWKWebView: {
        Commands: {
             getConstants: 7,
             goBack: 1,
             goForward: 2,
             ...
        },
        Constants: {},
        Manager: "WKWebViewManager",
        NativeProps: {
            allowsInlineMediaPlayback: "BOOL",
            bounces: "BOOL",
            ...
            onMessage: "BOOL",
        },
        baseModuleName: "RCTView",
        bubblingEventTypes:{ },
        directEventTypes: {
            topLoadingStart: {registrationName: "onLoadingStart"},
            topMessage: {registrationName: "onMessage"},
            ...
         }
    },
    
    RCTView: {
        ...
    }
    
    ...
}

4.原生控件信息处理、获取

上诉流程中,UIManager单例收集所有原生控件信息,但还不能直接使用,下面进一步分析信息的处理、使用流程。

原生控件信息传递.jpg
  • 1.原生控件在JS端封装对应JS组件,会执行requireNativeComponent函数,传入原生端导入的原生组件名。
// WebView.ios.js
const RCTWKWebView = requireNativeComponent('RCTWKWebView');

该函数其实是createReactNativeComponentClass函数的一层包装,传入一个回调函数。

// requireNativeComponent.js
const requireNativeComponent = (uiViewClassName: string): string =>
    createReactNativeComponentClass(uiViewClassName, () =>
        getNativeComponentAttributes(uiViewClassName),
);

getNativeComponentAttributes,负责从UIManager获取原生控件信息,并做进一步加工:添加基类属性;添加属性比较/处理函数。最终得到完整的原生控件信息。它作为回调函数存放到ReactNativeViewConfigRegistry中,以懒加载完整的原生控件信息。

// 获取原生控件信息
// getNativeComponentAttributes.js 
function getNativeComponentAttributes(uiViewClassName: string) {
  // 从UIManager获取原生控件信息
  const viewConfig = UIManager.getViewManagerConfig(uiViewClassName);

  // 添加基类原生模块信息
  let {baseModuleName, bubblingEventTypes, directEventTypes} = viewConfig;
  let nativeProps = viewConfig.NativeProps;
  while (baseModuleName) {
    const baseModule = UIManager.getViewManagerConfig(baseModuleName);
    ...
  }

  // 添加属性比较、处理函数
  const validAttributes = {};
  for (const key in nativeProps) {
    const typeName = nativeProps[key];
    const diff = getDifferForType(typeName);
    const process = getProcessorForType(typeName);
    validAttributes[key] =
      diff == null && process == null ? true : {diff, process};
  }

  Object.assign(viewConfig, {
    uiViewClassName,  
    validAttributes,  
    bubblingEventTypes,
    directEventTypes,
  });
  return viewConfig;
}
  • 2.createReactNativeComponentClass函数,则调用ReactNativeViewConfigRegistry模块的register函数,注册原生控件信息获取回调。
// 注册原生控件信息获取回调
// createReactNativeComponentClass.js
const createReactNativeComponentClass = function(
  name: string,
  callback: ViewConfigGetter,
): string {
  return register(name, callback);
};

ReactNativeViewConfigRegistry模块就是一个原生控件信息注册机,它包含所有需要的原生控件信息,并且是懒加载机制。
调用register函数注册原生控件信息获取回调,并原路返回传入的view name,作为组件名书写JSX。

// ReactNativeViewConfigRegistry.js

const viewConfigCallbacks = new Map(); // 原生控件信息获取回调函数表
const viewConfigs = new Map();         // 原生控件信息表

// 注册原生控件信息获取回调,用于从UIManager获取组件信息
exports.register = function(name: string, callback: ViewConfigGetter): string {
  viewConfigCallbacks.set(name, callback);
  return name;
};

 // 获取原生控件信息,懒加载机制
 exports.get = function(name: string): ReactNativeBaseComponentViewConfig<> {
  let viewConfig;
  if (!viewConfigs.has(name)) {
    const callback = viewConfigCallbacks.get(name);
    viewConfigCallbacks.set(name, null);
    viewConfig = callback();
    processEventTypes(viewConfig);
    viewConfigs.set(name, viewConfig);
  } else {
    viewConfig = viewConfigs.get(name);
  }
  return viewConfig;
};

总结
1.封装组件时调用requireNativeComponent注册一个原生控件信息获取/处理回调到原生控件信息注册机ReactNativeViewConfigRegistry
2.真正执行渲染,使用到对应组件,调用get函数从原生控件信息注册机获取原生控件信息,若控件信息存在,则直接使用;否则执行原生控件信息获取/处理回调,以得到完整并原生控件信息并缓存。
这一流程驱使原生控件数据进行加工,从UIManager流行ReactNativeViewConfigRegistry,并且具备懒加载特性。

五、渲染流程

1.注册JS组件

  • ReactNative项目会在JS执行的入口文件,注册根组件到AppRegistry
// index.js
AppRegistry.registerComponent('Main', () => Main);
  • AppRegistry:App注册机,准确来说是根组件注册机,因为一个应用中可存在多个根组件。AppRegistry主要负责:根组件的注册、运行、卸载。它注册到BatchedBridge作为JS模块,可供原生端调用。
    • 1.JS端通过registerComponent注册根组件到runnables表中;
    • 2.原生端通过runApplication运行指定的根组件;
    • 3.原生端在组件销毁时,通过unmountApplicationComponentAtRootTag卸载指定的根组件;
// AppRegistry.js

const runnables: Runnables = {};

const AppRegistry = {
  // 注册组件
  registerComponent(appKey: string, componentProvider: ComponentProvider, section?: boolean): string {
    runnables[appKey] = {
      componentProvider,
      run: ...
    };
    return appKey;
  },

  // 运行组件:获取注册表中的组件,运行
  runApplication(appKey: string, appParameters: any): void {
    runnables[appKey].run(appParameters);
  },

  // 卸载组件
  unmountApplicationComponentAtRootTag(rootTag: number): void {
    ReactNative.unmountComponentAtNodeAndRemoveContainer(rootTag);
  },
}

// 注册JS模块 AppRegistry
BatchedBridge.registerCallableModule('AppRegistry', AppRegistry);

2.运行JS组件,执行渲染

  • 原生端RCTRootView在js bundle执行完毕后,运行指定的JS组件
// RCTRootView.m
- (void)runApplication:(RCTBridge *)bridge
{
  NSString *moduleName = _moduleName ?: @"";
  NSDictionary *appParameters = @{
    @"rootTag": _contentView.reactTag,
    @"initialProps": _appProperties ?: @{},
  };
  
  // 调用js模块AppRegistry runApplication,运行组件
  [bridge enqueueJSCall:@"AppRegistry"
                 method:@"runApplication"
                   args:@[moduleName, appParameters]
             completion:NULL];
}
  • 上述操作会执行JS模块AppRegistry运行根组件接口runApplication。根据组件名appKey获取对应的根组件,调用run运行根组件,最终执行renderApplication进行渲染。
// AppRegistry.js

runnables[appKey] = {
 // 组件获取函数
 componentProvider,
 // 组件运行函数, 执行render
 run: appParameters => {
   renderApplication(
     componentProviderInstrumentationHook(componentProvider), 
     appParameters.initialProps,  // 原生层传递过来的初始化属性
     appParameters.rootTag,       // rootTag
     wrapperComponentProvider && wrapperComponentProvider(appParameters),
     appParameters.fabric,
   );
 },
};
  • renderApplication主要负责
    • 使用AppContainer包装根组件。AppContainer主要是包含了YellowBox之类的调试组件;
    • 调用ReactNativerender函数进行渲染。
      ReactFabric应该是React的正在重构的下一代渲染机制,目前还没有生效。
// renderApplication.js

function renderApplication<Props: Object>(
  RootComponent: React.ComponentType<Props>,  // 根组件
  initialProps: Props,
  rootTag: any,  
  WrapperComponent?: ?React.ComponentType<*>,
  fabric?: boolean,
  showFabricIndicator?: boolean,) 
{
  // 根组件RootComponent嵌入容器组件AppContainer(用于包装yellowBox等调试组件)
  let renderable = (
    <AppContainer rootTag={rootTag} WrapperComponent={WrapperComponent}>
      <RootComponent {...initialProps} rootTag={rootTag} />
    </AppContainer>
  );

  if (fabric) {
    require('ReactFabric').render(renderable, rootTag);
  } else {
    // 执行渲染
    require('ReactNative').render(renderable, rootTag);
  }
}
  • ReactNative根据运行环境执行相应脚本。调试环境使用ReactNativeRenderer-dev;生产环境使用ReactNativeRenderer-prod。这里分析调试环境。
// ReactNative.js

if (__DEV__) {
  ReactNative = require('ReactNativeRenderer-dev');
} else {
  ReactNative = require('ReactNativeRenderer-prod');
}
  • 再往下追踪,发现上述两个文件的代码量都是万行级的,这显然不是人看的代码。当然笔者相信FaceBook工程师不会写可读性这么差的代码。经探索发现这两个不是源文件,真正的源文件可在React中查看。
    React项目十分庞大,本文不作展开,我们只需知道它采用monorepo管理方式,一个项目拆分为多个独立包的。React中应用于ReactNative的包主要是如下红框所示,负责渲染、事件系统、协调等,使得React与ReactNative能链接起来,详见源码概览
    早在远古时期react-native-0.45.0,React的源码是直接引入ReactNative,这种应该比较好梳理逻辑。毕竟在无法调试代码的情况下研究源码太考验想象力了。
15754700576459.jpg

3.获取React计算结果

React与ReactNative的衔接是个大工程,此处省略一万字,直接分析两者最终的计算结果如何驱动原生端执行UI渲染。

Debug环境下,调用ReactNativeRenderer-dev.js导出对象ReactNativeRenderer的渲染函数render执行渲染。经过一个长长的调用栈之后会执行根组件的render(可在根组件render打断点追踪调用栈),返回一个根React Element。这就是React计算结果了,即我们用React编写的JSX代码的最终产物。
DEMO中的例子TestRender,最终返回React Element对象如下(省略了部分信息,testID为测试id),这其实就是一棵树,包含原生控件信息。

{
    $$typeof: Symbol(react.element),
    type: {$$typeof: Symbol(react.forward_ref), displayName: "View",},
    props: {
        children: [
            {
                $$typeof: Symbol(react.element),
                type: {$$typeof: Symbol(react.forward_ref), displayName: "View"},
                props: {
                    style: {backgroundColor: "yellow", width: 50, height: 50},
                    testID: "white-yellow"
                },
            },
            {
                $$typeof: Symbol(react.element),
                type: {$$typeof: Symbol(react.forward_ref), displayName: "View"},
                props:{
                    children: [{
                        $$typeof: Symbol(react.element),
                        props:{
                            style: {backgroundColor: "red", width: 50, height: 50},
                            testID: "white-green-red"
                        },
                        type: {$$typeof: Symbol(react.forward_ref), displayName: "View"}
                    }],
                    style: {backgroundColor: "green", width: 100, height: 100},
                    testID: "white-green",
                },
            },
            {
                $$typeof: Symbol(react.element),
                type: {displayName: "TouchableOpacity"},
                props: {
                    style: {backgroundColor: "blue", width: 50, height: 50}, 
                    testID: "white-blue", 
                    activeOpacity: 0.2, 
                    onPress: ƒ
                },
            }
        ],
        style: {backgroundColor: "white", paddingTop: 64},
        testID: "white",
    },
}

4.驱动Native执行渲染流程

  • 0.RCTUIManager简析
    前面提到RCTUIManager是渲染流程的集大成者,分析渲染流程,得先分析RCTUIManager的多个容器和队列ShadowQueue。渲染流程同样采用批处理思想。

    • _rootViewTags:根控件集合,存放所有根控件,UI布局计算就是从根控件开始递归结点树。

    • _pendingUIBlocks:UI操作集合,创建控件、设置属性、改变视图层级、设置布局信息…等UI操作会先缓存,在特定时机派发到主线程。

    • _shadowViewRegistry:布局结点集合,存放整个应用的布局结点

    • _viewRegistry:原生控件集合,存放整个应用的原生控件

    • _shadowViewsWithUpdatedProps:已更新属性的shadowView集合,记录更新了属性值的布局结点,布局时机一到会遍历该集合逐一更新布局结点的YOGA值

    • _shadowViewsWithUpdatedChildren:已更新子控件的shadowView集合,记录子控件有变动的布局结点,布局时机一到会遍历该集合逐一更新控件的子控件。

    • 渲染过程大多数操作发生在队列ShadowQueue中,它是串行队列,作为RCTUIManager导出函数的执行队列。
      上述容器的使用有严格的线程规定,以保证线程安全和UI操作在主线程执行。所有容器的创建、销毁在主线程,使用(增删改查)则有差别,_viewRegistry涉及UI操作(创建控件)需要在主线程使用,其他容器其实都是从数据层面的操作,并非正在执行UI操作,因此都在ShadowQueue执行;布局计算也在ShadowQueue执行(异步计算布局结果)。

  • 注:通过简洁的线程管理来实现:JS&Native(ReactNativeRenderer&RCTUIManager)的交互、控件层次结构(数据层面)更新、布局结点属性更新、布局计算等都在ShadowQueue队列执行;控件属性更新、更改视图层级、渲染等正在操作UI则主线程执行。即:非UI操作在ShadowQueue执行,操作UI在主线程进行,结合批处理机制,达到为主线程减负、高效渲染的目的。

@implementation RCTUIManager
{
  // 根控件集合 reactTag <reactTag>
  NSMutableSet<NSNumber *> *_rootViewTags;
  // 暂存UI操作;
  NSMutableArray<RCTViewManagerUIBlock> *_pendingUIBlocks;
  // 布局结点集合 { reactTag : RCTShadowView }
  NSMutableDictionary<NSNumber *, RCTShadowView *> *_shadowViewRegistry; // RCT thread only
  
  // 控件集合 {reactTag : UIView}
  NSMutableDictionary<NSNumber *, UIView *> *_viewRegistry; // Main thread only
  
  // 已更新属性的shadowView集合 { RCTShadowView: [props key] }
  NSMapTable<RCTShadowView *, NSArray<NSString *> *> *_shadowViewsWithUpdatedProps; // UIManager queue only.
  // 已更新子控件的shadowView集合
  NSHashTable<RCTShadowView *> *_shadowViewsWithUpdatedChildren; // UIManager queue only.
  ...
}

1.JS端根据计算结果,驱动原生端创建原生控件

  • JS端根据计算结果,驱动原生端创建原生控件
    • 生成reactTag,作为每个控件的标识;
    • 从原生控件信息注册机获取对应的控件信息,并处理控件属性(过滤非法属性……);
    • 调用原生模块UIManager创建原生控件
// ReactNativeRenderer-dev.js

function createInstance(
  type,
  props,
  rootContainerInstance,
  hostContext,
  internalInstanceHandle
) {
  // 创建组件tag
  var tag = allocateTag();
  // 获取原生控件信息
  var viewConfig = ReactNativeViewConfigRegistry.get(type);
  var updatePayload = create(props, viewConfig.validAttributes);
  
  // 调用原生模块,创建原生控件
  UIManager.createView(
    tag, // reactTag
    viewConfig.uiViewClassName, // viewName
    rootContainerInstance, // rootTag
    updatePayload // props
  );

  var component = new ReactNativeFiberHostComponent(tag, viewConfig);
  precacheFiberNode(internalInstanceHandle, tag);
  updateFiberProps(tag, props);
  return component;
}
  • 原生端创建原生控件和对应的布局结点;设置布局属性、控件属性值;
    设置属性的过程相对冗长,详见源码,大致流程是:根据属性名、是否是布局结点(isShadowView),构建导出属性的导出函数SEL以获得属性类型,进而构建出属性setter,并包装为Block,执行Block设置属性,最终控件属性值设置到原生控件,布局属性值设置到布局结点ShadowView
// RCTUIManager.m

// 创建原生控件
RCT_EXPORT_METHOD(createView:(nonnull NSNumber *)reactTag
                  viewName:(NSString *)viewName
                  rootTag:(nonnull NSNumber *)rootTag
                  props:(NSDictionary *)props)
{  
  RCTComponentData *componentData = _componentDataByName[viewName];
 
  // 创建对应的shadowView,存入容器_shadowViewRegistry,设置属性
  RCTShadowView *shadowView = [componentData createShadowViewWithTag:reactTag];
  if (shadowView) {
    [componentData setProps:props forShadowView:shadowView];
    _shadowViewRegistry[reactTag] = shadowView;
    RCTShadowView *rootView = _shadowViewRegistry[rootTag];
    shadowView.rootView = (RCTRootShadowView *)rootView;
  }

  // 主线程 创建NativeView,存入View注册表_viewRegistry
  __block UIView *preliminaryCreatedView = nil;
  void (^createViewBlock)(void) = ^{
    if (preliminaryCreatedView) {
      return;
    }
    preliminaryCreatedView = [componentData createViewWithTag:reactTag];
    if (preliminaryCreatedView) {
      self->_viewRegistry[reactTag] = preliminaryCreatedView;
    }
  };
  RCTExecuteOnMainQueue(createViewBlock);
  
  // 设置控件属性(对UIView的操作都先暂存在_pendingUIBlocks,批处理)
  [self addUIBlock:^(__unused RCTUIManager *uiManager, __unused NSDictionary<NSNumber *, UIView *> *viewRegistry) {
    createViewBlock();
    if (preliminaryCreatedView) {
      [componentData setProps:props forView:preliminaryCreatedView];
    }
  }];

  // 更新props,存入_shadowViewsWithUpdatedProps
  [self _shadowView:shadowView didReceiveUpdatedProps:[props allKeys]];
}

2.根据计算结果,设置视图层次结构

// ReactNativeRenderer-dev.js

function finalizeInitialChildren( parentInstance, type, props, rootContainerInstance, hostContext) {
  if (parentInstance._children.length === 0) {
    return false;
  }
  var nativeTags = parentInstance._children.map(function(child) {
    return typeof child === "number" ? child // Leaf node (eg text): child._nativeTag;
  });

  UIManager.setChildren(
    parentInstance._nativeTag, // 父控件tag
    nativeTags // 子控件tag集合
  );
  return false;
}

原生端布局结点树更改层次结构,此时仅从数据层面改变层次结构,并未真正addSubVew(详见UIView+React.m)

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

3.布局时机一到,真正执行UI渲染

  • _layoutAndMount按顺序做了以下几件事情,详见源码或DEMO
    • _dispatchPropsDidChangeEvents向属性值发生改变的原生控件/布局结点发送消息,主要驱使布局结点更新YOGA属性值;
    • _dispatchChildrenDidChangeEvents向视图层次结构发生改变的原生控件/布局结点发送消息,主要驱使原生控件更新视图树(addSubview);
    • 遍历根结点,调用uiBlockWithLayoutUpdateForRootView,封装渲染操作。
      先驱动根结点执行layout操作,底层会从根结点开始递归,使用yoga计算布局结果(shadowView.layoutMetrics);
      再把正真的布局(reactSetFrame)、动画(performAnimations)操作封装为一个uiBlock并收集到UI操作容器_pendingUIBlocks
    • 最后调用flushUIBlocksWithCompletion,统一把所有UI操作派发到主线程执行,包括最后一个渲染/动画操作block
// RCTUIManager.m

// 布局时机
- (void)batchDidComplete
{
  [self _layoutAndMount];
}

- (void)_layoutAndMount
{
  // 通知 RCTShadowView、RCTComponent 属性已更新
  [self _dispatchPropsDidChangeEvents];
  // 通知 RCTShadowView、RCTComponent 控件/布局结点层次结构已更新(subView已改变)
  [self _dispatchChildrenDidChangeEvents];

  // 构建一个UI渲染、执行布局动画的Block,添加到_pendingUIBlocks
  for (NSNumber *reactTag in _rootViewTags) {
    RCTRootShadowView *rootView = (RCTRootShadowView *)_shadowViewRegistry[reactTag];
    [self addUIBlock:[self uiBlockWithLayoutUpdateForRootView:rootView]];
  }

  // 统一执行所有 _pendingUIBlocks,包括UI渲染
  [self flushUIBlocksWithCompletion:^{
    [self->_observerCoordinator uiManagerDidPerformMounting:self];
  }];
}

// 构建一个UI渲染、执行布局动画的Block
- (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView *)rootShadowView
{
    ...
    return block;
}
  • 布局时机理解:渲染过程中布局的时机是batchDidComplete,追溯回JSIExecutor.cpp可知这个时间点就是执行Native call JS、 JS callback后把所有JS端暂存JS call Native调用信息传递给原生端,原生端发起执行(并未执行完毕)后,就触发布局。为何在这个时间点布局,笔者的猜想如下(不一定准确,欢迎探讨):
    • 1.首先明确一个前提:UI渲染的优先级无疑是最高的,原生开发中UI操作都是在主线程进行。因此JS端的计算结果(通常表现为原生控件信息的更新,即UI数据的更新)必须及时发送到原生端,执行UI更新。
    • 2.再回顾前言对ReactNative渲染原理的介绍:用户行为触发原生UI控件交互事件/回调事件从原生端传递到JS端,JS端处理业务逻辑,得到新的UI数据,再次传递到原生端,驱动原生模块进行新一轮UI更新。
      • 例子1:用户点击按钮触发一个弹窗。点击事件在RCTRootContentView通过RCTTouchHandlerRCTEventDispatcher,最终调用JS模块RCTEventEmitter函数receiveEvent通知JS端处理UI交互事件(Native call JS)。JS端收到信号后进行业务逻辑处理,计算结果是带弹窗的新UI数据。数据需要立即传递到原生端进行UI更新,绘制弹窗,在Native Call JS执行完毕后进行无疑是最及时的,这就是布局时机1。
      • 例子2:用户调用原生社交分享模块,完毕需要显示toast条提示用户分享结果。JS端发起一个带回调JS call Native调起原生模块进行社交分享,原生端分享完毕,执行JS callback通知JS端处理分享结果。JS端收到信号后进行业务逻辑处理,计算结果是带toast提示条的新UI数据。数据需要立即传递到原生端进行UI更新,绘制toast提示条,在JS callback执行完毕后进行是最及时的,这就是布局时机2。

至此,UI渲染的基本流程就分析完毕了。

六、JS端调用原生控件导出函数

JS端调用原生控件的导出函数,流程如下

  • 1.JS端调用RCTUIManager导出函数dispatchViewManagerCommand,传递reactTag、Command
  • 2.触发原生端走JS call Native流程的后半部分,最终定位到对应原生控件的函数(可参考WebView.ios.js
// RCTUIManager.m

/*
 向ViewManager派发命令:用于RN端调用控件导出函数
 reactTag 控件标签
 commandID 命令id
 commandArgs 参数
 */
RCT_EXPORT_METHOD(dispatchViewManagerCommand:(nonnull NSNumber *)reactTag
                  commandID:(NSInteger)commandID
                  commandArgs:(NSArray<id> *)commandArgs)
{
  RCTShadowView *shadowView = _shadowViewRegistry[reactTag];
  RCTComponentData *componentData = _componentDataByName[shadowView.viewName];
  Class managerClass = componentData.managerClass;
  RCTModuleData *moduleData = [_bridge moduleDataForName:RCTBridgeModuleNameForClass(managerClass)];
  id<RCTBridgeMethod> method = moduleData.methods[commandID];

  // 带上reactTag,用于获取对应Native控件
  NSArray *args = [@[reactTag] arrayByAddingObjectsFromArray:commandArgs];
  // 调用控件导出函数
  [method invokeWithBridge:_bridge module:componentData.manager arguments:args];
}
// RCTWKWebViewManager.m

RCT_EXPORT_METHOD(goBack:(nonnull NSNumber *)reactTag)
{
  [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTWKWebView *> *viewRegistry) {
    RCTWKWebView *view = viewRegistry[reactTag];
    if (![view isKindOfClass:[RCTWKWebView class]]) {  
    } else {
      [view goBack];
    }
  }];
}

七、结语

  • 这一系列文章就到此为止了,希望有助于大家理解ReactNative源码;
  • ReactNative还有很多设计值得研究,例如原生如何端驱动JS端定时任务、观察者模式的设计等;

Reference

ReactNative中文网
React
ReactNative源码解析——渲染机制详解

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

推荐阅读更多精彩内容