React Native 源码解析之RCTJavaScriptLoader

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

以下基于RN 0.38的版本分析

1.React Native结构图

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

cmd-markdown-logo

2.React Native代码类结构图

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

cmd-markdown-logo

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

3.RCTJavaScriptLoader 源码分析

3.1 RN初始化

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

- (instancetype)initWithDelegate:(id<RCTBridgeDelegate>)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<RCTBridgeDelegate>),如果外部实现了如下的接口,皆可以通过外部的方式加载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)];

断点调试调用栈如下:


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 的执行有什么不同,后续再作分析,对本文有什么意见,建议欢迎留言,提出,谢谢。

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

推荐阅读更多精彩内容