React Native bundle预加载解决方案

当在原生的app里面加载rn模块的时候,第一次加载的时候会发现加载的时候过长,白屏出现时间过长,这个因为生成rootview的时间过长导致,通过分析react native启动流程可以看出是因为createRootView和startReactApplication消耗时间较长,简单来说

那么我们可以提前加载bundle文件,比如可以在application生成的时候提前加载。

private static final Map<String, ReactRootView> CACHE = new WeakHashMap<>();

    /**
     * 初始化ReactRootView,并添加到缓存
     *
     * @param context   上下文对象
     * @param componentName 加载的组件名
     */
    public static void preLoad(Context context, String componentName) {

        if (CACHE.get(componentName) != null) {
            return;
        }
        ReactRootView rootView = new ReactRootView(new MutableContextWrapper(context));
        rootView.startReactApplication(
                ((MainApplication) context.getApplicationContext()).getReactNativeHost().getReactInstanceManager(),
                componentName,
                null);

        CACHE.put(componentName, rootView);
    }

页面加载在reactnative中是交给ReactActivityDelegate去实现的,

protected void onCreate(Bundle savedInstanceState) {
    if (mMainComponentName != null) {
      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);
  }

我们可以在重新自定义一个ReactActivityDelegate,重写onCreate方法获取我们提前预先加载缓存好的rootview

protected void onCreate(Bundle savedInstanceState) {
        if (mMainComponentName != null) {
            loadApp(mMainComponentName);
        }
        mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
    }

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

在loadApp方法里,首先在缓存里获取rootview,如果没有的话就生成一个rootview并把它存到缓存里,最后在我们Activity初始化的时候生成我们自定义的delegate

protected RNPreLoadDelegate createPreLoadReactDelegate() {
        return new RNPreLoadDelegate(this, getMainComponentName());
    }

这里贴一下
这里写图片描述

BUTTON是rn传统的启动方式,跳转到MainActivity,BUTTON1是预加载的启动方式,跳转到Main1Activity,可以看出BUTTON跳转的时候有一个白屏

但是上面代码是有问题的,问题在于preLoad这个方法context这个入参,context在这里考虑两种情况,
1.context是application,我们在js的代码里加一个modal(对应android的对话框)
这里写图片描述
可以看出这个是不支持展示对话框的
这里写图片描述

2.context是activity,我们在activity里面调用预加载preload方法,传入activity作为上下文对象,这样对话框可以正常展示
这里写图片描述
但是我们传入的是activity的context,并且缓存的这个rootview,那么就产生了内存泄漏,这也是有问题的

那我们可以考虑在缓存的时候用application的context来保证内存不泄露,在使用的时候替换成activity的context来展示modal,可以使用MutableContextWrapper,可以看一下RNRootViewPreLoader的具体代码

private static final Map<String, ReactRootView> CACHE = new WeakHashMap<>();

    /**
     * 初始化ReactRootView,并添加到缓存
     *
     * @param context   上下文对象
     * @param componentName 加载的组件名
     */
    public static void preLoad(Context context, String componentName) {

        if (CACHE.get(componentName) != null) {
            return;
        }
        ReactRootView rootView = new ReactRootView(new MutableContextWrapper(context.getApplicationContext()));
        rootView.startReactApplication(
                ((MainApplication) context.getApplicationContext()).getReactNativeHost().getReactInstanceManager(),
                componentName,
                null);

        CACHE.put(componentName, rootView);
    }

    /**
     * 获取ReactRootView
     *
     * @param componentName 加载的组件名
     * @return ReactRootView
     */
    public static ReactRootView getReactRootView(Activity activity, String componentName) {
        ReactRootView rootView = CACHE.get(componentName);
        if (rootView.getContext() instanceof MutableContextWrapper) {
            ((MutableContextWrapper) rootView.getContext()).setBaseContext(
                    activity
            );
        }
        return rootView;
    }


    /**
     * 从当前界面移除 ReactRootView
     *
     * @param componentName 加载的组件名
     */
    public static void detachView(String componentName) {
        try {
            ReactRootView rootView = CACHE.get(componentName);
            if (rootView == null)
                return;
            ViewGroup parent = (ViewGroup) rootView.getParent();
            if (parent != null) {
                parent.removeView(rootView);
            }
            if (rootView.getContext() instanceof MutableContextWrapper) {
                ((MutableContextWrapper) rootView.getContext()).setBaseContext(
                        rootView.getContext().getApplicationContext()
                );
            }
        } catch (Throwable e) {
            Log.e("RNRootViewPreLoader", e.getMessage());
        }
    }

代码里面在预加载的时候传入application生成rootview,真正需要界面加载的时候,替换成activity,在activity finish的时候,将rootview的context再次替换为application

上述的context问题解决了,但是设想一下比如我们app端需要传递token,cookies到rn以便rn调用网络请求,代码如下

public class TokenModule extends ReactContextBaseJavaModule {

    public static String NET_USER_ID = "NET_USER1";

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

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

    @Override
    public Map<String, Object> getConstants() {

        final Map<String, Object> constants = new HashMap<>();

        constants.put("NET_USER_ID", NET_USER_ID);

        return constants;
    }
}

我们在点击button3的时候将TokenModule.NET_USER_ID 设置为 “NET_USER2”;
这里写图片描述
发现预加载的值不会变,而button对应的界面由于可以在activity的层面重新设置它的delegate,所以不会有这样的问题,代码如下

public class MainActivity extends ReactActivity {

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

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        mReactNativeHost  = new ReactNativeHost(getApplication()) {
            @Override
            public boolean getUseDeveloperSupport() {
                return BuildConfig.DEBUG;
            }

            @Override
            protected List<ReactPackage> getPackages() {
                return Arrays.asList(
                        new MainReactPackage(),
                        new AccountPackage()
                );
            }
            @Override
            protected String getJSMainModuleName() {
                return "index";
            }
        };
        super.onCreate(savedInstanceState);
    }


    @Override
    protected ReactActivityDelegate createReactActivityDelegate() {

        return new ReactActivityDelegate(this, getMainComponentName()) {
            @Override
            protected ReactNativeHost getReactNativeHost() {
                return mReactNativeHost;
            }

        };
    }

}

而如果我们在Main1Activity按照这种方式是行不通的,因为ReactContext早已经生成,在这种情况下,就需要重新生成rootView的ReactContext,代码如下

public static void refreshRootView(String componentName) {
        ReactRootView rootView = CACHE.get(componentName);
        if (rootView == null)
            return;
        // 如果不为空,重新生成ReactContext
        rootView.getReactInstanceManager().recreateReactContextInBackground();
    }

可以看出运行结果,数据确实变了
这里写图片描述

综上,就实现了react native bundle文件的预加载

主要代码如下:

RNPreLoadActivity.java

package com.awesomeproject.preloadreact;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.KeyEvent;

import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.modules.core.PermissionAwareActivity;
import com.facebook.react.modules.core.PermissionListener;

import javax.annotation.Nullable;


public abstract class RNPreLoadActivity extends Activity implements DefaultHardwareBackBtnHandler, PermissionAwareActivity {

    private RNPreLoadDelegate mPreLoadReactDelegate;

    protected RNPreLoadActivity() {
        mPreLoadReactDelegate = createPreLoadReactDelegate();
    }

    protected RNPreLoadDelegate createPreLoadReactDelegate() {
        return new RNPreLoadDelegate(this, getMainComponentName());
    }

    /**
     * 子类重写,返回RN对应的界面组件名称
     *
     * @return String 需要加载的组建名
     */
    protected @Nullable
    String getMainComponentName() {
        return null;
    }

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

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

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

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mPreLoadReactDelegate.onDestroy();
        RNRootViewPreLoader.detachView(getMainComponentName());
    }

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

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

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

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

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

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

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

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

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

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

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

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


}

RNPreLoadDelegate.java

package com.awesomeproject.preloadreact;

import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.view.KeyEvent;

import com.facebook.infer.annotation.Assertions;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactRootView;
import com.facebook.react.bridge.Callback;
import com.facebook.react.devsupport.DoubleTapReloadRecognizer;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.modules.core.PermissionListener;

import javax.annotation.Nullable;

public class RNPreLoadDelegate {

    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 RNPreLoadDelegate(Activity activity, @Nullable String mainComponentName) {
        mActivity = activity;
        mMainComponentName = mainComponentName;
        mFragmentActivity = null;
    }

    public RNPreLoadDelegate(
            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) {
        if (mMainComponentName != null) {
            loadApp(mMainComponentName);
        }
        mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
    }

    protected void loadApp(String appKey) {
        if (mReactRootView != null) {
            throw new IllegalStateException("Cannot loadApp while app is already running.");
        }
        mReactRootView = RNRootViewPreLoader.getReactRootView(getPlainActivity(), mMainComponentName);

        if (mReactRootView == null) {
            mReactRootView = RNRootViewPreLoader.startReactApplication(
                    getPlainActivity(), 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);
        }
    }

    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (getReactNativeHost().hasInstance()
                && getReactNativeHost().getUseDeveloperSupport()
                && keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) {
            event.startTracking();
            return true;
        }
        return false;
    }

    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 onKeyLongPress(int keyCode, KeyEvent event) {
        if (getReactNativeHost().hasInstance()
                && getReactNativeHost().getUseDeveloperSupport()
                && keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) {
            getReactNativeHost().getReactInstanceManager().showDevOptionsDialog();
            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());
    }
}

RNRootViewPreLoader.java

package com.awesomeproject.preloadreact;

import android.app.Activity;
import android.content.Context;
import android.content.MutableContextWrapper;
import android.os.Bundle;
import android.util.Log;
import android.view.ViewGroup;

import com.awesomeproject.MainApplication;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactRootView;

import java.util.Map;
import java.util.WeakHashMap;

public class RNRootViewPreLoader {
    private static final Map<String, ReactRootView> CACHE = new WeakHashMap<>();

    /**
     * 初始化ReactRootView,并添加到缓存
     *
     * @param context   上下文对象
     * @param componentName 加载的组件名
     */
    public static void preLoad(Context context, String componentName) {

        if (CACHE.get(componentName) != null) {
            return;
        }
        ReactRootView rootView = new ReactRootView(new MutableContextWrapper(context.getApplicationContext()));
        rootView.startReactApplication(
                ((MainApplication) context.getApplicationContext()).getReactNativeHost().getReactInstanceManager(),
                componentName,
                null);

        CACHE.put(componentName, rootView);
    }

    /**
     * 获取ReactRootView
     *
     * @param componentName 加载的组件名
     * @return ReactRootView
     */
    public static ReactRootView getReactRootView(Activity activity, String componentName) {
        ReactRootView rootView = CACHE.get(componentName);
        if (rootView.getContext() instanceof MutableContextWrapper) {
            ((MutableContextWrapper) rootView.getContext()).setBaseContext(
                    activity
            );
        }
        return rootView;
    }



    /**
     * 从当前界面移除 ReactRootView
     *
     * @param componentName 加载的组件名
     */
    public static void detachView(String componentName) {
        try {
            ReactRootView rootView = CACHE.get(componentName);
            if (rootView == null)
                return;
            ViewGroup parent = (ViewGroup) rootView.getParent();
            if (parent != null) {
                parent.removeView(rootView);
            }
            if (rootView.getContext() instanceof MutableContextWrapper) {
                ((MutableContextWrapper) rootView.getContext()).setBaseContext(
                        rootView.getContext().getApplicationContext()
                );
            }
        } catch (Throwable e) {
            Log.e("RNRootViewPreLoader", e.getMessage());
        }
    }


    public static ReactRootView startReactApplication(Activity plainActivity, ReactInstanceManager reactInstanceManager, String componentName, Bundle launchOptions) {
        ReactRootView rootView = new ReactRootView(plainActivity);
        rootView.startReactApplication(
                reactInstanceManager,
                componentName,
                launchOptions);
        CACHE.put(componentName, rootView);
        return rootView;
    }

    public static void refreshRootView(String componentName) {
        ReactRootView rootView = CACHE.get(componentName);
        if (rootView == null)
            return;
        // 如果不为空,重新生成ReactContext
        rootView.getReactInstanceManager().recreateReactContextInBackground();
    }

}

猜你喜欢

转载自blog.csdn.net/kakaxiqianxin/article/details/80667411