react-native android白屏优化

react native android集成优化(react 0.38)

一、概述

之前的文档介绍了怎么集成react native android基本集成,基本集成很简单,但是把它应用到项目中,并替代原生模块还是有不少坑的,这里主要介绍使用过程中需要解决的几个常见问题

  • 相关API介绍
  • react和native交互
  • 去除DeviceInfo依赖
  • 白屏优化
  • 热更新
  • 混淆配置

二、API简单介绍

  • ReactContext:react上下文,类似于android中的Contextwrapper,就是继承自Contextwrapper。
  • ReactNativeHost:在集成时我们的application需要实现ReactApplication接口,这个接口就是得到ReactNativeHost对象;这个抽象类主要持有ReactInstanceManager对象,对ReactInstanceManager进行管理,如创建、配置、删除等操作;暴露JSBundleFile()方法配置JSBundle文件位置,还提供是否是开发模式等方法等,这些方法最终都是配置到ReactInstanceManager中。ReactNativeHost还持有Application对象。
  • ReactInstanceManager:管理react的声明周期。
  • ReactRootView:装载react实例认的view,继承自ViewGroup,监听布局变化、处理和分发touch事件,启动react应用。
  • ReactActivity:react中activity的基类,相当于我们项目中的BaseActivity;它不处理逻辑,交给代理实现
  • ReactActivityDelegate:ReactActivity和ReactFragmentActivity的代理,处理activity声明周期事件,我们可以继承这个代理,实现我们的自己的逻辑,例如预加载实现就是通过继承这个代理
  • NativeModule:能够提供JS和native交互使用,是一个接口;它里面的getName()方法返回这个module的名称,JS通过这个名称找到对应的module;BaseJavaModule使用java编写实现交互的module抽象类,实现NativeModule接口,提供getConstants(),该方法返回给js调用的一组变量;ReactContextBaseJavaModul继承自BaseJavaModule,并持有ReactApplicationContext引用,一般我们继承这个类来创建交互的module
  • ReactPackage:用于提供react更多额外的能力的一个接口,例如react可能要获取手机的设备相关的信息,要和native交互功能,都需要通过这个接口来注册

三、react和native交互

不管是webview还是react,都经常需要JS与native模块进行交互。react和native交互使用NativeModule接口来实现,通常我们可以继承ReactContextBaseJavaModul类。在交互过程中难免涉及到JS传递参数给native,native返回结果给JS。JS属于弱类型语言
,而且跨平台使用,所以不能使用返回参数的形式将结果返回,在react和native交互中使用发送事件的形式进行数据传递。定义交互方法方法名必须加ReactMethod注解,返回类型为void。

1.native主动向js传递事件,使用广播的形式

public static void sendToJS(ReactContext reactContext,String eventName,@Nullable Object data) {
    if (reactContext == null || TextUtils.isEmpty(eventName) || data == null)
        return;
    reactContext.getJSModule(DeviceEventManagerModule.RCTdeviceeventemitter.class)
            .emit(eventName,data);
}

DeviceEventManagerModule继承自ReactContextBaseJavaModule,react native自己创建的一个module,名称是DeviceEventManager。它持有手机硬件相关的事件,如点击返回键,在JS中可以通过这个module监听这些事件。我们可以调用它来发送自己的事件,在JS中使用addListener方式监听事件。

2.Callback接口方式,写在方法最后一个参数里面

首先创建一个类继承ReactContextBaseJavaModule,定义一个方法(和JS约定,随意取名)testCallback。

@ReactMethod
public void testCallback(String arg1,String arg2,Callback callback) {
    WritableMap map = Arguments.createMap();
    map.putBoolean("arg",true);
    callback.invoke(map);
}

callback是react中定义的一个接口,接口中只有一个invoke方法,它接受一个可变参数,我们可以将结果通过invoke方式发送出去。这种方式是JS主动调用native方法

3.Promise接口方式,写在方法最后一个参数里面

@ReactMethod
public void testPromise(String arg1,Promise promise) {
    try {
        if (arg1.length() > arg2.length()) {
            WritableMap map = Arguments.createMap();
            map.putBoolean("arg",true);
            promise.resolve(map);
        } else {
            promise.reject("1","false");
        }
    } catch (Exception e) {
        promise.reject("1","false",e);
    }
}

定义方式testPromise,方法最后一个参数是Promise接口,这个接口中定义了两类方法。如果执行成功调用resolve方法,接受一个参数,把成功结果返回;如果执行失败,调用reject方法返回,reject有多种重载,可以将返回码、返回消息和错误信息发送出去。这种方式也是JS主动调用native方法

4.注册交互类

使用ReactPackage接口来将我们的功能注册到ReactInstanceManager中,react和native交互也需要注册

  • 创建交互的类RnjsBridgeModule继承ReactContextBaseJavaModule,定义交互方法

    public RnjsBridgeModule(ReactApplicationContext reactContext) {
            super(reactContext);
            mReactContext = reactContext;
     }
    
     @Override
       public String getName() {
          return “moduleName”;
     }
    
      @ReactMethod
       public void testCallback(String arg1,Callback callback) {
          WritableMap map = Arguments.createMap();
               map.putBoolean("arg",true);
          callback.invoke(map);
      }
  • 创建BridgeReactPackage类实现ReactPackage接口,在createNativeModules将RnjsBridgeModule添加到List中返回

    public class BridgeReactPackage implements ReactPackage {
      @Override
        public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
            List<NativeModule> nativeModules = new ArrayList<>();
            nativeModules.add(new RnjsBridgeModule(reactContext));
            return nativeModules;
        }
    
        @Override
        public List<Class<? extends JavaScriptModule>> createJSModules() {
            return Collections.emptyList();
        }
    
        @Override
        public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
            return Collections.emptyList();
        }
    }
  • 在application创建ReactNativeHost时将BridgeReactPackage添加

    private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
            @Override
            protected boolean getUseDeveloperSupport() {
                return BuildConfig.DEBUG;
            }
    
            @Override
            protected List<ReactPackage> getPackages() {
                return Arrays.asList(
                        new RNDeviceInfo(),new MainReactPackage(),new BridgeReactPackage()
                );
            }
    
            @Nullable
            @Override
            protected String getJSBundleFile() {
                return ReactFileUtils.getJSBundlePath(CuliuApplication.this);
            }
        };
    
        @Override
        public ReactNativeHost getReactNativeHost() {
            return mReactNativeHost;
        }

四、去除DeviceInfo依赖

1.问题所在

上篇文章说如果JS中要获取手机的一些配置信息还 需要添依赖:

compile project(':react-native-device-info')

在setting.gradle中添加

include ':react-native-device-info'
    project(':react-native-device-info').projectDir = new File(rootProject.projectDir,'../node_modules/react-native-device-info/android')

这样的就生成一个叫做react-native-device-info的module,我们项目的主module中就必须依赖这个新的library,这是个蛋疼事。

2.去除device info依赖

其实这也属于react和native交互的一个功能,react需要读取设备相关信息。我们可以像处理BridgeReactPackage一样,创建一个RNDeviceInfo的ReactPackage,再注册到Manager中。

  • 创建RNDeviceModule继承ReactContextBaseJavaModule,实现getName方法;重写getConstants方法将js用到的设备相关的变量返回

    public class RNDeviceModule extends ReactContextBaseJavaModule {
        ReactApplicationContext reactContext;
    
        public RNDeviceModule(ReactApplicationContext reactContext) {
            super(reactContext);
            this.reactContext = reactContext;
        }
    
        @Override
        public String getName() {
            return "RNDeviceInfo";
        }
    
        private String getCurrentLanguage() {
            Locale current = getReactApplicationContext().getResources().getConfiguration().locale;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                return current.toLanguageTag();
            } else {
                StringBuilder builder = new StringBuilder();
                builder.append(current.getLanguage());
                if (current.getCountry() != null) {
                    builder.append("-");
                    builder.append(current.getCountry());
                }
                return builder.toString();
            }
        }
    
        private String getCurrentCountry() {
            Locale current = getReactApplicationContext().getResources().getConfiguration().locale;
            return current.getCountry();
        }
    
        private Boolean isEmulator() {
            return Build.FINGERPRINT.startsWith("generic")
                    || Build.FINGERPRINT.startsWith("unkNown")
                    || Build.MODEL.contains("google_sdk")
                    || Build.MODEL.contains("Emulator")
                    || Build.MODEL.contains("Android SDK built for x86")
                    || Build.MANUFACTURER.contains("Genymotion")
                    || (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
                    || "google_sdk".equals(Build.PRODUCT);
        }
    
        private Boolean isTablet() {
            int layout = getReactApplicationContext().getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK;
            return layout == Configuration.SCREENLAYOUT_SIZE_LARGE || layout == Configuration.SCREENLAYOUT_SIZE_XLARGE;
        }
    
        @Override
        public
        @Nullable
        Map<String,Object> getConstants() {
            HashMap<String,Object> constants = new HashMap<String,Object>();
    
            PackageManager packageManager = this.reactContext.getPackageManager();
            String packageName = this.reactContext.getPackageName();
    
            constants.put("appVersion","not available");
            constants.put("buildVersion","not available");
            constants.put("buildNumber",0);
    
            try {
                PackageInfo info = packageManager.getPackageInfo(packageName,0);
                constants.put("appVersion",info.versionName);
                constants.put("buildNumber",info.versionCode);
            } catch (PackageManager.NameNotFoundException e) {
                e.printstacktrace();
            }
    
            String deviceName = "UnkNown";
    
            try {
                BluetoothAdapter myDevice = BluetoothAdapter.getDefaultAdapter();
                deviceName = myDevice.getName();
            } catch (Exception e) {
                e.printstacktrace();
            }
    
            constants.put("instanceId",InstanceID.getInstance(this.reactContext).getId());
            constants.put("deviceName",deviceName);
            constants.put("systemName","Android");
            constants.put("systemVersion",Build.VERSION.RELEASE);
            constants.put("model",Build.MODEL);
            constants.put("brand",Build.BRAND);
            constants.put("deviceid",Build.BOARD);
            constants.put("deviceLocale",this.getCurrentLanguage());
            constants.put("deviceCountry",this.getCurrentCountry());
            constants.put("uniqueId",Settings.Secure.getString(this.reactContext.getContentResolver(),Settings.Secure.ANDROID_ID));
            constants.put("systemManufacturer",Build.MANUFACTURER);
            constants.put("bundleId",packageName);
            constants.put("userAgent",System.getProperty("http.agent"));
            constants.put("timezone",TimeZone.getDefault().getID());
            constants.put("isEmulator",this.isEmulator());
            constants.put("isTablet",this.isTablet());
            return constants;
        }
    }

可以直接coyp它的源码。

  • 同样在创建ReactNativeHost中添加
  • 有些api需要用到google gms中的东西,需要gradle中添加

    playServicesGcm = "com.google.android.gms:play-services-gcm:+"

四、白屏优化

第一次进入react页面会加载JSBundle文件,加载过程比较缓慢,会造成白屏。造成白屏主要有两个原因:

  • 加载JSBundle文件,通过native code的形式将大量的js文件读取
  • 第一次渲染成UI

1.优化加载JSBundle文件

在ReactActivityDelegate的onCreate中会创建RootView并启动reactApplication,加载JSBundle文件,再把rooview中设置到activity中,我们可以将创建RootView中启动reactApplication提前,在进入ReactActivity时就不会加载JSBundle文件

  • 创建单例类RnCacheViewManager,缓存ReactRootView

    public class RnCacheViewManager {
    
    private static volatile RnCacheViewManager instance;
    
    private ReactNativeHost mReactNativeHost;
    
    private String mModuleName;
    
    private Bundle mLaunchOptions;
    
    private ReactRootView mReactRootView;
    
    private boolean isLoadComplete;
    
    private RnCacheViewManager() {
    }
    
    public static RnCacheViewManager getInstance() {
        if (instance == null) {
            synchronized (RnCacheViewManager.class) {
                if (instance == null)
                    instance = new RnCacheViewManager();
            }
        }
        return instance;
    }
    
    public void init(Bundle launchOptions) {
        this.init(launchOptions,ReactMainActivity.MAIN_COMPONENTNAME,CuliuApplication.getInstance().getReactNativeHost());
    }
    
    public void init(Bundle launchOptions,String moduleName,ReactNativeHost nativeHost) {
        mLaunchOptions = launchOptions;
        mModuleName = moduleName;
        mReactNativeHost = nativeHost;
        if (!isLoadComplete)
            prepareLoadApp();
    }
    
    private void prepareLoadApp() {
        isLoadComplete = false;
        mReactRootView = new ReactRootView(CuliuApplication.getContext());
        mReactRootView.startReactApplication(mReactNativeHost.getReactInstanceManager(),mModuleName,mLaunchOptions);
        isLoadComplete = true;
    }
    
    public ReactRootView getReactRootView() {
        if (isLoadComplete == false)
            return null;
        return mReactRootView;
    }
    
    public void removeParent() {
        try {
            ViewParent parent = getReactRootView().getParent();
            if (parent != null)
                ((ViewGroup) parent).removeView(getReactRootView());
        } catch (Exception e) {
            e.printstacktrace();
        }
    }
    
    public void relese() {
        mReactNativeHost = null;
        mModuleName = null;
        mLaunchOptions = null;
        mReactRootView = null;
        instance = null;
        if (isLoadComplete)
            isLoadComplete = false;
    }
    }

在prepareLoadApp方法中创建ReactRootView,并启动startReactApplication。

  • 在进入ReactActivity页面之前预加载

    RnCacheViewManager.getInstance().init(launchOptions);
  • 创建PreLoadReactActivityDelegate继承ReactActivityDelegate,重写loadApp方法和onDestroy方法

    public class PreLoadReactActivityDelegate extends ReactActivityDelegate {
    
        private Activity mActivity;
    
        private ReactRootView mReactRootView;
    
        public PreLoadReactActivityDelegate(Activity activity,@Nullable String mainComponentName) {
            super(activity,mainComponentName);
            mActivity = activity;
        }
    
        public PreLoadReactActivityDelegate(FragmentActivity fragmentActivity,@Nullable String mainComponentName) {
            super(fragmentActivity,mainComponentName);
        }
    
        @Override
        protected void loadApp(String appKey) {
            mReactRootView = RnCacheViewManager.getInstance().getReactRootView();
            if (mReactRootView == null || mActivity == null) {
                super.loadApp(appKey);
            } else {
                ViewGroup.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
                mReactRootView.setLayoutParams(params);
                RnCacheViewManager.getInstance().removeParent();
                mActivity.setContentView(mReactRootView);
            }
        }
    
        @Override
        protected void onDestroy() {
            if (mReactRootView == null || mActivity == null)
                super.onDestroy();
            else
                RnCacheViewManager.getInstance().removeParent();
        }
    
    }

    在loadApp中,先通过RnCacheViewManager取ReactRootView,如果ReactRootView已经预加载完成,则不为null,直接调用setContentView就行,如果null的话则说明预加载还没有完成。为什么设置一下LayoutParams呢,下面再说。
    在onDestroy方法中我们将ReactRootView移除,否则下次进来会报错。建议在主界面退出调用release方法

  • 在ReactMainActivity使用我们自己的代理

    public class ReactMainActivity extends ReactActivity {
    
        /**
         * 应用的根容器名称
         */
        public static final String MAIN_COMPONENTNAME = "componentname";
    
        @Nullable
        @Override
        protected String getMainComponentName() {
            return MAIN_COMPONENTNAME;
        }
    
        @Override
        protected ReactActivityDelegate createReactActivityDelegate() {
            return new PreLoadReactActivityDelegate(this,getMainComponentName());
        }
    }

2.优化第一进入渲染UI

完成以上步骤之后白屏时间明显减短,在没有杀掉进程的情况下基本可以达到和native一样的速度,但是第一次启动应用并进入ReactActivity还是会白屏,下面继续优化白屏。

  • 分析:通过在JSBundleLoader断点发现确实是预加载过JSBundle文件,所以不是加载JSBundle文件造成的白屏。现在的现象是只有第一次进入应用才会出现白屏,如果可以预加载activity就好办了,但是activity是系统管理,不能预加载。我们发现ReactMainActivity里面没有任何ui,只有一个ReactRootView,可以将ReactRootView提前渲染一遍,在进入ReactMainActivity中就可以达到秒启。

  • 在进入ReactMainActivity前一个页面通过RnCacheViewManager获取ReactRootView,并渲染一遍。为了不影响前一个页面展示效果,我们将ReactRootView的大小设置成1像素,这样完全看不到,在进入ReactMainActivity在设置回来,当然也可以用别的方式达到效果就行。

    public static void loadRootView(ViewGroup view) {
            if (view == null)
                return;
            ReactRootView reactRootView = RnCacheViewManager.getInstance().getReactRootView();
            if (reactRootView == null)
                return;
            ViewGroup.LayoutParams params = new LinearLayout.LayoutParams(1,1);
            RnCacheViewManager.getInstance().removeParent();    // 可以不移除,为了安全调用一次
            view.addView(reactRootView,params);
        }

将ReactRootView添加在上一个页面的ViewGroup中,必须保证RnCacheViewManager中已经完成预加载才行,完成这一步之后,基本可以达到秒启,当然,手速够快的情况下,在JSBundle文件还没加载完或者第一次UI还没渲染完进入ReactMainActivity还是可能出现白屏,这种也是可以控制的,方法很多,不多说。

五、热更新

使用ReactNaitve一个很大的好处就是可以实现热更新,服务端下发JSBundle文件替换客户端的JSBundle文件实现热更新。发布release版本前我们将一份最新的JSBundle文件放在assets目录下,名称为index.android.bundle。如果服务端有更新,通过接口告诉我们,我们再拉取最新的JSBundle文件,把它放在内部存储中,在读取的时候,判断内部存储是否有JSBundle文件,如果有,就读取它,没有就读取assets中文件

1.创建ReactFileUtils管理JSBunlde文件位置

public class ReactFileUtils {

        private static final String BUNDLE_FILE_NAME = "index.android.bundle";

        private static final String BUNDLE_FILE_DIR = "RNHotUpdate";

        private static final String BUNDLE_ASSETS_PREFIX = "assets://";

        /**
         * 获取存放ReactNative bundle文件夹存储路径,内部存储下的RNHotUpdate文件夹中,取不到返回""
         * 路径为:/data/data/packagename/files/RNHotUpdate/,下载的JSBundle文件放在改文件夹中
         *
         * @param context
         * @return
         */
        public static String getRNHotUpdatePath(Context context) {
            if (context == null)
                return "";
            File innerFileStorage = FileUtils.getFileDirectory(context);
            if (innerFileStorage == null || TextUtils.isEmpty(innerFileStorage.getAbsolutePath()))
                return "";
            return innerFileStorage.getAbsolutePath() + File.separator + BUNDLE_FILE_DIR;

        }

        /**
         * 获取存放ReactNative bundle文件的存储路径,/data/data/packagename/files/RNHotUpdate/index.android.bundle
         * 由文件夹路径和文件名称组成,如果文件获取不到返回""
         *
         * @param context
         * @return 下载的ReactNative bundle文件的存储路径
         */
        public static String getExtraFileJSBundlePath(Context context) {
            if (context == null)
                return "";
            String path = getRNHotUpdatePath(context);
            if (TextUtils.isEmpty(path))
                return "";
            return path + File.separator + BUNDLE_FILE_NAME;
        }

        /**
         * 获取认ReactNative bundle文件存放路径,也就是assets下的文件
         *
         * @return assets://index.android.bundle
         */
        public static final String getAssetsJSBundlePath() {
            return BUNDLE_ASSETS_PREFIX + BUNDLE_FILE_NAME;
        }

        /**
         * 获取JSBundleFile路径,先从内部存储中取,没有就从assets中取
         *
         * @param context
         * @return JSBundleFile路径
         */
        public static final String getJSBundlePath(Context context) {
            if (context == null)
                return getAssetsJSBundlePath();
            String extraFilePath = getExtraFileJSBundlePath(context);
            if (TextUtils.isEmpty(extraFilePath) || !FileUtils.isFileExists(extraFilePath))
                return getAssetsJSBundlePath();
            return extraFilePath;
        }

    }

2.重写ReactNativeHost的getJSBundleFile方法,配置JSBundle文件位置

private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
            @Override
            protected boolean getUseDeveloperSupport() {
                return BuildConfig.DEBUG;
            }

            @Override
            protected List<ReactPackage> getPackages() {
                return Arrays.asList(
                        new RNDeviceInfo(),new BridgeReactPackage()
                );
            }

            @Nullable
            @Override
            protected String getJSBundleFile() {
                return ReactFileUtils.getJSBundlePath(CuliuApplication.this);
            }
        };

3.创建下载更新管理类RnUpdateManager

public class RnUpdateManager {

        private static final String TAG = "RnUpdateManager";

        /**
         * 服务端文件地址(index.android.bundle和一些图片文件)
         */
        private static final String JSBUNDLE_URL = "http://***.**.**.**:8081/index.android.bundle";

        private Activity mContext;

        public RnUpdateManager(Activity mContext) {
            this.mContext = mContext;
        }

        public void downloadJSBundle(boolean onlyWifi,final String md5) {

            final String downloadFile = ReactFileUtils.getExtraFileJSBundlePath(mContext);

            APP.getInstance().getAppCache().setJSBundleLock(true);

            Http.getInstance().asyncDownload(JSBUNDLE_URL,downloadFile,new DownLoadbroadcastReceiver.DownloadListener() {
                @Override
                public void onResopnse(final Context context,long downloadId) {
                    deleteJSBundle(context);
                    APP.getInstance().getAppCache().setJSBundleLock(false);
                    DebugLog.e(TAG,"onSuccessResopnse");
                }

                @Override
                public void onErrorResponse(Context context,int reason,long downloadId) {
                    DebugLog.e(TAG,"onErrorResponse,reason-->" + reason);
                    APP.getInstance().getAppCache().setJSBundleLock(false);
                }
            });
        }

        private boolean checkFileMd5(String fileName,String md5) {
            return true;
        }

        private boolean verifyPackage() {
            return true;
        }

        public boolean isNewBundle() {
            return true;
        }

        private void deleteJSBundle(Context context) {
            try {
                FileUtils.deleteDirectory(ReactFileUtils.getRNHotUpdatePath(context));
            } catch (IOException e) {
                e.printstacktrace();
            }
        }

    }

这里只是简单的下载和删除操作,实际应用中还需考虑到文件的安全性、完整性和正确性,等多种问题

六、混淆配置

  • keep掉react包本身的类:

    -keep class com.facebook.**{*;}
  • 保证native和react交互类中的方法不能被混淆,否则js找不到这个方法会报错

相关文章

一、前言 在组件方面react和Vue一样的,核心思想玩的就是组件...
前言: 前段时间学习完react后,刚好就接到公司一个react项目...
前言: 最近收到组长通知我们项目组后面新开的项目准备统一技...
react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom...