APP中RN頁面熱更新流程-ReactNative原始碼分析

滴水微瀾發表於2023-05-18
平時使用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頁面的熱更新主要流程結束。

 

相關文章