通過對RN熱更新的剖析來感受熱更新思維 一、前言 二、View顯示機制簡介 三、JS Bundle在哪裏?它跟原生代碼如何結合的 熱更新策略 熱更新思維之光 附錄

一、前言

    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文件模塊。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章