React Native源码分析原理(二)(基于0.48版本)

上一篇文章大家如果仔细阅读揣摩对RN有了一个初步的认识了,接下来将基于上一篇文章的这种初步认识然我们详细了解一下RN的启动过程

启动过程

[RCTRootView initWithBundleURL:...]
[RCTBridge initWithBundleURL:...]
[RCTBridge setUp]
初始化batchedBridge
[RCTCxxBridge start]
开启一个线程jsThread用于js
[RCTCxxBridge _initModulesWithDispatchGroup]
初始化JSCExecutorFactory, 用于执行js代码以及处理回调 JSCExecutor::JSCExecutor()
JSCExecutor::initOnJSVMThread()
installGlobalProxy -> nativeModuleProxy
RCTJavaScriptLoader -> 加载js代码
[RCTCxxBridge executeSourceCode]
[RCTRootView initWithBridge:...]
[RCTRootView bundleFinishedLoading]
初始化RCTRootContentView
[RCTRootView runApplication]//是Native调用js的一个完美例子。本质调用了AppRegistry的runApplication方法
我们初始化RN工程一般都是如下代码:

  jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil];

  RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                      moduleName:@"AwesomeProject"
                                               initialProperties:nil
                                                   launchOptions:launchOptions];

1、 获取到app的js入口文件的NSURL
2 、初始化RCTRootView, 传递的参数moduleName:@"AwesomeProject"是我们在入口的js文件中注册的, initialProperties:nil, 将作为js中的跟view的props, 可以用来传递需要的初始数据

进入RCTRootView初始化中,其实很简单

  RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:bundleURL
                                            moduleProvider:nil
                                             launchOptions:launchOptions];

  return [self initWithBridge:bridge moduleName:moduleName initialProperties:initialProperties];

就实例化了一个RCTBridge,这个RCTBridge的实例其实就是一个外壳而已,接下来我们看看内部,为啥是一个外壳。

[self setUp];

内部中这个方法就是关键,这个方法里边实例化了一个真正的Bridge,即RCTCxxBridgeRCTBatchedBridge,所以说一开始实例化的Bridge只是一个壳子而已。为什么会出现两个不同的Bridge呢?主要是0.44之前的都是用的RCTBatchedBridge,之后RN用的是RCTCxxBridge,根据它的官方资料RCTCxxBridge会慢慢取代RCTBatchedBridge。(RCTCxxBridge有个问题就是依赖了4个包,而且这4个包被墙了具体可以看将RN工程嵌入到现有原生iOS应用)。

  self.batchedBridge = [[bridgeClass alloc] initWithParentBridge:self];
  [self.batchedBridge start];

首先是bridgeClass的获取,具体代码是优先加载RCTCxxBridge,如果获取不到的话,其次加载RCTBatchedBridge。本次分析是基于0.48并且工程初始化是使用了RCTCxxBridge,所以下边只要提到Bridge说的就是RCTCxxBridge

start是一个非常有料的方法。就像之前说的Bridge其实就是实现了一个调度管理的作用,那调度管理什么呢?所有需要调度管理的前提环境和类全部都是在start方法进行了初始化。

1、 首先做的是实例化了一个js线程,我们之前所说的js thread就是它,用来执行js代码的线程

  // Set up the JS thread early
  _jsThread = [[NSThread alloc] initWithTarget:self
                                      selector:@selector(runJSRunLoop)
                                        object:nil];
  _jsThread.name = RCTJSThreadName;
  _jsThread.qualityOfService = NSOperationQualityOfServiceUserInteractive;
  [_jsThread start];

这个线程执行的方法是runJSRunLoop通过名字大家也能猜出一个大概吧。其实就是启动了线程的runloop,保证了_jsThread能够一直运行。

2、创建了一个group,用来初始化Birdge。接下来就开始加载我们本地所有的已经实现了RCTBridgeModule协议的modules

   dispatch_group_t prepareBridge = dispatch_group_create();
  // Initialize all native modules that cannot be loaded lazily
  [self _initModulesWithDispatchGroup:prepareBridge];

3、让我们看看initModulesWithDispatchGroup内部

 for (Class moduleClass in RCTGetModuleClasses()) {
    NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass);

    // Don't initialize the old executor in the new bridge.
    // TODO mhorowitz #10487027: after D3175632 lands, we won't need
    // this, because it won't be eagerly initialized.
    if ([moduleName isEqual:@"RCTJSCExecutor"]) {
      continue;
    }

    // Check for module name collisions
    RCTModuleData *moduleData = moduleDataByName[moduleName];
    if (moduleData) {
      if (moduleData.hasInstance) {
        // Existing module was preregistered, so it takes precedence
        continue;
      } else if ([moduleClass new] == nil) {
        // The new module returned nil from init, so use the old module
        continue;
      } else if ([moduleData.moduleClass new] != nil) {
        // Both modules were non-nil, so it's unclear which should take precedence
        RCTLogError(@"Attempted to register RCTBridgeModule class %@ for the "
                    "name '%@', but name was already registered by class %@",
                    moduleClass, moduleName, moduleData.moduleClass);
      }
    }

    // Instantiate moduleData
    // TODO #13258411: can we defer this until config generation?
    moduleData = [[RCTModuleData alloc] initWithModuleClass:moduleClass
                                                     bridge:self];
    moduleDataByName[moduleName] = moduleData;
    [moduleClassesByID addObject:moduleClass];
    [moduleDataByID addObject:moduleData];
  }

主要是循环遍历用2个数组分别存储了所有的class和RCTModuleData(存储着我们class所有的信息包括class名称、所有的方法、等一些有效的信息和一个instance。还有一个Dictionary key是class,value是moudleData。这个就是通过class名称去取moduleData用的。

这个方法中有一个很重要的方法RCTGetModuleClasses()

void RCTRegisterModule(Class);
void RCTRegisterModule(Class moduleClass)
{
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    RCTModuleClasses = [NSMutableArray new];
  });

  RCTAssert([moduleClass conformsToProtocol:@protocol(RCTBridgeModule)],
            @"%@ does not conform to the RCTBridgeModule protocol",
            moduleClass);

  // Register module
  [RCTModuleClasses addObject:moduleClass];
}

这个就是我们一直说的必须要实现了RCTBridgeModule这个协议,如果实现其实会直接报错的。这个加载是通过RCT_EXPORT_MODULE()宏来实现的,宏是通过运行时+load()方法在我们应用启动的时候其实已经将所有的module加入到了数组之中。

4 、以上终于完成了Modules信息的保存, 接着会进行JSExecutorFactory的初始化和相关的设置, JSExecutorFactory内部会持有一个JSCExecutor 所有与JS的通信,一定都通过JSCExecutor来进行, 所以需要重点关注, 它的构造函数中调用了initOnJSVMThread(), 里面进行了很多的JS上下文的准备, 创建JSClass, 全局的context, 以及添加全局的回调.注意在这个里面使用到的JSC_JSXXX的宏的作用, 实际上是会转换为调用苹果的JavaScriptCore对应的方法(去掉JSC_), 同时还要留意JSCExecutor构造函数中为js的上下文中设置了一个Proxy, nativeModuleProxy.

void JSCExecutor::initOnJSVMThread()
  // 在js的上下文中注册全局的闭包回调 , 这两个是我们需要重点关注的回调.
  nativeFlushQueueImmediate
  nativeCallSyncHook

    installNativeHook<&JSCExecutor::nativeFlushQueueImmediate>("nativeFlushQueueImmediate");
    // 这一个注册的回调, 可以用于在js中直接调用oc, 注意, 在rn中js调用oc一般都是由oc发起的, 
    // 这就像我们的界面一般是有用户的操作才会触发系统调用, rn中类似的, 
    // 当oc调了js后, 在对应的js回调中才会实现调用oc的逻辑

 // 在react native MessageQueue.js中的源码中可以找到 这样一段代码, 
 // 就是当oc没有及时的在消息队列中调js的时候, 大于5ms后js就会调用这个回调, 主动调用oc

   MIN_TIME_BETWEEN_FLUSHES_MS = 5ms;
      if (global.nativeFlushQueueImmediate &&
        (now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS ||
         this._inCall === 0)) {
      var queue = this._queue;
      this._queue = [[], [], [], this._callID];
      this._lastFlush = now;
      global.nativeFlushQueueImmediate(queue);
    }

// 这个回调是用来调用查找native中对应的方法相关信息的

installNativeHook<&JSCExecutor::nativeCallSyncHook>("nativeCallSyncHook");
在react native的 NativeModules.js中有相关的调用来获取native方法的相关信息
function genMethod(moduleID: number, methodID: number, type: MethodType) {
//...
    return global.nativeCallSyncHook(moduleID, methodID, args);
}

为js的上下设置 nativeModuleProxy, 这一个属性很重要, js端用来获取所有注册的nativeModule. 在js中我们需要引入native端的时候回写这样的类似代码 var {NativeModules} from 'react-native', 实际上就是获取到我们这里设置的.

JSCExecutor::JSCExecutor()
  {
  // 这个函数在js的全局上下文中设置了`nativeModuleProxy`, 
// 使得在js端可以获取到, 同时注意这个函数的第三个参数中
// 还包装了一个方法用于调用, 里面涉及到JSCNativeModules这个类, 
// 就是获取NativeModule的操作, 后面分析
    installGlobalProxy(m_context, "nativeModuleProxy",
                       exceptionWrapMethod<&JSCExecutor::getNativeModule>());
  }

在react-native 的NativeModules.js底部有这样的代码, 就直接调用了上面在native端设置的全局属性了 

let NativeModules : {[moduleName: string]: Object} = {};
if (global.nativeModuleProxy) {
  NativeModules = global.nativeModuleProxy;
}
module.exports = NativeModules;

那么js中是怎么获取到我们之前在native端已经生成好的'配置表'信息的呢? 这个问题需要我们重点关注下, 在之前的react-native版本中, 是通过native端直接在js的上下文中注入这样一个__fbBatchedBridgeConfig配置表,
传过去一个json(remoteModuleConfig). 现在的版本中改了一些. 还是上面这段代码. 在如下的调用栈中,我们只需要关注最后一个函数.

JSCExecutor::getNativeModule()
JSCNativeModules::getModule()
JSCNativeModules::createModule()
ModuleRegistry::getConfig()
RCTNativeModule::getMethods()
NSStringFromSelector(selector) hasPrefix:@"rct_export"(初始化RCTModuleMethod信息)

folly::Optional<Object> JSCNativeModules::createModule(const std::string& name, JSContextRef context) {
  if (!m_genNativeModuleJS) {
// 获取js中contenxt中的global
    auto global = Object::getGlobalObject(context);  
// 获取 global中的__fbGenNativeModule对象
    m_genNativeModuleJS = global.getProperty("__fbGenNativeModule").asObject();
    m_genNativeModuleJS->makeProtected();
  }

 // 这个方法最终会调到 RCTNativeModule::getMethods()中,
 // 取得之前使用宏暴露的所有的方法  -> 前缀是 "__rct_export__"
 auto result = m_moduleRegistry->getConfig(name);


  if (!result.hasValue()) {
    return nullptr;
  }
// 调用 js中的__fbGenNativeModule方法, 并且传递过去native端的配置表, 用于端js处理
  Value moduleInfo = m_genNativeModuleJS->callAsFunction({
    Value::fromDynamic(context, result->config),
    Value::makeNumber(context, result->index)
  });
  return moduleInfo.asObject().getProperty("module").asObject();
}

// 下面是对应的js中的处理, NativeModules.js
// 暴露一个全局的属性给native(就是上面我们调用的js中的方法, 触发了后面的js后取到native端的配置表, 并且保存)
// export this method as a global so we can call it from native
global.__fbGenNativeModule = genModule;

function genModule(...) {
  // 获取到native端调用时传递过来的配置信息
  const [moduleName, constants, methods, promiseMethods, syncMethods] = config;
  const module = {};
  // 遍历 所有的native的方法列表, 获取所有native的方法的详细信息
  methods && methods.forEach((methodName, methodID) => {
    module[methodName] = genMethod(moduleID, methodID, methodType);
  });

}
function genMethod(...) {
  /// ...
  // 调用之前native端在js中注入的回调, 获取native中的所有方法的详细信息
  return global.nativeCallSyncHook(moduleID, methodID, args);
}

5、 使用dispatch_group方式初始化Instance(执行代码需要), 和加载js代码RCTJavaScriptLoader,这个类比较单纯,就是用来加载js代码的,没有其他用处

NSData *sourceCode = [RCTJavaScriptLoader attemptSynchronousLoadOfBundleAtURL:self.bundleURL
                                                                 runtimeBCVersion:bcVersion
                                                                     sourceLength:NULL
                                                                            error:&error];

6、 modules and source code都已经准备好了, 在notify中JSCExecutor专属的Thread内执行jsbundle代码执行js代码. 执行完毕后会将_displayLink添加到runloop(注意是在jsThread所在的runloop)中, 开始运行。

[RCTCxxBridge executeSourceCode: ]
[RCTCxxBridge enqueueApplicationScript:]
void Instance::loadScriptFromString()
void NativeToJsBridge::loadApplication()
void JSCExecutor::loadApplicationScript()
void JSCExecutor::flush()
void JSCExecutor::bindBridge()
上面的调用栈中我们主要关注后面几个函数

void JSCExecutor::loadApplicationScript(), 在这个函数中注意下面两行代码, 注意, RN中有个很明显的问题就是, 首次进入RN模块的时候, 加载的很慢, 会有几秒的加载时间, 其实就是在这个函数中加载jsBundle造成的, 如果要优化加载的时间, 以及不同模块页面切换流畅, 就需要对jsBundle文件进行精简, 缓存等操作。

  // 执行js代码, 加载jsBundle
  evaluateScript(m_context, jsScript, jsSourceURL);
  // 在加载完jsbundle后主动调用一次, 为js的上下文环境添加必要的全局属性和回调
  flush();

void JSCExecutor::bindBridge()保存js的上下文环境中必要的全局属性和回调, 这些在 react native的 MessageQueue.js中定义的调用方法, 当native需要调用js的方法的时候, 需要通过调用这些js方法, 在这些方法中, js端会根据传递的信息查找到在js中需要调用的方法.

    auto global = Object::getGlobalObject(m_context);
    auto batchedBridgeValue = global.getProperty("__fbBatchedBridge");
// 这些是在MessageQueue.js中定义的调用方法
    auto batchedBridge = batchedBridgeValue.asObject();
    m_callFunctionReturnFlushedQueueJS = batchedBridge.getProperty("callFunctionReturnFlushedQueue").asObject();
    m_invokeCallbackAndReturnFlushedQueueJS = batchedBridge.getProperty("invokeCallbackAndReturnFlushedQueue").asObject();
    m_flushedQueueJS = batchedBridge.getProperty("flushedQueue").asObject();
    m_callFunctionReturnResultAndFlushedQueueJS = batchedBridge.getProperty("callFunctionReturnResultAndFlushedQueue").asObject();
  }

7 、上面的工作完成之后, 初始化bridge的工作就完成了, jsBundle已经加载完成, oc端的'配置表'也已经处理好, 并且成功已经传递给js端, js的上下文配置已经准备好, 下面就是开始执行我们的js代码了, React就会开始计算好所有的布局信息, 以及Component层级关系等, 等待native端完成对应的真正的页面渲染和布局. 回到RCTRootView的初始化方法中, 注意在[RCTRootView initWithBridge:...]的初始化方法中, 注册了几个js执行情况的通知, 我们重点关注js执行完毕后的通知RCTJavaScriptDidLoadNotification

- (instancetype)initWithBundleURL:(NSURL *)bundleURL
                       moduleName:(NSString *)moduleName
                initialProperties:(NSDictionary *)initialProperties
                    launchOptions:(NSDictionary *)launchOptions
{
  RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:bundleURL
                                            moduleProvider:nil
                                             launchOptions:launchOptions];
// 上面完成了bridge的初始化工作 , 下面开始完成rootView的初始化
  return [self initWithBridge:bridge moduleName:moduleName initialProperties:initialProperties];
}

下面是js执行完毕后的通知RCTJavaScriptDidLoadNotification中的一系列的处理

[RCTRootView javaScriptDidLoad]
[RCTRootView bundleFinishedLoading]
RCTRootContentView (创建contentView)
[RCTRootView runApplication]
在创建RCTRootContentView的时候, 注意有个参数是reactTag, 这个属性很重要, 每一个reactTag都应该是唯一的, 从1开始, 每次递增10. RCTRootContentView初始化时, 还需要在RCTUIManager中通过reactTag去注册, 从而由RCTUIManager来统一管理所有的js端使用Component对应的每个原生view(_viewRegistry[tag]表), 有了这个, 我们就可以很方便的在其他地方通过reactTag获取到我们的Component所在的rootView.

    /**
     * Every root view that is created must have a unique react tag.
     * Numbering of these tags goes from 1, 11, 21, 31, etc
     *
     * NOTE: Since the bridge persists, the RootViews might be reused, so the
     * react tag must be re-assigned every time a new UIManager is created.
     */

- (NSNumber *)allocateRootTag
{
  NSNumber *rootTag = objc_getAssociatedObject(self, _cmd) ?: @1;
  objc_setAssociatedObject(self, _cmd, @(rootTag.integerValue + 10), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  return rootTag;
}

然后才是[RCTRootView runApplication], 这里就会调用js里面AppRegistry对应的方法runApplication了, 在AppRegistry.js中有下面的一段注释, 已经解释得很清楚了

/**
 * `AppRegistry` is the JS entry point to running all React Native apps.  App
 * root components should register themselves with
 * `AppRegistry.registerComponent`, then the native system can load the bundle
 * for the app and then actually run the app when it's ready by invoking
 * `AppRegistry.runApplication`.
 *
 * To "stop" an application when a view should be destroyed, call
 * `AppRegistry.unmountApplicationComponentAtRootTag` with the tag that was
 * passed into `runApplication`. These should always be used as a pair.
 *
 * `AppRegistry` should be `require`d early in the `require` sequence to make
 * sure the JS execution environment is setup before other modules are
 * `require`d.
 */

- (void)runApplication:(RCTBridge *)bridge
{
  NSString *moduleName = _moduleName ?: @"";
  NSDictionary *appParameters = @{
    @"rootTag": _contentView.reactTag,
    @"initialProps": _appProperties ?: @{},
  };
// 调用AppRegistry.js中的runApplication开始运行
  RCTLogInfo(@"Running application %@ (%@)", moduleName, appParameters);
  [bridge enqueueJSCall:@"AppRegistry"
                 method:@"runApplication"
                   args:@[moduleName, appParameters]
             completion:NULL];
}

8、 因为执行js代码的时候, js端会计算好每个view的布局, 属性等信息, 然后通过调用native的系统方法来完成, 页面的渲染, 页面的布局. 这个过程中就涉及到了js 和native的互调通信了. 这个过程分为两个部分.

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

推荐阅读更多精彩内容