一直希望抽时间了解一下React Native的源码,充分了解它的实现机制。从本文开始推出React Native 源码解析系列文章(以iOS为主),有问题欢迎留言指正,谢谢。
以下基于RN 0.38的版本分析
1.React Native结构图
摘抄自:折腾范儿の味精-ReactNative iOS源码解析(一)
2.React Native代码类结构图
摘抄自:折腾范儿の味精-ReactNative iOS源码解析(一)
以上是摘抄的关于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中):bundle
与unbundle
命令分别打出的包对应的就是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)];
断点调试调用栈如下:
查看sourceCode
变量的处理流程如下,进入了代码的执行阶段,因此可以推断RAM bundle
是执行的js代码需要加载其他的模块的代码时才开始加载相应的模块。
dispatch_group_notify(initModulesAndLoadSource, bridgeQueue, ^{
RCTBatchedBridge *strongSelf = weakSelf;
if (sourceCode && strongSelf.loading) {
[strongSelf executeSourceCode:sourceCode];
}
});
bundle加载告一段落,但是执行是如何处理的,普通的bundle,RAM bundle 的执行有什么不同,后续再作分析,对本文有什么意见,建议欢迎留言,提出,谢谢。