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文件,加载过程比较缓慢,会造成白屏。造成白屏主要有两个原因:
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。
-
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(); } } }
这里只是简单的下载和删除操作,实际应用中还需考虑到文件的安全性、完整性和正确性,等多种问题