咱們先來看看React Native 動態更新實際效果:java
咱們知道, React Native全部的js文件都打包在一個jsbundle文件中,發佈時也是打包到app裏面,通常是放到asset目錄.
如是猜測是否是能夠從遠程下載jsbundle文件覆蓋asset的jsbundle. 查資料發現asset目錄是隻讀的,該想法行不通.android
在看React Native 啓動入口時,看到經過是setBundleAssetName指定 asset文件的, 查看方法實現:git
public ReactInstanceManager.Builder setBundleAssetName(String bundleAssetName) { return this.setJSBundleFile(bundleAssetName == null?null:"assets://" + bundleAssetName); }
發現調用了setJSBundleFile方法, 並且該方法是public的, 也就是能夠經過這個方法指定的jsbundle文件github
public ReactInstanceManager.Builder setJSBundleFile(String jsBundleFile) { this.mJSBundleFile = jsBundleFile; this.mJSBundleLoader = null; return this; }
能夠設置了jsbundle文件, 那咱們就能夠把jsbundle文件放到sdcard, 通過測試發現, 確實能夠讀取sdcard jsbundle.shell
sdcar的文件開業讀取了,那咱們就能夠把文件放到遠程服務器, 啓動後下載遠程jsbundle文件到sdcard. 大概思路以下:react-native
咱們打好包jsbundle文件放到遠程服務器服務器
啓動React Native, 檢查sdcard是否有jsbundle文件, 若是沒有調用setBundleAssetName加載asset目錄的jsbundle, 同時啓動線程下載遠程jsbundle文件到sdcard目錄.app
待下次啓動時, sdcard是有jsbundle文件的, 加載的就是最新的jsbundle文件.ide
實現代碼以下:
public static final String JS_BUNDLE_REACT_UPDATE_PATH = Environment.getExternalStorageDirectory().toString() + File.separator + "react_native_update/debug.android.bundle"; private void iniReactRootView(boolean isRelease) { ReactInstanceManager.Builder builder = ReactInstanceManager.builder() .setApplication(getApplication()) .setJSMainModuleName("debug.android.bundle") .addPackage(new MainReactPackage()) .addPackage(new Package()) .setInitialLifecycleState(LifecycleState.RESUMED); File file = new File(JS_BUNDLE_LOCAL_PATH); if (isRelease && file != null && file.exists()) { builder.setJSBundleFile(JS_BUNDLE_LOCAL_PATH); Log.i(TAG, "load bundle from local cache"); } else { builder.setBundleAssetName(JS_BUNDLE_LOCAL_FILE); Log.i(TAG, "load bundle from asset"); updateJSBundle(); } mReactRootView = new ReactRootView(this); mReactInstanceManager = builder.build(); mReactRootView.startReactApplication(mReactInstanceManager, "SmartReactApp", null); setContentView(mReactRootView); } // 從遠程服務器下載新的jsbundle文件 private void updateJSBundle() { DownloadManager.Request request = new DownloadManager.Request(Uri.parse(JS_BUNDLE_REMOTE_URL)); request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI); request.setDestinationUri(Uri.parse("file://" + JS_BUNDLE_LOCAL_PATH)); DownloadManager dm = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE); mDownloadId = dm.enqueue(request); Log.i(TAG, "start download remote js bundle file"); }
通過測試發現, 確實能夠實現動態更新, 但要下次啓動才能看到最新的效果, 那有沒有辦法實現當即看到更新效果呢?
經過查看React Native 源碼和查閱資料是能夠實現的, 具體實現以下:
爲了在運行中從新加載bundle文件,查看ReactInstanceManager的源碼,找到以下方法:
private void recreateReactContextInBackground(JavaScriptExecutor jsExecutor, JSBundleLoader jsBundleLoader) { UiThreadUtil.assertOnUiThread(); ReactContextInitParams initParams = new ReactContextInitParams(jsExecutor, jsBundleLoader); if (!mIsContextInitAsyncTaskRunning) { // No background task to create react context is currently running, create and execute one. ReactContextInitAsyncTask initTask = new ReactContextInitAsyncTask(); initTask.execute(initParams); mIsContextInitAsyncTaskRunning = true; } else { // Background task is currently running, queue up most recent init params to recreate context // once task completes. mPendingReactContextInitParams = initParams; } }
雖然這個方法是private的,可是能夠經過反射調用,下面是0.29版本的實現(上面React-Native-Remote-Update項目實現React Native版本舊了,直接拷貝反射參數有問題)
private void onJSBundleLoadedFromServer() { File file = new File(JS_BUNDLE_LOCAL_PATH); if (file == null || !file.exists()) { Log.i(TAG, "js bundle file download error, check URL or network state"); return; } Log.i(TAG, "js bundle file file success, reload js bundle"); Toast.makeText(UpdateReactActivity.this, "download bundle complete", Toast.LENGTH_SHORT).show(); try { Class<?> RIManagerClazz = mReactInstanceManager.getClass(); Field f = RIManagerClazz.getDeclaredField("mJSCConfig"); f.setAccessible(true); JSCConfig jscConfig = (JSCConfig)f.get(mReactInstanceManager); Method method = RIManagerClazz.getDeclaredMethod("recreateReactContextInBackground", com.facebook.react.cxxbridge.JavaScriptExecutor.Factory.class, com.facebook.react.cxxbridge.JSBundleLoader.class); method.setAccessible(true); method.invoke(mReactInstanceManager, new com.facebook.react.cxxbridge.JSCJavaScriptExecutor.Factory(jscConfig.getConfigMap()), com.facebook.react.cxxbridge.JSBundleLoader.createFileLoader(getApplicationContext(), JS_BUNDLE_LOCAL_PATH)); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (NoSuchFieldException e){ e.printStackTrace(); } }
經過監聽下載成功事件, 而後調用onJSBundleLoadedFromServer接口就能夠看到當即更新的效果.
private CompleteReceiver mDownloadCompleteReceiver; private long mDownloadId; private void initDownloadManager() { mDownloadCompleteReceiver = new CompleteReceiver(); registerReceiver(mDownloadCompleteReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); } private class CompleteReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { long completeDownloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); if (completeDownloadId == mDownloadId) { onJSBundleLoadedFromServer(); } } }
嘗試之後果真能夠更新, 當時心情很是好~ 但是......, 後面繼續實現項目時發現, 動態更新後, 本地圖片始終不顯示, 遠程圖片能夠.
接下來查看React Native, jsbundle 源碼和查看資料, 終於尋的一點蛛絲馬跡, 大概的意思以下:
若是bundle在sd卡【 好比bundle在file://sdcard/react_native_update/index.android.bundle 那麼圖片目錄在file://sdcard/react_native_update/drawable-mdpi】
若是你的bundle在assets裏,圖片資源要放到res文件夾裏,例如res/drawable-mdpi
接下來按照該說法進行了實驗, 發現確實能夠. 當界面刷新時,心情格外好! 下面是詳細代碼實現(部分代碼參考React-Native-Remote-Update項目,在這裏直接引用):
package com.react.smart; import android.app.Activity; import android.app.DownloadManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.util.Log; import android.view.KeyEvent; import android.widget.Toast; import com.facebook.react.JSCConfig; import com.facebook.react.LifecycleState; import com.facebook.react.ReactInstanceManager; import com.facebook.react.ReactRootView; import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; import com.facebook.react.shell.MainReactPackage; import com.react.smart.componet.Package; import com.react.smart.utils.FileAssetUtils; import java.io.File; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * Created by sky on 16/7/15. * https://github.com/hubcarl */ /** * Created by sky on 16/9/4. * */ public class UpdateReactActivity extends Activity implements DefaultHardwareBackBtnHandler { private static final String TAG = "UpdateReactActivity"; public static final String JS_BUNDLE_REMOTE_URL = "https://raw.githubusercontent.com/hubcarl/smart-react-native-app/debug/app/src/main/assets/index.android.bundle"; public static final String JS_BUNDLE_LOCAL_FILE = "debug.android.bundle"; public static final String JS_BUNDLE_REACT_UPDATE_PATH = Environment.getExternalStorageDirectory().toString() + File.separator + "react_native_update"; public static final String JS_BUNDLE_LOCAL_PATH = JS_BUNDLE_REACT_UPDATE_PATH + File.separator + JS_BUNDLE_LOCAL_FILE; private ReactInstanceManager mReactInstanceManager; private ReactRootView mReactRootView; private CompleteReceiver mDownloadCompleteReceiver; private long mDownloadId; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); iniReactRootView(true); initDownloadManager(); updateJSBundle(true); } // 若是bundle在sd卡【 好比bundle在file://sdcard/react_native_update/index.android.bundle 那麼圖片目錄在file://sdcard/react_native_update/drawable-mdpi】 // 若是你的bundle在assets裏,圖片資源要放到res文件夾裏,例如res/drawable-mdpi private void iniReactRootView(boolean isRelease) { ReactInstanceManager.Builder builder = ReactInstanceManager.builder() .setApplication(getApplication()) .setJSMainModuleName(JS_BUNDLE_LOCAL_FILE) .addPackage(new MainReactPackage()) .addPackage(new Package()) .setInitialLifecycleState(LifecycleState.RESUMED); File file = new File(JS_BUNDLE_LOCAL_PATH); if (isRelease && file != null && file.exists()) { builder.setJSBundleFile(JS_BUNDLE_LOCAL_PATH); Log.i(TAG, "load bundle from local cache"); } else { builder.setBundleAssetName(JS_BUNDLE_LOCAL_FILE); Log.i(TAG, "load bundle from asset"); } mReactRootView = new ReactRootView(this); mReactInstanceManager = builder.build(); mReactRootView.startReactApplication(mReactInstanceManager, "SmartReactApp", null); setContentView(mReactRootView); } private void updateJSBundle(boolean isRelease) { File file = new File(JS_BUNDLE_LOCAL_PATH); if (isRelease && file != null && file.exists()) { Log.i(TAG, "new bundle exists !"); return; } File rootDir = new File(JS_BUNDLE_REACT_UPDATE_PATH); if (rootDir != null && !rootDir.exists()) { rootDir.mkdir(); } File res = new File(JS_BUNDLE_REACT_UPDATE_PATH + File.separator + "drawable-mdpi"); if (res != null && !res.exists()) { res.mkdir(); } FileAssetUtils.copyAssets(this, "drawable-mdpi", JS_BUNDLE_REACT_UPDATE_PATH); DownloadManager.Request request = new DownloadManager.Request(Uri.parse(JS_BUNDLE_REMOTE_URL)); request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI); request.setDestinationUri(Uri.parse("file://" + JS_BUNDLE_LOCAL_PATH)); DownloadManager dm = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE); mDownloadId = dm.enqueue(request); Log.i(TAG, "start download remote js bundle file"); } private void initDownloadManager() { mDownloadCompleteReceiver = new CompleteReceiver(); registerReceiver(mDownloadCompleteReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); } private class CompleteReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { long completeDownloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); if (completeDownloadId == mDownloadId) { onJSBundleLoadedFromServer(); } } } private void onJSBundleLoadedFromServer() { File file = new File(JS_BUNDLE_LOCAL_PATH); if (file == null || !file.exists()) { Log.i(TAG, "js bundle file download error, check URL or network state"); return; } Log.i(TAG, "js bundle file file success, reload js bundle"); Toast.makeText(UpdateReactActivity.this, "download bundle complete", Toast.LENGTH_SHORT).show(); try { Class<?> RIManagerClazz = mReactInstanceManager.getClass(); Field f = RIManagerClazz.getDeclaredField("mJSCConfig"); f.setAccessible(true); JSCConfig jscConfig = (JSCConfig)f.get(mReactInstanceManager); Method method = RIManagerClazz.getDeclaredMethod("recreateReactContextInBackground", com.facebook.react.cxxbridge.JavaScriptExecutor.Factory.class, com.facebook.react.cxxbridge.JSBundleLoader.class); method.setAccessible(true); method.invoke(mReactInstanceManager, new com.facebook.react.cxxbridge.JSCJavaScriptExecutor.Factory(jscConfig.getConfigMap()), com.facebook.react.cxxbridge.JSBundleLoader.createFileLoader(getApplicationContext(), JS_BUNDLE_LOCAL_PATH)); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (NoSuchFieldException e){ e.printStackTrace(); } } @Override protected void onDestroy() { super.onDestroy(); unregisterReceiver(mDownloadCompleteReceiver); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_MENU && mReactInstanceManager != null) { mReactInstanceManager.showDevOptionsDialog(); return true; } return super.onKeyUp(keyCode, event); } @Override public void onBackPressed() { if (mReactInstanceManager != null) { mReactInstanceManager.onBackPressed(); } else { super.onBackPressed(); } } @Override public void invokeDefaultOnBackPressed() { super.onBackPressed(); } @Override protected void onPause() { super.onPause(); } @Override protected void onResume() { super.onResume(); } }
asset資源文件拷貝到sdcard, 固然實際實現時, 資源文件和jsbundle文件能夠都應該放到遠程服務器.
package com.react.smart.utils; import android.content.Context; import android.content.res.AssetManager; import android.util.Log; import java.io.*; /** * Created by sky on 16/9/19. */ public class FileAssetUtils { public static void copyAssets(Context context, String src, String dist) { AssetManager assetManager = context.getAssets(); String[] files = null; try { files = assetManager.list(src); } catch (IOException e) { Log.e("tag", "Failed to get asset file list.", e); } for(String filename : files) { InputStream in = null; OutputStream out = null; try { in = assetManager.open(src + File.separator + filename); File outFile = new File(dist + File.separator + src, filename); out = new FileOutputStream(outFile); copyFile(in, out); in.close(); in = null; out.flush(); out.close(); out = null; } catch(IOException e) { Log.e("tag", "Failed to copy asset file: " + filename, e); } } } public static void copyFile(InputStream in, OutputStream out) throws IOException { byte[] buffer = new byte[1024]; int read; while((read = in.read(buffer)) != -1){ out.write(buffer, 0, read); } } }
最後附上github項目地址:https://github.com/hubcarl/smart-react-native-app,歡迎follow!