React Native 源码解析之RCTJavaScriptLoader

一直希望抽时间了解一下React Native的源码,充分了解它的实现机制。从本文开始推出React Native 源码解析系列文章(以iOS为主),有问题欢迎留言指正,谢谢。

以下基于RN 0.38的版本分析

1.React Native结构图

摘抄自:折腾范儿の味精-ReactNative iOS源码解析(一)

React Native 源码解析之RCTJavaScriptLoader_第1张图片
cmd-markdown-logo

2.React Native代码类结构图

摘抄自:折腾范儿の味精-ReactNative iOS源码解析(一)

React Native 源码解析之RCTJavaScriptLoader_第2张图片
cmd-markdown-logo

以上是摘抄的关于React Native的两个结构图,后续将基于这两个图作底层的细化分析,重点关注具体的代码实现层。现在直接进入本文的主题:jsbundle的加载类RCTJavaScriptLoader

3.RCTJavaScriptLoader 源码分析

3.1 RN初始化

RN的对外的加载无非是通过RCTBridge.h如下两个初始化方法:

- (instancetype)initWithDelegate:(id)delegate
                   launchOptions:(NSDictionary *)launchOptions;
- (instancetype)initWithBundleURL:(NSURL *)bundleURL
                   moduleProvider:(RCTBridgeModuleProviderBlock)block
                    launchOptions:(NSDictionary *)launchOptions;

核心都会调用到[self setUp];,我们来看看setUp的具体实现:

 // Only update bundleURL from delegate if delegate bundleURL has changed
  NSURL *previousDelegateURL = _delegateBundleURL;
  _delegateBundleURL = [self.delegate sourceURLForBridge:self];
  if (_delegateBundleURL && ![_delegateBundleURL isEqual:previousDelegateURL]) {
    _bundleURL = _delegateBundleURL;
  }

  // Sanitize the bundle URL
  _bundleURL = [RCTConvert NSURL:_bundleURL.absoluteString];

  [self createBatchedBridge];
  [self.batchedBridge start];

createBatchedBridge:创建RCTBridge持有的真正处理核心操作的实例:self.batchedBridge;

[self.batchedBridge start]:初始化js/OC交互所需要的完整的环境配置,其中就包含了本文的核心内容:jsbundle的加载。

3.2 jsbundle的加载

在RN初始化中我们找到了jsbunsle加载的核心代码片段:

  __weak RCTBatchedBridge *weakSelf = self;
  __block NSData *sourceCode;
  [self loadSource:^(NSError *error, NSData *source, __unused int64_t sourceLength) {
    if (error) {
      RCTLogWarn(@"Failed to load source: %@", error);
      dispatch_async(dispatch_get_main_queue(), ^{
        [weakSelf stopLoadingWithError:error];
      });
    }

    sourceCode = source;
    dispatch_group_leave(initModulesAndLoadSource);
  } onProgress:^(RCTLoadingProgress *progressData) {
#ifdef RCT_DEV
    RCTDevLoadingView *loadingView = [weakSelf moduleForClass:[RCTDevLoadingView class]];
    [loadingView updateProgress:progressData];
#endif
  }];
- (void)loadSource:(RCTSourceLoadBlock)_onSourceLoad onProgress:(RCTSourceLoadProgressBlock)onProgress
{
  ...
  // 1.如果外部实现了bundle的加载直接走外部的加载逻辑
  if ([self.delegate respondsToSelector:@selector(loadSourceForBridge:onProgress:onComplete:)]) {
    [self.delegate loadSourceForBridge:_parentBridge onProgress:onProgress onComplete:onSourceLoad];
  } else if ([self.delegate respondsToSelector:@selector(loadSourceForBridge:withBlock:)]) {
    [self.delegate loadSourceForBridge:_parentBridge withBlock:onSourceLoad];
  } else {
    RCTAssert(self.bundleURL, @"bundleURL must be non-nil when not implementing loadSourceForBridge");

    // 2.外部未实现就走默认的加载逻辑
    [RCTJavaScriptLoader loadBundleAtURL:self.bundleURL onProgress:onProgress onComplete:^(NSError *error, NSData *source, int64_t sourceLength) {
      if (error && [self.delegate respondsToSelector:@selector(fallbackSourceURLForBridge:)]) {
        NSURL *fallbackURL = [self.delegate fallbackSourceURLForBridge:self->_parentBridge];
        if (fallbackURL && ![fallbackURL isEqual:self.bundleURL]) {
          RCTLogError(@"Failed to load bundle(%@) with error:(%@)", self.bundleURL, error.localizedDescription);
          self.bundleURL = fallbackURL;
          [RCTJavaScriptLoader loadBundleAtURL:self.bundleURL onProgress:onProgress onComplete:onSourceLoad];
          return;
        }
      }
      onSourceLoad(error, source, sourceLength);
    }];
  }
}

回到3.1中RN的对外的方式初始化方法的第一种,需要传入(id),如果外部实现了如下的接口,皆可以通过外部的方式加载jsbundle的数据,如:考虑jsbundle的安全性可以考虑本地文件做加密,加载时再解密加载等其他场景,对此我们公司做的分包热更新加载就是通过扩展该协议实现的。

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge;

- (void)loadSourceForBridge:(RCTBridge *)bridge
                 onProgress:(RCTSourceLoadProgressBlock)onProgress
                 onComplete:(RCTSourceLoadBlock)loadCallback;

- (void)loadSourceForBridge:(RCTBridge *)bridge
                  withBlock:(RCTSourceLoadBlock)loadCallback;

jsbundle默认实现的加载

外部的实现方式我们不作深究,下面分析默认的加载实现机制。构造了如下的两种测试cases:

// 第一种加载服务端的bundle
jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios&dev=true"];
// 第二种加载本地的jsbundle
 NSString *path = [[NSBundle mainBundle] pathForResource:@"main"
                                                   ofType:@"jsbundle"];
  jsCodeLocation = [NSURL URLWithString:path];

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

jsbundle 异步加载

前提

利用budle的命令(后续会讲到为什么使用bundle)打加载的main.jsbundle,开始测试。

断点查看均走入到异步加载bundle的方法体中:

static void attemptAsynchronousLoadOfBundleAtURL(NSURL *scriptURL, RCTSourceLoadProgressBlock onProgress, RCTSourceLoadBlock onComplete)
{
  scriptURL = sanitizeURL(scriptURL); // 1.检测url的有效性

  // 2.异步的方式加载本地的jsbundle:第二种加载方式
  if (scriptURL.fileURL) {
    // Reading in a large bundle can be slow. Dispatch to the background queue to do it.
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      NSError *error = nil;
      NSData *source = [NSData dataWithContentsOfFile:scriptURL.path
                                              options:NSDataReadingMappedIfSafe
                                                error:&error];
      onComplete(error, source, source.length);
    });
    return;
  }

 /* 
  * 3.第一种加载方式:实例化一个下载的任务,实际是 
  * NSURLSessionDataTask实现的,然后开始任务
  */
  RCTMultipartDataTask *task = [[RCTMultipartDataTask alloc] initWithURL:scriptURL partHandler:^(NSInteger statusCode, NSDictionary *headers, NSData *data, NSError *error, BOOL done) {
    if (!done) {
      if (onProgress) {
        onProgress(progressEventFromData(data));
      }
      return;
    }

    // Handle general request errors
    if (error) {
      if ([error.domain isEqualToString:NSURLErrorDomain]) {
        error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain
                                    code:RCTJavaScriptLoaderErrorURLLoadFailed
                                userInfo:
                 @{
                   NSLocalizedDescriptionKey:
                     [@"Could not connect to development server.\n\n"
                      "Ensure the following:\n"
                      "- Node server is running and available on the same network - run 'npm start' from react-native root\n"
                      "- Node server URL is correctly set in AppDelegate\n\n"
                      "URL: " stringByAppendingString:scriptURL.absoluteString],
                   NSLocalizedFailureReasonErrorKey: error.localizedDescription,
                   NSUnderlyingErrorKey: error,
                   }];
      }
      onComplete(error, nil, 0);
      return;
    }
    
    // For multipart responses packager sets X-Http-Status header in case HTTP status code
    // is different from 200 OK
    NSString *statusCodeHeader = [headers valueForKey:@"X-Http-Status"];
    if (statusCodeHeader) {
      statusCode = [statusCodeHeader integerValue];
    }

    if (statusCode != 200) {
      error = [NSError errorWithDomain:@"JSServer"
                                  code:statusCode
                              userInfo:userInfoForRawResponse([[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding])];
      onComplete(error, nil, 0);
      return;
    }
    onComplete(nil, data, data.length);
  }];

  [task startTask];
}

上述中的最后一段关于multipart responses,起初不太了解为什么要加入这一段,RCTMultipartDataTask初始化请求也加入相关的如下的代码段:

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:_url];
if (isStreamTaskSupported()) {
    [request addValue:@"multipart/mixed" forHTTPHeaderField:@"Accept"];
}

查阅了相关的资料了解是这样设置是可以接受带附件的数据,此处不作详细的扩展。

jsbundle 同步加载

在bundle的加载逻辑中是先尝试同步加载,失败了再尝试异步方式加载,在上述的例子中,在attemptSynchronousLoadOfBundleAtURL方法中设置断点,如下代码处直接return nil;,不得不好奇RCTScriptTag是什么?RCTMagicNumber magicNumber又是什么?下面来做一一分析。

前提

利用unbudle的命令(后续会讲到为什么使用unbundle)打加载的main.jsbundle,开始测试。

RCTMagicNumber/RCTScriptTag

  • RCTMagicNumber:一个联合体,用于表示不同的bundle包的类型的数据结构。
  • RCTScriptTag:区分不同的bundle的类型,通过取出bundle头部的数据,与RCTRAMBundleMagicNumber,RCTBCBundleMagicNumber匹配,返回bundle的类型(注意js在构建bundle时根据不同的命令打出不同类型的bundle)。目前存在三种:
    普通js文本bundle,可随机访问的bundle(按需加载使用),字节码的bundle。
/**
 * RCTMagicNumber
 *
 * RAM bundles and BC bundles begin with magic numbers. For RAM bundles this is
 * 4 bytes, for BC bundles this is 8 bytes. This structure holds the first 8
 * bytes from a bundle in a way that gives access to that information.
 */
typedef union {
  uint64_t allBytes;
  uint32_t first4;
  uint64_t first8;
} RCTMagicNumber;

/**
 * RCTScriptTag
 *
 * Scripts given to the JS Executors to run could be in any of the following
 * formats. They are tagged so the executor knows how to run them.
 */
typedef NS_ENUM(NSInteger) {
  RCTScriptString = 0,
  RCTScriptRAMBundle,
  RCTScriptBCBundle,
} RCTScriptTag;

static uint32_t const RCTRAMBundleMagicNumber = 0xFB0BD1E5;
static uint64_t const RCTBCBundleMagicNumber  = 0xFF4865726D657300;

NSString *const RCTJavaScriptLoaderErrorDomain = @"RCTJavaScriptLoaderErrorDomain";

RCTScriptTag RCTParseMagicNumber(RCTMagicNumber magic)
{
  if (NSSwapLittleIntToHost(magic.first4) == RCTRAMBundleMagicNumber) {
    return RCTScriptRAMBundle;
  }

  if (NSSwapLittleLongLongToHost(magic.first8) == RCTBCBundleMagicNumber) {
    return RCTScriptBCBundle;
  }

  return RCTScriptString;
}

同时简单介绍一下上述遗留的两种bundle类型的打包方式(react-native-xcode.sh中):bundleunbundle命令分别打出的包对应的就是RCTScriptString,RCTScriptRAMBundle。目前尚不太清楚RCTScriptBCBundle的打包方式,不做扩展。

$NODE_BINARY "$REACT_NATIVE_DIR/local-cli/cli.js" bundle \ // 此处的bundle
  --entry-file "$ENTRY_FILE" \
  --platform ios \
  --dev $DEV \
  --reset-cache \
  --bundle-output "$BUNDLE_FILE" \ 
  --assets-dest "$DEST"

RAM jsbundle的加载

如下代码所述:

 // Load the first 4 bytes to check if the bundle is regular or RAM ("Random Access Modules" bundle).
  // The RAM bundle has a magic number in the 4 first bytes `(0xFB0BD1E5)`.
  // The benefit of RAM bundle over a regular bundle is that we can lazily inject
  // modules into JSC as they're required.
  
  // 1.开启文件读取
  FILE *bundle = fopen(scriptURL.path.UTF8String, "r");
  if (!bundle) {
    if (error) {
      *error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain
                                   code:RCTJavaScriptLoaderErrorFailedOpeningFile
                               userInfo:@{NSLocalizedDescriptionKey:
                                            [NSString stringWithFormat:@"Error opening bundle %@", scriptURL.path]}];
    }
    return nil;
  }
 
  //  2.读取sizeof(magicNumber)大小的头部信息到magicNumber
  RCTMagicNumber magicNumber = {.allBytes = 0};
  size_t readResult = fread(&magicNumber, sizeof(magicNumber), 1, bundle);
  fclose(bundle);
  if (readResult != 1) {
    if (error) {
      *error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain
                                   code:RCTJavaScriptLoaderErrorFailedReadingFile
                               userInfo:@{NSLocalizedDescriptionKey:
                                            [NSString stringWithFormat:@"Error reading bundle %@", scriptURL.path]}];
    }
    return nil;
  }

  // 3.解析magicNumber判断bundle的类型然后区分加载
  RCTScriptTag tag = RCTParseMagicNumber(magicNumber);
  if (tag == RCTScriptString) {
    if (error) {
      *error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain
                                   code:RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously
                               userInfo:@{NSLocalizedDescriptionKey:
                                            @"Cannot load text/javascript files synchronously"}];
    }
    return nil;
  }

  struct stat statInfo;
  if (stat(scriptURL.path.UTF8String, &statInfo) != 0) {
    if (error) {
      *error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain
                                   code:RCTJavaScriptLoaderErrorFailedStatingFile
                               userInfo:@{NSLocalizedDescriptionKey:
                                            [NSString stringWithFormat:@"Error stating bundle %@", scriptURL.path]}];
    }
    return nil;
  }
  if (sourceLength) {
    *sourceLength = statInfo.st_size;
  }
  
  // 4.如果是RAM的bundle直接返回头部的8bytes的数据
  return [NSData dataWithBytes:&magicNumber length:sizeof(magicNumber)];

断点调试调用栈如下:


React Native 源码解析之RCTJavaScriptLoader_第3张图片
cmd-markdown-logo

查看sourceCode变量的处理流程如下,进入了代码的执行阶段,因此可以推断RAM bundle是执行的js代码需要加载其他的模块的代码时才开始加载相应的模块。

 dispatch_group_notify(initModulesAndLoadSource, bridgeQueue, ^{
    RCTBatchedBridge *strongSelf = weakSelf;
    if (sourceCode && strongSelf.loading) {
      [strongSelf executeSourceCode:sourceCode];
    }
  });


bundle加载告一段落,但是执行是如何处理的,普通的bundle,RAM bundle 的执行有什么不同,后续再作分析,对本文有什么意见,建议欢迎留言,提出,谢谢。

你可能感兴趣的:(React Native 源码解析之RCTJavaScriptLoader)