ReactNative 0.54.4 基于 iOS 端源码解析 (一) :探究 RCTBundleURLProvider
QC-L opened this issue · 0 comments
ReactNative 0.54.4 基于 iOS 端源码解析(一):探究 RCTBundleURLProvider
最近在做优化相关事宜,需要了解 ReactNative 的原理。由于公司相关版本是 0.54.4 ,所以本源码解析也基于 0.54.4 。由于整个 ReactNative 项目分为两端,整体代码体系较为庞大,因此,本人先从 iOS 端着手进行源码分析。
准备
-
安装 Node 环境 (安利下本人编写的 install-node-sh):
curl -o- https://raw.githubusercontent.com/QC-L/install-node-sh/master/install-node.sh | bash
-
安装最新的
react-native-cli
:npm install -g react-native-cli
or
yarn add global react-native-cli
-
初始化
0.54.4
版本的 ReactNative 项目react-native init TestOptimize --version 0.54.4
-
运行:
react-native run-ios
运行结果如下:
源码走起🏂
做过 iOS 原生开发的童鞋应该都有经验,iOS 项目的代码会从 AppDelegate
开始阅读。
打开 AppDelegate.m
文件,熟悉又陌生的代码映入眼帘:
#import "AppDelegate.h"
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSURL *jsCodeLocation;
jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"TestOptimize"
initialProperties:nil
launchOptions:launchOptions];
rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [UIViewController new];
rootViewController.view = rootView;
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];
return YES;
}
@end
简单阅览以上代码我们会提出如下两点疑问:
RCTBundleURLProvider
是啥?RCTRootView
又是啥?
接下来带着以上两个疑问,开启我们的寻码之旅:
探究 RCTBundleURLProvider🔍
查看生成的 RCTBundleURLProvider
具体做了什么?
其实如果接触 ReactNative 历史版本的话,会很清楚的知道,其实 RCTBundleURLProvider
生成了一个 jsCodeLocation
的 NSURL
对象。另外从名字上也可以看出,这是一个 jsBundleURL
的 Provider(生成器)。
历史版本的 jsCodeLocation
像下面这样👇:
jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.bundle?platform=ios&dev=true"];
大体了解了 RCTBundleURLProvider
的作用,源码读起来:
NSURL *jsCodeLocation;
jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
从调用了类方法 sharedSetting
可以猜测该类可能是单例。
点击查看该方法,可以核实我们的猜测是正确的:
+ (instancetype)sharedSettings
{
static RCTBundleURLProvider *sharedInstance;
static dispatch_once_t once_token;
dispatch_once(&once_token, ^{
sharedInstance = [RCTBundleURLProvider new];
});
return sharedInstance;
}
获得该类实例后,紧接着调用了 jsBundleURLForBundleRoot:fallbackResource:
实例方法:
/**
* 根据传入的 bundleRoot,生成 jsBundle 的 URL
*
* @param bundleRoot 开启 Sever 服务的 bundle 名 默认传入 index
* @param resourceName 资源名 默认为 main.jsbundle
* @return NSURL 对象
*/
- (NSURL *)jsBundleURLForBundleRoot:(NSString *)bundleRoot fallbackResource:(NSString *)resourceName {
return [self jsBundleURLForBundleRoot:bundleRoot fallbackResource:resourceName fallbackExtension:nil];
}
看到这里,发现 jsBundleURLForBundleRoot:fallbackResource:
实例方法,内部其实调用了 jsBundleURLForBundleRoot:fallbackResource:fallbackExtension:
/**
根据传入的 bundleRoot 或 resourceName,生成 jsBundle 的 URL
@param bundleRoot bundleRoot 开启 Sever 服务的 bundle 名
@param resourceName 资源名,填本地 jsbundle 的资源名,默认为 main.jsbundle
@param extension 资源名的后缀
@return NSURL 对象
*/
- (NSURL *)jsBundleURLForBundleRoot:(NSString *)bundleRoot fallbackResource:(NSString *)resourceName fallbackExtension:(NSString *)extension
{
// 1. 获取 packagerServerHost
NSString *packagerServerHost = [self packagerServerHost];
// 2. 判断 packagerServerHost 是否存在
if (!packagerServerHost) {
// 3. 如果 packagerServerHost 不存在, 则根据 resourceName(资源名) 和 extension(后缀) 读取本地文件
// 并返回 url
return [self jsBundleURLForFallbackResource:resourceName fallbackExtension:extension];
} else {
// 4.如果 packagerServerHost 存在, 根据 packagerServerHost 和 bundleRoot 等生成 URL
// 备注: 后两个参数为 开发模式是否开启 和 压缩模式是否开启
// 查看 [self defaults] 可以得知默认情况下 dev 开启, minify 关闭
return [RCTBundleURLProvider jsBundleURLForBundleRoot:bundleRoot
packagerHost:packagerServerHost
enableDev:[self enableDev]
enableMinification:[self enableMinification]];
}
}
获取 packagerServerHost
,具体实现如下:
- (NSString *)packagerServerHost
{
// NSUserdefaults 中获取 RCT_jsLocation
NSString *location = [self jsLocation];
NSLog(@"------------------%@------------------", location);
// 默认情况下, location 为 null
if (location != nil) {
// 不为空, 返回 location
return location;
}
// 如果开发环境
#if RCT_DEV
// 获取 package 的 Host
NSString *host = [self guessPackagerHost];
NSLog(@"=================%@=================", host);
// 默认情况下, host 为 localhost
// 此时, 我添加了 ip.txt 文件, 则 host 为 127.0.0.1
if (host) {
return host;
}
#endif
return nil;
}
上述代码都很简单,不作过多赘述,我们来看一个小细节。
在实例方法 packagerServerHost
中,有这样一个方法叫 guessPackagerHost
。
内部读取了 ip.txt
的文件,所以当你想要修改 packager
中的 host
的时候,你可以创建该文件,在文件中填入 host
即可。
同样,如下代码也是只在开发环境下运行:
#if RCT_DEV
- (BOOL)isPackagerRunning:(NSString *)host
{
NSURL *url = [serverRootWithHost(host) URLByAppendingPathComponent:@"status"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLResponse *response;
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:NULL];
NSString *status = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
return [status isEqualToString:@"packager-status:running"];
}
- (NSString *)guessPackagerHost
{
static NSString *ipGuess;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 获取 bundle 中 ip.txt 获取路径
NSString *ipPath = [[NSBundle mainBundle] pathForResource:@"ip" ofType:@"txt"];
// 将路径文件转换为字符串
ipGuess = [[NSString stringWithContentsOfFile:ipPath encoding:NSUTF8StringEncoding error:nil]
stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];
NSLog(@"++++++++++++++++++++++%@++++++++++++++++++++++", ipPath);
NSLog(@"------------------%@------------------", ipGuess);
});
// 如果 ip.txt 存在, 并且有内容, 则展示 ip.txt 中的内容, 否则为 localhost
NSString *host = ipGuess ?: @"localhost";
// 判断该 host 是否运行
if ([self isPackagerRunning:host]) {
// 有效返回 host
return host;
}
// 以上均未返回, 则返回 nil
return nil;
}
#endif
获取到 Host 之后,后面的逻辑就很容易猜了:
// 判断 packagerServerHost 是否存在
if (!packagerServerHost) {
// 如果 packagerServerHost 不存在, 则根据 resourceName(资源名) 和 extension(后缀) 读取本地文件
// 并返回 url
return [self jsBundleURLForFallbackResource:resourceName fallbackExtension:extension];
} else {
// 如果 packagerServerHost 存在, 根据 packagerServerHost 和 bundleRoot 等生成 URL
// 备注: 后两个参数为 开发模式是否开启 和 压缩模式是否开启
// 查看 [self defaults] 可以得知默认情况下 dev 开启, minify 关闭
return [RCTBundleURLProvider jsBundleURLForBundleRoot:bundleRoot
packagerHost:packagerServerHost
enableDev:[self enableDev]
enableMinification:[self enableMinification]];
}
如果 packagerServerHost
存在,则走 else 中的代码;如果 packagerServerHost
不存在,则会根据 resourceName 和 extension 读取本地文件。resourceName 的默认值为 main.jsbundle
。
如果 packagerServerHost
不存在时,调用的方法实现如下:
- (NSURL *)jsBundleURLForFallbackResource:(NSString *)resourceName
fallbackExtension:(NSString *)extension
{
// 资源名默认为 main
resourceName = resourceName ?: @"main";
// 资源后缀默认为 jsbundle
extension = extension ?: @"jsbundle";
// 从主 bundle 获取该资源的 url
return [[NSBundle mainBundle] URLForResource:resourceName withExtension:extension];
}
else 中调用的方法实现:
+ (NSURL *)jsBundleURLForBundleRoot:(NSString *)bundleRoot
packagerHost:(NSString *)packagerHost
enableDev:(BOOL)enableDev
enableMinification:(BOOL)enableMinification
{
// 根据你起的 bundleRoot 生成路径
// 默认传入的为 index ,则 path 为 index.bundle
NSString *path = [NSString stringWithFormat:@"/%@.bundle", bundleRoot];
// When we support only iOS 8 and above, use queryItems for a better API.
// 如果默认所有参数都开启, 则最终 query 为 platform=ios&dev=true&minify=false
NSString *query = [NSString stringWithFormat:@"platform=ios&dev=%@&minify=%@",
enableDev ? @"true" : @"false",
enableMinification ? @"true": @"false"];
// 17. 根据 path、packagerHost 及 query 生成 URL
return [[self class] resourceURLForResourcePath:path packagerHost:packagerHost query:query];
}
PS:其中初始化时,会执行 [self defaults]
产生默认值。其中 dev 为 true,minify 为 false 。
- (NSDictionary *)defaults {
return @{
kRCTEnableLiveReloadKey: @NO,
kRCTEnableDevKey: @YES,
kRCTEnableMinificationKey: @NO,
};
}
无论如何,最终都会返回一个 NSURL 对象给 AppDelegate
,至此我们对 RCTBundleURLProvider
有了一个基本了解。
下一篇文章,我们将对 RCTRootView
进行源码解析。