1. 程式人生 > >React-Native 熱更新嘗試(Android)

React-Native 熱更新嘗試(Android)

前言:由於蘋果釋出的ios的一些rn的app存在安全問題,主要就是由於一些第三方的熱更新庫導致的,然而訊息一出就鬧得沸沸揚揚的,導致有些人直接認為“學了大半年的rn白學啦~~!!真是哭笑不得。廢話不多說了,馬上進入我們今天的主題吧。“

因為一直在做android開發,所以今天也只是針對於android進行熱更新嘗試(ios我也無能為力哈,看都看不懂,哈哈~~~)。

先看一下效果:

這裡寫圖片描述

怎麼樣?效果還是不錯的吧?其實呢,實現起來還是不是很難的,下面讓我們一點一點的嘗試一下吧(小夥伴跟緊一點哦)。

首先我們來看看當我們執行:

react-native init xxxx

命令的時候,rn會自動幫我們建立一個android專案跟ios專案,然後我們看看rn幫我們建立的android專案長啥樣:

這裡寫圖片描述

我們看到,幫我們建立了一個MainActivity跟一個MainApplication,我們先看一下MainActivity:

package com.businessstore;

import com.facebook.react.ReactActivity;

public class MainActivity extends ReactActivity {

    /**
     * Returns the name of the main component registered from JavaScript.
     * This is used to schedule rendering of the component.
     */
@Override protected String getMainComponentName() { return "BusinessStore"; } }

很簡單,就一行程式碼getMainComponentName,然後返回我們的元件名字,這個名字即為我們在index.android.js中註冊的元件名字:

這裡寫圖片描述

然後我們看看MainApplication長啥樣:

public class MainApplication extends Application implements ReactApplication {

  private
final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { @Override protected boolean getUseDeveloperSupport() { return BuildConfig.DEBUG; } @Override protected List<ReactPackage> getPackages() { return Arrays.<ReactPackage>asList( new MainReactPackage() ); } }; @Override public ReactNativeHost getReactNativeHost() { return mReactNativeHost; } @Override public void onCreate() { super.onCreate(); SoLoader.init(this, /* native exopackage */ false); } }

也是沒有幾行程式碼…..

好啦~那我們的rn頁面是怎麼出來的呢? 不急,我們來一步一步往下看,首先點開MainActivity:

public class MainActivity extends ReactActivity {

    /**
     * Returns the name of the main component registered from JavaScript.
     * This is used to schedule rendering of the component.
     */
    @Override
    protected String getMainComponentName() {
        return "BusinessStore";
    }
}

一個activity要顯示一個頁面的話肯定得setContentView,既然我們的activity沒有,然後就找到它的父類ReactActivity:

public abstract class ReactActivity extends Activity
    implements DefaultHardwareBackBtnHandler, PermissionAwareActivity {

  private final ReactActivityDelegate mDelegate;

  protected ReactActivity() {
    mDelegate = createReactActivityDelegate();
  }

  /**
   * Returns the name of the main component registered from JavaScript.
   * This is used to schedule rendering of the component.
   * e.g. "MoviesApp"
   */
  protected @Nullable String getMainComponentName() {
    return null;
  }

  /**
   * Called at construction time, override if you have a custom delegate implementation.
   */
  protected ReactActivityDelegate createReactActivityDelegate() {
    return new ReactActivityDelegate(this, getMainComponentName());
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mDelegate.onCreate(savedInstanceState);
  }

  @Override
  protected void onPause() {
    super.onPause();
    mDelegate.onPause();
  }

  @Override
  protected void onResume() {
    super.onResume();
    mDelegate.onResume();
  }

  @Override
  protected void onDestroy() {
    super.onDestroy();
    mDelegate.onDestroy();
  }

  @Override
  public void onActivityResult(int requestCode, int resultCode, Intent data) {
    mDelegate.onActivityResult(requestCode, resultCode, data);
  }

  @Override
  public boolean onKeyUp(int keyCode, KeyEvent event) {
    return mDelegate.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event);
  }

  @Override
  public void onBackPressed() {
    if (!mDelegate.onBackPressed()) {
      super.onBackPressed();
    }
  }

  @Override
  public void invokeDefaultOnBackPressed() {
    super.onBackPressed();
  }

  @Override
  public void onNewIntent(Intent intent) {
    if (!mDelegate.onNewIntent(intent)) {
      super.onNewIntent(intent);
    }
  }

  @Override
  public void requestPermissions(
    String[] permissions,
    int requestCode,
    PermissionListener listener) {
    mDelegate.requestPermissions(permissions, requestCode, listener);
  }

  @Override
  public void onRequestPermissionsResult(
    int requestCode,
    String[] permissions,
    int[] grantResults) {
    mDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults);
  }

  protected final ReactNativeHost getReactNativeHost() {
    return mDelegate.getReactNativeHost();
  }

  protected final ReactInstanceManager getReactInstanceManager() {
    return mDelegate.getReactInstanceManager();
  }

  protected final void loadApp(String appKey) {
    mDelegate.loadApp(appKey);
  }
}

程式碼也不是很多,可見,我們看到了activity的很多生命週期方法,然後都是由一個叫mDelegate的類給處理掉了,所以我們繼續往下走看看mDelegate:

// Copyright 2004-present Facebook. All Rights Reserved.

package com.facebook.react;

import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.support.v4.app.FragmentActivity;
import android.view.KeyEvent;
import android.widget.Toast;

import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.Callback;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.devsupport.DoubleTapReloadRecognizer;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.modules.core.PermissionListener;

import javax.annotation.Nullable;

/**
 * Delegate class for {@link ReactActivity} and {@link ReactFragmentActivity}. You can subclass this
 * to provide custom implementations for e.g. {@link #getReactNativeHost()}, if your Application
 * class doesn't implement {@link ReactApplication}.
 */
public class ReactActivityDelegate {

  private final int REQUEST_OVERLAY_PERMISSION_CODE = 1111;
  private static final String REDBOX_PERMISSION_GRANTED_MESSAGE =
    "Overlay permissions have been granted.";
  private static final String REDBOX_PERMISSION_MESSAGE =
    "Overlay permissions needs to be granted in order for react native apps to run in dev mode";

  private final @Nullable Activity mActivity;
  private final @Nullable FragmentActivity mFragmentActivity;
  private final @Nullable String mMainComponentName;

  private @Nullable ReactRootView mReactRootView;
  private @Nullable DoubleTapReloadRecognizer mDoubleTapReloadRecognizer;
  private @Nullable PermissionListener mPermissionListener;
  private @Nullable Callback mPermissionsCallback;

  public ReactActivityDelegate(Activity activity, @Nullable String mainComponentName) {
    mActivity = activity;
    mMainComponentName = mainComponentName;
    mFragmentActivity = null;
  }

  public ReactActivityDelegate(
    FragmentActivity fragmentActivity,
    @Nullable String mainComponentName) {
    mFragmentActivity = fragmentActivity;
    mMainComponentName = mainComponentName;
    mActivity = null;
  }

  protected @Nullable Bundle getLaunchOptions() {
    return null;
  }

  protected ReactRootView createRootView() {
    return new ReactRootView(getContext());
  }

  /**
   * Get the {@link ReactNativeHost} used by this app. By default, assumes
   * {@link Activity#getApplication()} is an instance of {@link ReactApplication} and calls
   * {@link ReactApplication#getReactNativeHost()}. Override this method if your application class
   * does not implement {@code ReactApplication} or you simply have a different mechanism for
   * storing a {@code ReactNativeHost}, e.g. as a static field somewhere.
   */
  protected ReactNativeHost getReactNativeHost() {
    return ((ReactApplication) getPlainActivity().getApplication()).getReactNativeHost();
  }

  public ReactInstanceManager getReactInstanceManager() {
    return getReactNativeHost().getReactInstanceManager();
  }

  protected void onCreate(Bundle savedInstanceState) {
    boolean needsOverlayPermission = false;
    if (getReactNativeHost().getUseDeveloperSupport() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
      // Get permission to show redbox in dev builds.
      if (!Settings.canDrawOverlays(getContext())) {
        needsOverlayPermission = true;
        Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getContext().getPackageName()));
        FLog.w(ReactConstants.TAG, REDBOX_PERMISSION_MESSAGE);
        Toast.makeText(getContext(), REDBOX_PERMISSION_MESSAGE, Toast.LENGTH_LONG).show();
        ((Activity) getContext()).startActivityForResult(serviceIntent, REQUEST_OVERLAY_PERMISSION_CODE);
      }
    }

    if (mMainComponentName != null && !needsOverlayPermission) {
      loadApp(mMainComponentName);
    }
    mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
  }

  protected void loadApp(String appKey) {
    if (mReactRootView != null) {
      throw new IllegalStateException("Cannot loadApp while app is already running.");
    }
    mReactRootView = createRootView();
    mReactRootView.startReactApplication(
      getReactNativeHost().getReactInstanceManager(),
      appKey,
      getLaunchOptions());
    getPlainActivity().setContentView(mReactRootView);
  }

  protected void onPause() {
    if (getReactNativeHost().hasInstance()) {
      getReactNativeHost().getReactInstanceManager().onHostPause(getPlainActivity());
    }
  }

  protected void onResume() {
    if (getReactNativeHost().hasInstance()) {
      getReactNativeHost().getReactInstanceManager().onHostResume(
        getPlainActivity(),
        (DefaultHardwareBackBtnHandler) getPlainActivity());
    }

    if (mPermissionsCallback != null) {
      mPermissionsCallback.invoke();
      mPermissionsCallback = null;
    }
  }

  protected void onDestroy() {
    if (mReactRootView != null) {
      mReactRootView.unmountReactApplication();
      mReactRootView = null;
    }
    if (getReactNativeHost().hasInstance()) {
      getReactNativeHost().getReactInstanceManager().onHostDestroy(getPlainActivity());
    }
  }

  public void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (getReactNativeHost().hasInstance()) {
      getReactNativeHost().getReactInstanceManager()
        .onActivityResult(getPlainActivity(), requestCode, resultCode, data);
    } else {
      // Did we request overlay permissions?
      if (requestCode == REQUEST_OVERLAY_PERMISSION_CODE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        if (Settings.canDrawOverlays(getContext())) {
          if (mMainComponentName != null) {
            loadApp(mMainComponentName);
          }
          Toast.makeText(getContext(), REDBOX_PERMISSION_GRANTED_MESSAGE, Toast.LENGTH_LONG).show();
        }
      }
    }
  }

  public boolean onKeyUp(int keyCode, KeyEvent event) {
    if (getReactNativeHost().hasInstance() && getReactNativeHost().getUseDeveloperSupport()) {
      if (keyCode == KeyEvent.KEYCODE_MENU) {
        getReactNativeHost().getReactInstanceManager().showDevOptionsDialog();
        return true;
      }
      boolean didDoubleTapR = Assertions.assertNotNull(mDoubleTapReloadRecognizer)
        .didDoubleTapR(keyCode, getPlainActivity().getCurrentFocus());
      if (didDoubleTapR) {
        getReactNativeHost().getReactInstanceManager().getDevSupportManager().handleReloadJS();
        return true;
      }
    }
    return false;
  }

  public boolean onBackPressed() {
    if (getReactNativeHost().hasInstance()) {
      getReactNativeHost().getReactInstanceManager().onBackPressed();
      return true;
    }
    return false;
  }

  public boolean onNewIntent(Intent intent) {
    if (getReactNativeHost().hasInstance()) {
      getReactNativeHost().getReactInstanceManager().onNewIntent(intent);
      return true;
    }
    return false;
  }

  @TargetApi(Build.VERSION_CODES.M)
  public void requestPermissions(
    String[] permissions,
    int requestCode,
    PermissionListener listener) {
    mPermissionListener = listener;
    getPlainActivity().requestPermissions(permissions, requestCode);
  }

  public void onRequestPermissionsResult(
    final int requestCode,
    final String[] permissions,
    final int[] grantResults) {
    mPermissionsCallback = new Callback() {
      @Override
      public void invoke(Object... args) {
        if (mPermissionListener != null && mPermissionListener.onRequestPermissionsResult(requestCode, permissions, grantResults)) {
          mPermissionListener = null;
        }
      }
    };
  }

  private Context getContext() {
    if (mActivity != null) {
      return mActivity;
    }
    return Assertions.assertNotNull(mFragmentActivity);
  }

  private Activity getPlainActivity() {
    return ((Activity) getContext());
  }
}

我們終於看到了一些有用的程式碼了,這個類就是處理跟activity生命週期相關的一些方法,包括(給activity新增contentview、監聽使用者回退、按鍵、6.0的一些執行時許可權等等…)我們看到onCreate方法:

protected void onCreate(Bundle savedInstanceState) {
    boolean needsOverlayPermission = false;
    if (getReactNativeHost().getUseDeveloperSupport() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
      // Get permission to show redbox in dev builds.
      if (!Settings.canDrawOverlays(getContext())) {
        needsOverlayPermission = true;
        Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getContext().getPackageName()));
        FLog.w(ReactConstants.TAG, REDBOX_PERMISSION_MESSAGE);
        Toast.makeText(getContext(), REDBOX_PERMISSION_MESSAGE, Toast.LENGTH_LONG).show();
        ((Activity) getContext()).startActivityForResult(serviceIntent, REQUEST_OVERLAY_PERMISSION_CODE);
      }
    }

    if (mMainComponentName != null && !needsOverlayPermission) {
      loadApp(mMainComponentName);
    }
    mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
  }

我們看到這麼一個判斷,這個是做什麼的呢?是為了檢測是不是具有彈出懸浮窗的許可權:

if (getReactNativeHost().getUseDeveloperSupport() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
      // Get permission to show redbox in dev builds.
      if (!Settings.canDrawOverlays(getContext())) {

getReactNativeHost().getUseDeveloperSupport()返回的即為我們在MainApplication中寫的:

@Override
    protected boolean getUseDeveloperSupport() {
      return BuildConfig.DEBUG;
    }

也就是說,當我們執行debug包的時候,會去檢測app是不是具有彈出懸浮窗的許可權,沒有許可權的話就會去請求許可權,懸浮窗即為rn的除錯menu:
這裡寫圖片描述

好啦~!有點偏離我們今天的主題了,我們繼續往下走…往下我們看到會去執行一個叫loadApp的方法:


    if (mMainComponentName != null && !needsOverlayPermission) {
      loadApp(mMainComponentName);
    }

我們點開loadApp:

protected void loadApp(String appKey) {
    if (mReactRootView != null) {
      throw new IllegalStateException("Cannot loadApp while app is already running.");
    }
    mReactRootView = createRootView();
    mReactRootView.startReactApplication(
      getReactNativeHost().getReactInstanceManager(),
      appKey,
      getLaunchOptions());
    getPlainActivity().setContentView(mReactRootView);
  }

好啦~~! 看到這裡我們看到直接給activity設定了一個叫mReactRootView的元件,而這個元件正是rn封裝的元件,我們在js中寫的元件都會被轉換成native元件,然後新增進mReactRootView這個元件中,那麼問題來了,這些rn的元件又是在何時新增進我們的mReactRootView這個元件的呢???我們繼續往下走,看到rootview有一個startReactApplication方法:

public void startReactApplication(
      ReactInstanceManager reactInstanceManager,
      String moduleName,
      @Nullable Bundle launchOptions) {
    UiThreadUtil.assertOnUiThread();

    // TODO(6788889): Use POJO instead of bundle here, apparently we can't just use WritableMap
    // here as it may be deallocated in native after passing via JNI bridge, but we want to reuse
    // it in the case of re-creating the catalyst instance
    Assertions.assertCondition(
        mReactInstanceManager == null,
        "This root view has already been attached to a catalyst instance manager");

    mReactInstanceManager = reactInstanceManager;
    mJSModuleName = moduleName;
    mLaunchOptions = launchOptions;

    if (!mReactInstanceManager.hasStartedCreatingInitialContext()) {
      mReactInstanceManager.createReactContextInBackground();
    }

    // We need to wait for the initial onMeasure, if this view has not yet been measured, we set which
    // will make this view startReactApplication itself to instance manager once onMeasure is called.
    if (mWasMeasured) {
      attachToReactInstanceManager();
    }
  }

我們看到這麼一行程式碼:

if (!mReactInstanceManager.hasStartedCreatingInitialContext()) {
      mReactInstanceManager.createReactContextInBackground();
    }

看名字就知道肯定是載入了某些東西,可是點進去我們發現居然是一個抽象的方法,尷尬了~~!!:

public abstract void createReactContextInBackground();

那麼肯定有它的實現類,我們看到這個mReactInstanceManager是我們在呼叫loadApp這個方法的時候傳進入的:

protected void loadApp(String appKey) {
    if (mReactRootView != null) {
      throw new IllegalStateException("Cannot loadApp while app is already running.");
    }
    mReactRootView = createRootView();
    mReactRootView.startReactApplication(
      getReactNativeHost().getReactInstanceManager(),
      appKey,
      getLaunchOptions());
    getPlainActivity().setContentView(mReactRootView);
  }

而mReactInstanceManager又是呼叫getReactNativeHost().getReactInstanceManager()方法獲取的,getReactNativeHost()這個返回的物件即為我們在MainApplication中建立的host物件:

public class MainApplication extends Application implements ReactApplication {
    private static final String FILE_NAME = "index.android";

    private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {

然後我們順著getReactNativeHost().getReactInstanceManager()一直往下找最後發現mReactInstanceManager的實現類在這裡被建立了:

這裡寫圖片描述

我們趕緊找到XReactInstanceManagerImpl類,然後看一下createReactContextInBackground這個方法:

@Override
  public void createReactContextInBackground() {
    Assertions.assertCondition(
        !mHasStartedCreatingInitialContext,
        "createReactContextInBackground should only be called when creating the react " +
            "application for the first time. When reloading JS, e.g. from a new file, explicitly" +
            "use recreateReactContextInBackground");

    mHasStartedCreatingInitialContext = true;
    recreateReactContextInBackgroundInner();
  }

然後我們繼續往下:

private void recreateReactContextInBackgroundInner() {
   ...
              @Override
              public void onPackagerStatusFetched(final boolean packagerIsRunning) {
                UiThreadUtil.runOnUiThread(
                    new Runnable() {
                      @Override
                      public void run() {
        ...
        recreateReactContextInBackgroundFromBundleLoader();
                        }
                      }
                    });
              }
            });
      }
      return;
    }

我們找到recreateReactContextInBackgroundFromBundleLoader繼續往下:

 private void recreateReactContextInBackgroundFromBundleLoader() {
    recreateReactContextInBackground(
        new JSCJavaScriptExecutor.Factory(mJSCConfig.getConfigMap()),
        mBundleLoader);
  }

然後看到recreateReactContextInBackground方法:

private void recreateReactContextInBackground(
      JavaScriptExecutor.Factory jsExecutorFactory,
      JSBundleLoader jsBundleLoader) {
    UiThreadUtil.assertOnUiThread();

    ReactContextInitParams initParams =
        new ReactContextInitParams(jsExecutorFactory, jsBundleLoader);
    if (mReactContextInitAsyncTask == null) {
      // No background task to create react context is currently running, create and execute one.
      mReactContextInitAsyncTask = new ReactContextInitAsyncTask();
      mReactContextInitAsyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, initParams);
    } else {
      // Background task is currently running, queue up most recent init params to recreate context
      // once task completes.
      mPendingReactContextInitParams = initParams;
    }
  }

recreateReactContextInBackground這就是我們今天需要找的方法,傳遞了兩個引數:一個是執行js程式碼的執行緒池、另外一個是我們bundle檔案的載入器(bundle檔案可以是我們的npm伺服器中的檔案(debug模式),也可以是我們assert目錄中的bundle檔案(釋出版))。

既然如此,那我們熱更新方案是不是可以這樣呢?
1、請求伺服器介面,當介面中返回的版本號跟我們rn中儲存的版本號不一致的時候,那麼這個時候就需要更新版本了。
2、伺服器介面返回一個jsbundle檔案的下載地址,然後我們app中拿到地址下載到本地,替換掉當前版本的jsbundle檔案。
3、重新執行一下recreateReactContextInBackground方法,讓app重新載入新的jsbundle檔案。

好啦~! 有了思路以後,我們就可以寫我們的程式碼了:

首先,我們模擬一個後臺介面:

{
    "url": "/business/version",
    "method": "post",
    "response": {
      "code": "0",
      "message": "請求成功",
      "body": {
        "versionName": "2.0.0",
        "description":"添加了熱更新功能",
        "url":"http://www.baidu.com"
      }
    }
  },

然後在我們的rn中我們對應定義了一個常量叫version:

這裡寫圖片描述

可以看到我們rn中定義的為1.0.0,所以待會我去請求介面,當介面返回的2.0.0不等於1.0.0的時候,我就去下載更新bundle檔案了,於是在我們rn主頁面的時候,我們就傳送一個請求,然後做判斷:

componentDidMount() {
        this._versionCheck();
    }

    _versionCheck() {
        this.versionRequest = new HomeMenuRequest(null, 'POST');
        this.versionRequest.start((version)=> {
            version = version.body;
            if (version && version.versionName != AppConstant.version) {
                if (Platform.OS == 'android') {
                    Alert.alert(
                        '發現新版本,是否升級?',
                        `版本號: ${version.versionName}\n版本描述: ${version.description}`,
                        [
                            {
                                text: '是',
                                onPress: () => {
                                    this.setState({
                                        currProgress: Math.random() * 80,
                                        modalVisible: true
                                    });

                                    NativeModules.UpdateAndroid.doUpdate('index.android.bundle_2.0', (progress)=> {
                                        let pro = Number.parseFloat('' + progress);
                                        if (pro >= 100) {
                                            this.setState({
                                                modalVisible: false,
                                                currProgress: 100
                                            });
                                        } else {
                                            this.setState({
                                                currProgress: pro
                                            });
                                        }
                                    });
                                }
                            },
                            {
                                text: '否'
                            }
                        ]
                    )
                }
            }
        }, (erroStr)=> {

        });
    }
}

會彈出一個對話方塊:
這裡寫圖片描述

當我們點選是的時候:

 NativeModules.UpdateAndroid.doUpdate('index.android.bundle_2.0', (progress)=> {
                                        let pro = Number.parseFloat('' + progress);
                                        if (pro >= 100) {
                                            this.setState({
                                                modalVisible: false,
                                                currProgress: 100
                                            });
                                        } else {
                                            this.setState({
                                                currProgress: pro
                                            });
                                        }
                                    });

我們執行了native中的doUpdate並傳遞了兩個引數,一個是下載地址,一個是當native完成熱更新後的回撥:

NativeModules.UpdateAndroid.doUpdate()

這裡宣告一下,因為我這邊用的伺服器是mock的伺服器,所以沒法放一個檔案到伺服器上,我就直接把需要下載的bundle_2.0放在了跟1.0同級的一個目錄中了,然後我們去copy 2.0到記憶體卡(模擬從網路上獲取),替換掉1.0的版本。
這裡寫圖片描述

再次宣告,我們釋出apk的時候需要把本地的js檔案打成bundle,然後丟到assets目錄中,所以最初的版本應該是index.android.bundle_1.0,這裡出現了一個index.android.bundle_2.0是為了模擬從伺服器下載,我就直接丟在了assert目錄了(正常這個檔案是在我們的遠端伺服器中的)。

如果還不知道怎麼釋出apk的童鞋可以去看我前面的一篇部落格:

接下來就看看我們native的程式碼如何實現了….

我就直接拿釋出版的例子來說了,我們首先看看我們的MainApplication中該怎麼寫:

public class MainApplication extends Application implements ReactApplication {
    private static final String FILE_NAME = "index.android";

    private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
        @Override
        protected boolean getUseDeveloperSupport() {
            //這裡返回false的話即為釋出版,否則為測試版
            //釋出版的話,app預設就會去assert目錄中找bundle檔案,
            // 如果為測試版的話,就回去npm伺服器上獲取bundle檔案
            return false;
        }

        @Override
        protected List<ReactPackage> getPackages() {
            return Arrays.<ReactPackage>asList(
                    new MainReactPackage(),
                    new VersionAndroidPackage(),
                    new UpdateAndroidPackage()
            );
        }
        @Nullable
        @Override
        protected String getJSBundleFile() {
            File file = new File(getExternalCacheDir(), FILE_NAME);
            if (file != null && file.length() > 0) {
                return file.getAbsolutePath();
            }
            return super.getJSBundleFile();
        }
    };

    @Override
    public ReactNativeHost getReactNativeHost() {
        return mReactNativeHost;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        copyBundle();
        SoLoader.init(this, /* native exopackage */ false);
    }

    private void copyBundle(){
        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            return;
        }
        File file = new File(getExternalCacheDir(), FILE_NAME);
        if (file != null && file.length() > 0) {
            return;
        }
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;
        try {
            bis = new BufferedInputStream(getAssets().open("index.android.bundle_1.0"));
            bos = new BufferedOutputStream(new FileOutputStream(file));
            int len = -1;
            byte[] buffer = new byte[512];
            while ((len = bis.read(buffer)) != -1) {
                bos.write(buffer, 0, len);
                bos.flush();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (bis != null) {
                    bis.close();
                }
                if (bos != null) {
                    bos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

可以看到我們多重寫了一個方法,然後還寫了一段copy bundle檔案到記憶體卡的程式碼:

 @Nullable
        @Override
        protected String getJSBundleFile() {
            File file = new File(getExternalCacheDir(), FILE_NAME);
            if (file != null && file.length() > 0) {
                return file.getAbsolutePath();
            }
            return super.getJSBundleFile();
        }
    };

因為rn預設是去assert目錄中載入bundle檔案的,當指定了bundle檔案的地址後,rn會去載入我們指定的目錄。所以當我們第一次執行app的時候,我們首先把assert中的bundle檔案拷貝到了記憶體卡,然後讓rn去記憶體卡中加在bundle檔案。

好啦~~!!此時的rn已經知道去記憶體卡中載入bundle檔案了,我們要做的就是:
1、根據rn 中傳遞的地址去下載最新的bundle檔案。
2、替換掉記憶體卡中的bundle檔案。
3、呼叫createReactContextInBackground方法重新載入bundle檔案。

至於rn怎麼去跟native互動,我這裡簡單的說一下哈:
首先我們需要建一個叫UpdateAndroid去繼承ReactContextBaseJavaModule,然後註釋宣告為react的module:

@ReactModule(name = "UpdateAndroid")
public class UpdateAndroid extends ReactContextBaseJavaModule {

然後重寫裡面的一個叫getName的方法給這個module取一個名字:

  @Override
    public String getName() {
        return "UpdateAndroid";
    }

最後宣告一個類方法,讓rn調取:

@ReactMethod
    public void doUpdate(String url, Callback callback) {
        if (task == null) {
            task = new UpdateTask(callback);
            task.execute("index.android.bundle_2.0");
        }
    }

全部程式碼:

@ReactModule(name = "UpdateAndroid")
public class UpdateAndroid extends ReactContextBaseJavaModule {
    private UpdateTask task;

    public UpdateAndroid(ReactApplicationContext reactContext) {
        super(reactContext);
    }

    @ReactMethod
    public void doUpdate(String url, Callback callback) {
        if (task == null) {
            task = new UpdateTask(callback);
            task.execute("index.android.bundle_2.0");
        }
    }

    @Override
    public String getName() {
        return "UpdateAndroid";
    }

    private class UpdateTask extends AsyncTask<String, Float, File> {
        private Callback callback;
        private static final String FILE_NAME = "index.android";

        private UpdateTask(Callback callback) {
            this.callback = callback;
        }

        @Override
        protected File doInBackground(String... params) {
            return downloadBundle(params[0]);
        }

        @Override
        protected void onProgressUpdate(Float... values) {
//            if (callback != null && values != null && values.length > 0){
//                callback.invoke(values[0]);
//                Log.e("TAG", "progress-->" + values[0]);
//            }
        }

        @Override
        protected void onPostExecute(File file) {
            if (callback != null) callback.invoke(100f);
            //重寫初始化rn元件
            onJSBundleLoadedFromServer(file);
        }

        private void onJSBundleLoadedFromServer(File file) {
            if (file == null || !file.exists()) {
                Log.i(TAG, "download error, check URL or network state");
                return;
            }

            Log.i(TAG, "download success, reload js bundle");

            Toast.makeText(getCurrentActivity(), "Downloading complete", Toast.LENGTH_SHORT).show();
            try {
                ReactApplication application = (ReactApplication) getCurrentActivity().getApplication();
                Class<?> RIManagerClazz = application.getReactNativeHost().getReactInstanceManager().getClass();
                Method method = RIManagerClazz.getDeclaredMethod("recreateReactContextInBackground",
                        JavaScriptExecutor.Factory.class, JSBundleLoader.class);
                method.setAccessible(true);
                method.invoke(application.getReactNativeHost().getReactInstanceManager(),
                        new JSCJavaScriptExecutor.Factory(JSCConfig.EMPTY.getConfigMap()),
                        JSBundleLoader.createFileLoader(file.getAbsolutePath()));
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            }
        }

        /**
         * 模擬bundle下載連結url
         *
         * @param url
         */
        private File downloadBundle(String url) {
            if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
                return null;
            }
            //刪除以前的檔案
            File file = new File(getReactApplicationContext().getExternalCacheDir(), FILE_NAME);
            if (file != null && file.length() > 0) {
                file.delete();
            }
            BufferedInputStream bis = null;
            BufferedOutputStream bos = null;
            try {
                //模擬網路下載過程,我這直接放在了assert目錄了
                long size = getReactApplicationContext().getAssets().open(url).available();
                bis = new BufferedInputStream(getReactApplicationContext().getAssets().open(url));
                bos = new BufferedOutputStream(new FileOutputStream(file));
                int len = -1;
                long total = 0;
                byte[] buffer = new byte[100];
                while ((len = bis.read(buffer)) != -1) {
                    total += len;
                    bos.write(buffer, 0, len);
                    bos.flush();
                    float progress = total * 1.0f / size;
                    publishProgress(progress);
                }
                return file;
            } catch (Exception e) {
                e.printStackTrace();
            } finally