Skip to content

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

Open
@QC-L

Description

@QC-L

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 进行源码解析。

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions