APP中RN页面热更新流程-ReactNative源码分析

平时使用WebStorm或VSCode对RN工程中的文件修改后,在键盘上按一下快捷cmd+s进行文件保存,此时当前调试的RN页面就会自动进行刷新,这是RN开发相比于原生开发一个很大的优点:热更新。
那么,从按一下快捷cmd+s到RN页面展示出最新的JS页面,这个过程是怎样发生的呢?下面根据时间顺序来梳理一下。
这里约定后面说的原生部分是指iOS端,ReactNative源码分析指的是iOS端集成的RN框架分析。
 
原生APP中RN页面的热更新简要流程
React Native应用包含两部分:原生代码和JavaScript代码。JavaScript和原生代码通信的桥梁是Bridge,而Bridge的实现又依赖于JSThread的runloop。
当JS有消息要传递给原生时,它会把消息封装成事件放入到JSThread的runloop的事件队列中,而JSThread的runloop会监听消息队列中的事件,一旦有事件需要处理,就会将其交给Bridge处理,从而实现JavaScript和原生代码的相互调用和数据传输。
 
基于JavaScript和原生代码的消息传递机制,RN热更新步骤如下:
1.在iOS原生中的RN页面触发热更新时,首先是JS环境中的websocket收到了Metro服务器的通知,在这个通知中包含了需要更新的JS bundle的URL地址,然后js将这个通知事件放到了JSThread的runloop中传到了RCTCxxBridge。
2.RCTCxxBridge收到热更新事件后,调用JavaScriptCore框架中的方法来执行一个JavaScript脚本,这个脚本会告诉JavaScript环境去下载新的JS bundle并执行它。事件又回到了JS。
3.JavaScript环境调用下载命令向远程服务器请求新的JS bundle,事件又被转回到了原生侧。
4.原生侧下载最新的bundle.js,下载完成后调用加载,执行js方法。
5.重新设置RN页面的根组件。
 
热更新触发条件
React Native在调试模式下有两种热更新方式:Hot Reloading和Live Reload。Hot Reloading可以实现代码的增量更新,而Live Reload技术则是全量更新。
下面以在index.js中新增一个组件注册语句为例。
在RN项目中index.js是ReactNative项目的入口文件,RN启动时首先会执行这个文件,把组件注册到AppRegistry中,这里在index.js的文件底部顺序添加一个组件注册,然后按快捷键cmd+s保存修改
AppRegistry.registerComponent('FlatListDemo', () => FlatListDemo);
这时本地的Packager服务会监听本地文件系统的变化,当有文件修改并保存时,Packager会运行RN命令行工具,自动生成一个新的bundle文件
react-native bundle --platform ios --dev false --entry-file index.js --bundle-output ios/main.jsbundle --assets-dest ios
然后将生成的bundle文件上传至服务器,接着Metro 服务器通过 WebSocket 链接发送消息给APP应用程序。

 

触发热更新

在APP中RN框架启动时,会在APP的JS运行上下文中创建一个websocket链接并与js服务器建立连接,它用于监听Metro服务器发来的bundle.js更新通知。
当Packger打了新的bundle.js并上传到js服务器后,Metor服务器就会向与它链接的websocket发送更新通知,js引擎收到服务器的通知后,就会做后续的事件处理。
具体js建立websocket的代码如下:
在iOS端ReactNative框架中,RCTCxxBridge文件中的+ (void)runRunLoop方法内负责创建并运行JS引擎对应的jsThread的runloop。它在APP启动时被创建,用来处理js引擎的任务和事件,并保证了_jsThread的常驻,不被销毁。
runloop被注册时机在RN框架启动时:
js引擎收到服务器的通知后,被包成一个原生runloop的事件源放入到事件队列中,然后jsThread的runloop开始处理事件。
然后把事件加入到RCTMessageThread中进行异步处理
RCTCxxBridge调用js脚本,让js处理与最新bundle.js下载相关的事件。把事件传入到js事件队列中
js引擎处理完JS层面的事件后,将事件转回给原生,让原生代码执行实际的下载工作
原生侧的RCTDevSettings模块的reloadWithReason:方法进行处理。调用ReloadCommand监听的触发器,进行触发reloadCommand命令
RCTReloadCommand发送didReceiveReloadCommand收到下载指令
接着调用RCTBridge中的didReceiveReloadCommand方法,RCTBridge中销毁之前的js缓存,调用setUp进行js环境重置,js资源下载
js资源下载完成后,执行js代码
js资源reload完成后,调用js引擎,展示目标组件
当js文件加载成功后,APP会重新创建一个RCTRootContentView, 并将旧的移除,把新的添加上去。新的RCTRootContentView.reactTag使用规则递增
- (void)bundleFinishedLoading:(RCTBridge *)bridge
{
  RCTAssert(bridge != nil, @"Bridge cannot be nil");
  if (!bridge.valid) {
    return;
  }
  
  [_contentView removeFromSuperview];
  _contentView = [[RCTRootContentView alloc] initWithFrame:self.bounds
                                                    bridge:bridge
                                                  reactTag:self.reactTag
                                            sizeFlexiblity:_sizeFlexibility];
  [self runApplication:bridge];
  
  _contentView.passThroughTouches = _passThroughTouches;
  [self insertSubview:_contentView atIndex:0];
  
  if (_sizeFlexibility == RCTRootViewSizeFlexibilityNone) {
    self.intrinsicContentSize = self.bounds.size;
  }
}
  
- (void)runApplication:(RCTBridge *)bridge
{
  NSString *moduleName = _moduleName ?: @"";
  NSDictionary *appParameters = @{
    @"rootTag" : _contentView.reactTag,
    @"initialProps" : _appProperties ?: @{},
  };
  
  RCTLogInfo(@"Running application %@ (%@)", moduleName, appParameters);
  [bridge enqueueJSCall:@"AppRegistry" method:@"runApplication" args:@[ moduleName, appParameters ] completion:NULL];
}
  

至此,APP中RN页面的热更新主要流程结束。

 
posted @ 2023-05-18 22:45  滴水微澜  阅读(857)  评论(0编辑  收藏  举报