QC-L/blog

ReactNative 0.54.4 基于 iOS 端源码解析 (一) :探究 RCTBundleURLProvider

QC-L opened this issue · 0 comments

QC-L commented

ReactNative 0.54.4 基于 iOS 端源码解析(一):探究 RCTBundleURLProvider

最近在做优化相关事宜,需要了解 ReactNative 的原理。由于公司相关版本是 0.54.4 ,所以本源码解析也基于 0.54.4 。由于整个 ReactNative 项目分为两端,整体代码体系较为庞大,因此,本人先从 iOS 端着手进行源码分析。

准备

  1. 安装 Node 环境 (安利下本人编写的 install-node-sh):

    curl -o- https://raw.githubusercontent.com/QC-L/install-node-sh/master/install-node.sh | bash
    
  2. 安装最新的 react-native-cli

    npm install -g react-native-cli
    

    or

    yarn add global react-native-cli
    
  3. 初始化 0.54.4 版本的 ReactNative 项目

    react-native init TestOptimize --version 0.54.4
    
  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 生成了一个 jsCodeLocationNSURL 对象。另外从名字上也可以看出,这是一个 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 进行源码解析。