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它的源码。
四、白屏优化
第一次进入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();
}
}
}
这里只是简单的下载和删除操作,实际应用中还需考虑到文件的安全性、完整性和正确性,等多种问题
六、混淆配置