一、关于预加载方案预研
有一个方案是使用内存换取读取时间的一种折中的方案,网上通篇也说的这个方案。关于这个给大家一个链接,大家可以参考。
React-Native 安卓预加载优化方案
对比IOS端与Android端的首屏时间数据,我们发现安卓端占有一定的劣势,我们在启动React-Native安卓应用时,会发现第一次启动React-Native安卓页面会有一个短暂的白屏过程,而且在完全退出后再进入,仍然会有这个白屏,为什么Android端的白屏时间较IOS较长呢?我们首先分析React-Native页面加载各个阶段的时间响应图
我们可以看到耗时最长的是JsBundle离线包的加载与解析。使用上面的那种全局Map存放RootView的方案,只是优化的是从Bundle解析页面的时间。追踪React Native源码这种方案优化的只是这一部分的时间,本质上并不能解决启动白屏的现像。
/**
* Schedule rendering of the react component rendered by the JS application from the given JS
* module (@{param moduleName}) using provided {@param reactInstanceManager} to attach to the
* JS context of that manager. Extra parameter {@param launchOptions} can be used to pass initial
* properties for the react component.
*/
public void startReactApplication(
ReactInstanceManager reactInstanceManager,
String moduleName,
@Nullable Bundle initialProperties) {
Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "startReactApplication");
try {
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;
mAppProperties = initialProperties;
if (!mReactInstanceManager.hasStartedCreatingInitialContext()) {
mReactInstanceManager.createReactContextInBackground();
}
attachToReactInstanceManager();
} finally {
Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
}
}
二、彻底解决应用白屏的方案
其实说起来也很简单,只是我们在使用时不会去注意这么细节。上面也说了要优化白屏,那就着重2个方面入手一个是JsBundle的加载一个是JsBundle的解析。前一种方案只是优化了JsBundle的解析的时间,那么加载JsBundle是在哪一个函数里加载的呢?
分析源码首先我们来看getReactNativeHost(),这是一个接口函数,我们的实现是传递一些初始化的ReactPackage和JSBundle文件名。
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new RNFSPackage(),
new LottiePackage(),
new AutoHeightWebViewPackage(),
new MReactPackage(),
new LinearGradientPackage(),
new CodePush(BuildConfig.CODEPUSH_KEY, SysApplication.this, BuildConfig.DEBUG),
new SvgPackage(),
new RNViewShotPackage()
);
}
@Override
protected String getJSBundleFile() {
return CodePush.getJSBundleFile();
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
好像这里getJSBundleFile有点像,但只是返回文件名,并不能执行真正的加载,看来加载JSBundle不在这里,继续追踪getReactInstanceManager()方法。追踪到createReactInstanceManager()方法里可以看到ReactNativeHost中声明的getJSBundleFile()在这里调用。使用ReactInstanceManagerBuilder构造ReactInstanceManager
protected ReactInstanceManager createReactInstanceManager() {
ReactMarker.logMarker(ReactMarkerConstants.BUILD_REACT_INSTANCE_MANAGER_START);
ReactInstanceManagerBuilder builder = ReactInstanceManager.builder()
.setApplication(mApplication)
.setJSMainModulePath(getJSMainModuleName())
.setUseDeveloperSupport(getUseDeveloperSupport())
.setRedBoxHandler(getRedBoxHandler())
.setJavaScriptExecutorFactory(getJavaScriptExecutorFactory())
.setUIImplementationProvider(getUIImplementationProvider())
.setJSIModulesProvider(getJSIModulesProvider())
.setInitialLifecycleState(LifecycleState.BEFORE_CREATE);
for (ReactPackage reactPackage : getPackages()) {
builder.addPackage(reactPackage);
}
String jsBundleFile = getJSBundleFile();
if (jsBundleFile != null) {
builder.setJSBundleFile(jsBundleFile);
} else {
builder.setBundleAssetName(Assertions.assertNotNull(getBundleAssetName()));
}
ReactInstanceManager reactInstanceManager = builder.build();
ReactMarker.logMarker(ReactMarkerConstants.BUILD_REACT_INSTANCE_MANAGER_END);
return reactInstanceManager;
}
继续追踪ReactInstanceManagerBuilder的build()方法
/**
* Instantiates a new {@link ReactInstanceManager}.
* Before calling {@code build}, the following must be called:
* <ul>
* <li> {@link #setApplication}
* <li> {@link #setCurrentActivity} if the activity has already resumed
* <li> {@link #setDefaultHardwareBackBtnHandler} if the activity has already resumed
* <li> {@link #setJSBundleFile} or {@link #setJSMainModulePath}
* </ul>
*/
public ReactInstanceManager build() {
Assertions.assertNotNull(
mApplication,
"Application property has not been set with this builder");
Assertions.assertCondition(
mUseDeveloperSupport || mJSBundleAssetUrl != null || mJSBundleLoader != null,
"JS Bundle File or Asset URL has to be provided when dev support is disabled");
Assertions.assertCondition(
mJSMainModulePath != null || mJSBundleAssetUrl != null || mJSBundleLoader != null,
"Either MainModulePath or JS Bundle File needs to be provided");
if (mUIImplementationProvider == null) {
// create default UIImplementationProvider if the provided one is null.
mUIImplementationProvider = new UIImplementationProvider();
}
// We use the name of the device and the app for debugging & metrics
String appName = mApplication.getPackageName();
String deviceName = getFriendlyDeviceName();
return new ReactInstanceManager(
mApplication,
mCurrentActivity,
mDefaultHardwareBackBtnHandler,
mJavaScriptExecutorFactory == null
? new JSCJavaScriptExecutorFactory(appName, deviceName)
: mJavaScriptExecutorFactory,
(mJSBundleLoader == null && mJSBundleAssetUrl != null)
? JSBundleLoader.createAssetLoader(
mApplication, mJSBundleAssetUrl, false /*Asynchronous*/)
: mJSBundleLoader,
mJSMainModulePath,
mPackages,
mUseDeveloperSupport,
mBridgeIdleDebugListener,
Assertions.assertNotNull(mInitialLifecycleState, "Initial lifecycle state was not set"),
mUIImplementationProvider,
mNativeModuleCallExceptionHandler,
mRedBoxHandler,
mLazyNativeModulesEnabled,
mLazyViewManagersEnabled,
mDelayViewManagerClassLoadsEnabled,
mDevBundleDownloadListener,
mMinNumShakes,
mMinTimeLeftInFrameForNonBatchedOperationMs,
mJSIModulesProvider);
}
}
可以发现有这么一段代码
JSBundleLoader.createAssetLoader(
mApplication, mJSBundleAssetUrl, false /*Asynchronous*/)
紧接着看JSBundleLoader的源码
/**
* This loader is recommended one for release version of your app. In that case local JS executor
* should be used. JS bundle will be read from assets in native code to save on passing large
* strings from java to native memory.
*/
public static JSBundleLoader createAssetLoader(
final Context context,
final String assetUrl,
final boolean loadSynchronously) {
return new JSBundleLoader() {
@Override
public String loadScript(CatalystInstanceImpl instance) {
instance.loadScriptFromAssets(context.getAssets(), assetUrl, loadSynchronously);
return assetUrl;
}
};
}
近loadScriptFromAssets()可以发现这里调用了JNI方法从Assets文件夹里读取打包完的JsBundle文件
/* package */ void loadScriptFromAssets(AssetManager assetManager, String assetURL, boolean loadSynchronously) {
mSourceURL = assetURL;
jniLoadScriptFromAssets(assetManager, assetURL, loadSynchronously);
}
那么是哪个函数调用了JSBundleLoader的loadScript(CatalystInstanceImpl instance)了呢?好像明明之中我们也知道了答案,除了createReactContextInBackground()这个方法没有进去看,其他的基本都看了。那么我们来看看createReactContextInBackground()这个方法的内部实现。
我们一步步追踪到这个函数,可以看到这里有一个mBundleLoader对象,通过全局搜索可以得知这个是ReactInstanceManager的一个内部变量,而这个变量就是在上面的ReactInstanceManagerBuilder的build()方法里初始化的。
@ThreadConfined(UI)
private void recreateReactContextInBackgroundFromBundleLoader() {
Log.d(
ReactConstants.TAG,
"ReactInstanceManager.recreateReactContextInBackgroundFromBundleLoader()");
PrinterHolder.getPrinter()
.logMessage(ReactDebugOverlayTags.RN_CORE, "RNCore: load from BundleLoader");
recreateReactContextInBackground(mJavaScriptExecutorFactory, mBundleLoader);
}
继续追踪recreateReactContextInBackground()方法,最后可以看到BundleLoader对象被用于createReactContext()方法中
try {
Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY);
final ReactApplicationContext reactApplicationContext =
createReactContext(
initParams.getJsExecutorFactory().create(),
initParams.getJsBundleLoader());
mCreateReactContextThread = null;
ReactMarker.logMarker(PRE_SETUP_REACT_CONTEXT_START);
final Runnable maybeRecreateReactContextRunnable =
new Runnable() {
@Override
public void run() {
if (mPendingReactContextInitParams != null) {
runCreateReactContextOnNewThread(mPendingReactContextInitParams);
mPendingReactContextInitParams = null;
}
}
};
Runnable setupReactContextRunnable =
new Runnable() {
@Override
public void run() {
try {
setupReactContext(reactApplicationContext);
} catch (Exception e) {
mDevSupportManager.handleException(e);
}
}
};
reactApplicationContext.runOnNativeModulesQueueThread(setupReactContextRunnable);
UiThreadUtil.runOnUiThread(maybeRecreateReactContextRunnable);
} catch (Exception e) {
mDevSupportManager.handleException(e);
}
进一步追踪createReactContext()最终发现,JSBundleLoader的loadScript(CatalystInstanceImpl instance)是在CatalystInstanceImpl中的runJSBundle()方法中调用。
@Override
public void runJSBundle() {
Log.d(ReactConstants.TAG, "CatalystInstanceImpl.runJSBundle()");
Assertions.assertCondition(!mJSBundleHasLoaded, "JS bundle was already loaded!");
// incrementPendingJSCalls();
mJSBundleLoader.loadScript(CatalystInstanceImpl.this);
synchronized (mJSCallsPendingInitLock) {
// Loading the bundle is queued on the JS thread, but may not have
// run yet. It's safe to set this here, though, since any work it
// gates will be queued on the JS thread behind the load.
mAcceptCalls = true;
for (PendingJSCall function : mJSCallsPendingInit) {
function.call(this);
}
mJSCallsPendingInit.clear();
mJSBundleHasLoaded = true;
}
// This is registered after JS starts since it makes a JS call
Systrace.registerListener(mTraceListener);
}
那么前面阐述的问题也便知道了答案,JsBundle是在createReactContextInBackground()中加载的
那么我们优化也是着重这一块优化,把这个函数放在Loading页面里去加载,把原来加载JsBundle的代码从Application挪到Loading页面(启动页:应用启动第一个页面)。
这样有2个好处,一个是Application不会因为加载JsBundle耗时,而迟迟Loading页显示不出来,如果没有做过Android冷启动优化的App可能就是白屏3S以上或者点击应用图标没有要过一会才能进Loading页面,这样就加快了应用的启动速度。
另一个好处就是可以在Loading页面预加载首页的React Native页面,加快首页的加载时间。还有一个就是今天的重点,怎么去解决首页白屏问题。
那就是在Loading页也设置一个ReactRootView,并且给这个View设置setEventListener监听事件,待JsBundle加载完毕之后,就会走进这个监听方法里,在这个方法里跳转首页。这样就不会引起,JsBundle还未加载完成,就跳近了首页。导致首页白屏或者黑屏,需要等JsBundle加载完毕之后才能显示出来。如下面代码所示,initReactNative()在onCreate()中调用。
/**
* 作者:郭翰林
* 时间:2018/8/9 0009 17:59
* 注释:初始化RN,预加载JsBundle
*/
private void initReactNative() {
mReactInstanceManager = ((ReactApplication) GlobalServiceManager.getService(IAppService.class).getAppContext())
.getReactNativeHost()
.getReactInstanceManager();
if (!mReactInstanceManager.hasStartedCreatingInitialContext()) {
mReactInstanceManager.createReactContextInBackground();
mReactRootView = findViewById(R.id.reactRootView);
mReactRootView.startReactApplication(
mReactInstanceManager,
"NetWorkSettingPage",
null
);
//设置ReactRootView监听,如果JsBundle加载完成才允许跳转下个页面
mReactRootView.setEventListener((rootView) -> {
gotoHomeActivity();
});
} else {
gotoHomeActivity();
}
}
网友评论