一、前言
RN入門調研一文中提到:RN熱更新的核心技術是構建JS與原生之間的解釋器,基本原理是替換JS Bundle。
JS 解釋器(javaScriptCore)是一個翻譯系統,它非常複雜,辛虧前人已經做好,我們只要知道通過JS解釋器,便可以用JS語言和原生系統進行交流——指揮原生做事情,從原生獲取信息。
如何替換JS Bundle則是實現RN熱更新需要掌握的,幸運的是替換JS Bundle和更換磁帶、CD差不多, 換CD誰不會啊,哈哈~還真不一定⊙︿⊙。所以我們本文主要內容就是教大家如何換CD( ̄▽ ̄):
1、認識播放器,瞭解它如何使用;
2、介紹CD存放在哪裏,它跟播放器都有哪些接觸點;
3、什麼時候把舊CD取出來,替換爲新CD(CD更換策略)。
嘿嘿~算了,大家還是百度如何換磁帶、CD吧,這裏按照更換CD的流程來介紹如何更換JS Bundle:
1、瞭解View顯示機制;
2、瞭解JS Bundle存放在哪裏,它跟原生代碼有哪些結合點;
3、以什麼樣的策略去更新JS Bundle。
二、View顯示機制簡介
假如你女朋友突然變得又聾又啞,你必須畫一幅有趣的畫給她,她才能好起來,那麼主要分幾步走呢?
1、你要準備好繪畫的內容,比如畫面中建築的高矮,天空的顏色,人物的表情等;
2、你需要有繪畫能力,能把每一個元素按照設想的大小繪製在正確的位置上;
3、你需要把這幅畫展示在女朋友面前,也許還要在她要求下修改一二。
View顯示機制也差不多這樣:
具體Android中如下:
簡單解釋:
內容:描述顯示和用戶交互所有數據。比如顯示的圖片是什麼、顯示的文字是什麼、文字的顏色是什麼等,滑動圖片怎麼切換到一張圖等。
內容提供者:也稱爲內容管理者,內容管理者跟內容的區別在於他有主動性,他既可以主動去獲取內容,也會在合適的時機將內容分發給訂閱的機構、人、模塊等。
View:相當於繪製系統+觸摸反饋系統。它可以將內容數據轉換成一張張可以供展示的頁面,可以將用戶觸摸事件傳遞給內容管理者,從而改變內容數據,當然內容變化後一般繪製-展示也會隨之變化。
Activity: Activity是各個管理、控制模塊在基層的交匯之處,也是對外服務點,不管是顯示、語音、指紋控制、生命週期等,它都要插一腳,有點類似我們基層的行政服務中心,其中就包括展示與交互系統。比如一個Acitity有幾個不同的頁面,確定讓哪一個展示給用戶呢?點屏幕事件怎麼通過觸控系統傳遞到View繪製系統等。
對於原生來說,RN模塊是內容用JS語言表達的一個View:(一種比較特殊的自定義View)
從RN顯示機制一圖中可以看到,JS Bundle 就是內容管理者管理的內容數據,RN頁面之間跳轉,不過是JS Bundle不同部分的內容展示。知道了JS Bundle的作用後,接下來便是查找:
三、JS Bundle在哪裏?它跟原生代碼如何結合的
類比播放器,CD作爲內容存儲設備,存在播放器可接觸到位置,然後有一個磁頭可以將CD內容讀出來,經過播放器轉換爲聲音放出來。所以JS Bundle也是處於App某一個地方,而且JS 解釋器肯定也有一個類似於磁頭一樣的東西去讀寫JS Bundle的內容,供顯示機制來顯示,下面我們就從代碼級別看看:
按照RN中文網去創建一個最簡單demo,用AS打開目錄下的Android部分,經過一番努力就能發現,在如下箭頭位置獲取並加載JS Bundle:
其中ReactNativeHost相當於內容管理者,getJSBundleFile就是內容管理者去取內容,而builder.setJSBundleFile(jsBundleFile)則代表啓用jsBundleFile中的內容,好比說播放器現在播放這個CD。
再回頭看下這個一番努力是怎麼努力的?上文有提到當RN代碼在移動設備上運行時候,原生代碼相當於殼,入口、交互一切的起點都在原生髮起。我們就從Android進程創建後首先加載的頁面(清單中定義的)MainActivity入手,跟蹤它生命週期進行分析起來:
public class MainActivity extends ReactActivity {
/**
* 在RN代碼中註冊的模塊名
*/
@Override
protected String getMainComponentName() {
return "AwesomeProject";
}
/**
* 創建一個ReactActivity的代理
*/
@Override
protected ReactActivityDelegate createReactActivityDelegate() {
return new ReactActivityDelegate(this, getMainComponentName()) {
@Override
protected ReactRootView createRootView() {
return new RNGestureHandlerEnabledRootView(MainActivity.this);
}
};
}
}
可以看到MainActivity中,我們做了三件事:
A:填寫我們的RN模塊名;
一個ReactActivity相當於一個磁帶盒/CD盒,每一個ReactActivity啓動的時候就會根據getMainComponentName返回的模塊名去加載對應的CD(JS 文件集合)。
讀者問題1):模塊名是在哪裏註冊?怎麼得到這些模塊名?
讀者問題2):如何根據模塊名去尋找對應的JS文件集合呢?
B:創建一個ReactActivity的代理ReactActivityDelegate
所謂代理是什麼呢,就是遇到不想做或者不方便做的事,我就把權限發給某一個人讓他幫忙做,那麼他就是我的代理。
讀者問題3):爲什麼要創建這個代理?
C:最關鍵的是繼承自ReactActivity
ReactActivity是RN應用的基本類,它封裝了所有RN與原生相關的東西。
從ReactActivity的源碼可以看出來,它主體功能都是用一個代理ReactActivityDelegate來實現:
插一句,這裏這個代理就是剛纔在MainActivity中創建那個代理對象,目的就是讓碼農們方便自定義的一些東西,這也是代理的精髓。
繼續我們的事件流,Activity的onCreate事件最終執行的就是ReactActivityDelegate的onCreate事件,這裏有一個重要方法loadApp()。它的參數是前面提到的模塊名,稍微跟蹤下就會發現是在MainActivity中getMainComponentName方法中自定義那個。
繼續跟蹤loadApp到startReactApplication,我們追蹤進它第一個參數:
public ReactInstanceManager getReactInstanceManager() {
if (mReactInstanceManager == null) {
ReactMarker.logMarker(ReactMarkerConstants.GET_REACT_INSTANCE_MANAGER_START);
// 創建RN Instance
mReactInstanceManager = createReactInstanceManager();
ReactMarker.logMarker(ReactMarkerConstants.GET_REACT_INSTANCE_MANAGER_END);
}
return mReactInstanceManager;
}
當原生首次加載RN時候,mReactInstanceManager等於null,所以我們繼續追蹤到createReactInstanceManager,這便是我們最上面指出的那個獲取並更新js bundle的地方。
protected ReactInstanceManager createReactInstanceManager() {
ReactMarker.logMarker(ReactMarkerConstants.BUILD_REACT_INSTANCE_MANAGER_START);
ReactInstanceManagerBuilder builder = ReactInstanceManager.builder()
.setApplication(mApplication)
.setJSMainModulePath(getJSMainModuleName())
.setUseDeveloperSupport(getUseDeveloperSupport())
.setRedBoxHandler(getRedBoxHandler())
.setJavaScriptExecutorFactory(getJavaScriptExecutorFactory())
.setUIImplementationProvider(getUIImplementationProvider())
.setJSIModulesPackage(getJSIModulePackage())
.setInitialLifecycleState(LifecycleState.BEFORE_CREATE);
for (ReactPackage reactPackage : getPackages()) {
builder.addPackage(reactPackage);
}
// 獲取js bundle
String jsBundleFile = getJSBundleFile();
if (jsBundleFile != null) {
// 設置js bundle
builder.setJSBundleFile(jsBundleFile);
} else {
builder.setBundleAssetName(Assertions.assertNotNull(getBundleAssetName()));
}
ReactInstanceManager reactInstanceManager = builder.build();
ReactMarker.logMarker(ReactMarkerConstants.BUILD_REACT_INSTANCE_MANAGER_END);
return reactInstanceManager;
}
跟蹤進如getJSBundleFile方法內發現它返回null,是因爲官方給的demo並沒有接入熱更新,所以它取的是本地Assert目錄下本地打包的 Bundle文件。而做熱更新的關鍵就是去重寫這個getJSBundleFile,在這個方法裏面可以從服務器拉取新的js bundle文件,設置到mReactInstanceManager中, 完成熱更新。
怎麼重寫這個getJSBundleFile方法呢,我們看到這個方法位於ReactNativeHost類中,再回到上層找到這個ReactNativeHost對象來源於:
protected ReactNativeHost getReactNativeHost() {
return ((ReactApplication) getPlainActivity().getApplication()).getReactNativeHost();
}
所以我們在:
重寫,至於我們怎麼從網絡獲取新的bundle 則是另外一個問題,如上圖中封裝在UpdateContext.getBundleUrl(MainApplication.this)方法中。
現在我們知道了熱更新就是替換js bundle,也找到了替換的地方,那最後的問題就是熱更新策略,就是指什麼時候更新你的CD,怎麼驗證你更新的CD對不對等。
熱更新策略
熱更新策略就是如何按照需求正確地更新JS Bundle,包括什麼時候更新js bundle,更新過程中校驗,更新失敗處理等。最簡單的熱更新策略如下:
檢查更新方案很多,比如最基本方案是給每一個js bundle一個版本號,每次加載的時候先請求服務器JS Bundle最新版本號,如果版本號比本地使用的bundle版本號大,就啓動下載程序。
注意:上圖可以稱之爲全量熱更新,如果有新的版本的時候,舊的js bundle完全捨棄,替換爲新的Js bundle。如果當前js bundle已經非常大,但是你新的js 文件就修改了一個字段,這時候還要這樣全量更新,會大大浪費用戶流量,也會增加不少更新時間,所以現在已經演變出來很多增量更新的方案,可以參考文章:增量更新RN
熱更新思維之光
本文介紹了RN熱原理與具體實現,指出了熱更新是因爲客戶端有了內容管理者和內容解析者,使客戶端既可以主動去獲取數據與命令,又可以理解這些數據與指令,這些數據和指令便是可熱更新的部分。思路發散下,如果可以熱更新的不止業務數據與指令,而是人、部門、團體、產品配件等等,那該如何構建熱更新機制使整個系統平穩、安全、快速的進化呢?
借鑑在技術領域的熱更新,這裏總結了三個關鍵點:
1、對可更新的部分構建精確、穩定、 高效的解釋系統;
2、可更新的部分有明確和標準的對外接口;
3、對可更新部分有完整的更新策略,成功判斷,失敗處理等。
從這三點來看,報紙行業-->新聞中心+手機客戶端 算是一種熱更新轉變,紙質信件-->電子郵件 是一種熱更新,廚師培訓->標準化飲食製作是一種熱更新,構建公司人才策略,構建公司整體架構也可以是一種熱更新。
在這個節奏越來越快的社會,商品需要快速換代,服務需要快速反饋,個人知識需要快速增長、公司架構需要快速進化 ,只要你能想到需要速度的地方,都可以將熱更新之光照耀過去。
附錄
關於問題:RN模塊名是在哪裏註冊?怎麼得到這些模塊名?
答: 在原生中有一個接口AppRegistry,這個接口在RN代碼中實現,當我們在原生中調用AppRegistry中的方法的時候,RN代碼中對應的實現變開始執行。AppRegistry 是RN代碼執行入口點,就是RN代碼首先都是從這個實現中的方法開始運行的。
當首次從原生入口到RN代碼時,會運行AppRegistry.registerComponent,(在RN模塊中修改,有默認值)這個方法會把很多模塊註冊到RN框架中,參數就是模塊名和入口文件名,相當於把CD放到某一個CD盒內,並把播放頭調整到特定位置。然後在每次原生調用RN時候,都會執行AppRegistry. runApplication,這個方法傳遞的主要參數是getMainComponentName返回的模塊名,這就是前面提到的根據模塊名加載對應的JS文件模塊。