|
16 | 16 |
|
17 | 17 | import android.app.Application; |
18 | 18 | import android.content.Context; |
| 19 | +import android.os.AsyncTask; |
19 | 20 | import android.os.Bundle; |
20 | 21 | import android.view.View; |
21 | 22 |
|
@@ -72,6 +73,8 @@ public class ReactInstanceManager { |
72 | 73 | /* should only be accessed from main thread (UI thread) */ |
73 | 74 | private final List<ReactRootView> mAttachedRootViews = new ArrayList<>(); |
74 | 75 | private LifecycleState mLifecycleState; |
| 76 | + private boolean mIsContextInitAsyncTaskRunning; |
| 77 | + private @Nullable ReactContextInitParams mPendingReactContextInitParams; |
75 | 78 |
|
76 | 79 | /* accessed from any thread */ |
77 | 80 | private final @Nullable String mBundleAssetName; /* name of JS bundle file in assets folder */ |
@@ -105,11 +108,70 @@ public void toggleElementInspector() { |
105 | 108 |
|
106 | 109 | private final DefaultHardwareBackBtnHandler mBackBtnHandler = |
107 | 110 | new DefaultHardwareBackBtnHandler() { |
| 111 | + @Override |
| 112 | + public void invokeDefaultOnBackPressed() { |
| 113 | + ReactInstanceManager.this.invokeDefaultOnBackPressed(); |
| 114 | + } |
| 115 | + }; |
| 116 | + |
| 117 | + private class ReactContextInitParams { |
| 118 | + private final JavaScriptExecutor mJsExecutor; |
| 119 | + private final JSBundleLoader mJsBundleLoader; |
| 120 | + |
| 121 | + public ReactContextInitParams( |
| 122 | + JavaScriptExecutor jsExecutor, |
| 123 | + JSBundleLoader jsBundleLoader) { |
| 124 | + mJsExecutor = Assertions.assertNotNull(jsExecutor); |
| 125 | + mJsBundleLoader = Assertions.assertNotNull(jsBundleLoader); |
| 126 | + } |
| 127 | + |
| 128 | + public JavaScriptExecutor getJsExecutor() { |
| 129 | + return mJsExecutor; |
| 130 | + } |
| 131 | + |
| 132 | + public JSBundleLoader getJsBundleLoader() { |
| 133 | + return mJsBundleLoader; |
| 134 | + } |
| 135 | + } |
| 136 | + |
| 137 | + /* |
| 138 | + * Task class responsible for (re)creating react context in the background. These tasks can only |
| 139 | + * be executing one at time, see {@link #recreateReactContextInBackground()}. |
| 140 | + */ |
| 141 | + private final class ReactContextInitAsyncTask extends |
| 142 | + AsyncTask<ReactContextInitParams, Void, ReactApplicationContext> { |
| 143 | + |
| 144 | + @Override |
| 145 | + protected void onPreExecute() { |
| 146 | + if (mCurrentReactContext != null) { |
| 147 | + tearDownReactContext(mCurrentReactContext); |
| 148 | + mCurrentReactContext = null; |
| 149 | + } |
| 150 | + } |
| 151 | + |
| 152 | + @Override |
| 153 | + protected ReactApplicationContext doInBackground(ReactContextInitParams... params) { |
| 154 | + Assertions.assertCondition(params != null && params.length > 0 && params[0] != null); |
| 155 | + return createReactContext(params[0].getJsExecutor(), params[0].getJsBundleLoader()); |
| 156 | + } |
| 157 | + |
108 | 158 | @Override |
109 | | - public void invokeDefaultOnBackPressed() { |
110 | | - ReactInstanceManager.this.invokeDefaultOnBackPressed(); |
| 159 | + protected void onPostExecute(ReactApplicationContext reactContext) { |
| 160 | + try { |
| 161 | + setupReactContext(reactContext); |
| 162 | + } finally { |
| 163 | + mIsContextInitAsyncTaskRunning = false; |
| 164 | + } |
| 165 | + |
| 166 | + // Handle enqueued request to re-initialize react context. |
| 167 | + if (mPendingReactContextInitParams != null) { |
| 168 | + recreateReactContextInBackground( |
| 169 | + mPendingReactContextInitParams.getJsExecutor(), |
| 170 | + mPendingReactContextInitParams.getJsBundleLoader()); |
| 171 | + mPendingReactContextInitParams = null; |
| 172 | + } |
111 | 173 | } |
112 | | - }; |
| 174 | + } |
113 | 175 |
|
114 | 176 | private ReactInstanceManager( |
115 | 177 | Context applicationContext, |
@@ -161,10 +223,35 @@ private static void initializeSoLoaderIfNecessary(Context applicationContext) { |
161 | 223 | SoLoader.init(applicationContext, /* native exopackage */ false); |
162 | 224 | } |
163 | 225 |
|
| 226 | + /** |
| 227 | + * Trigger react context initialization asynchronously in a background async task. This enables |
| 228 | + * applications to pre-load the application JS, and execute global code before |
| 229 | + * {@link ReactRootView} is available and measured. |
| 230 | + */ |
| 231 | + public void createReactContextInBackground() { |
| 232 | + if (mUseDeveloperSupport) { |
| 233 | + if (mDevSupportManager.hasUpToDateJSBundleInCache()) { |
| 234 | + // If there is a up-to-date bundle downloaded from server, always use that |
| 235 | + onJSBundleLoadedFromServer(); |
| 236 | + return; |
| 237 | + } else if (mBundleAssetName == null || |
| 238 | + !mDevSupportManager.hasBundleInAssets(mBundleAssetName)) { |
| 239 | + // Bundle not available in assets, fetch from the server |
| 240 | + mDevSupportManager.handleReloadJS(); |
| 241 | + return; |
| 242 | + } |
| 243 | + } |
| 244 | + // Use JS file from assets |
| 245 | + recreateReactContextInBackground( |
| 246 | + new JSCJavaScriptExecutor(), |
| 247 | + JSBundleLoader.createAssetLoader( |
| 248 | + mApplicationContext.getAssets(), |
| 249 | + mBundleAssetName)); |
| 250 | + } |
| 251 | + |
164 | 252 | /** |
165 | 253 | * This method will give JS the opportunity to consume the back button event. If JS does not |
166 | | - * consume the event, mDefaultBackButtonImpl will be invoked at the end of the round trip |
167 | | - * to JS. |
| 254 | + * consume the event, mDefaultBackButtonImpl will be invoked at the end of the round trip to JS. |
168 | 255 | */ |
169 | 256 | public void onBackPressed() { |
170 | 257 | UiThreadUtil.assertOnUiThread(); |
@@ -255,16 +342,24 @@ public void showDevOptionsDialog() { |
255 | 342 |
|
256 | 343 | /** |
257 | 344 | * Attach given {@param rootView} to a catalyst instance manager and start JS application using |
258 | | - * JS module provided by {@link ReactRootView#getJSModuleName}. This view will then be tracked |
259 | | - * by this manager and in case of catalyst instance restart it will be re-attached. |
| 345 | + * JS module provided by {@link ReactRootView#getJSModuleName}. If the react context is currently |
| 346 | + * being (re)-created, or if react context has not been created yet, the JS application associated |
| 347 | + * with the provided root view will be started asynchronously, i.e this method won't block. |
| 348 | + * This view will then be tracked by this manager and in case of catalyst instance restart it will |
| 349 | + * be re-attached. |
260 | 350 | */ |
261 | 351 | /* package */ void attachMeasuredRootView(ReactRootView rootView) { |
262 | 352 | UiThreadUtil.assertOnUiThread(); |
263 | 353 | mAttachedRootViews.add(rootView); |
264 | | - if (mCurrentReactContext == null) { |
265 | | - initializeReactContext(); |
266 | | - } else { |
267 | | - attachMeasuredRootViewToInstance(rootView, mCurrentReactContext.getCatalystInstance()); |
| 354 | + |
| 355 | + // If react context is being created in the background, JS application will be started |
| 356 | + // automatically when creation completes, as root view is part of the attached root view list. |
| 357 | + if (!mIsContextInitAsyncTaskRunning) { |
| 358 | + if (mCurrentReactContext == null) { |
| 359 | + createReactContextInBackground(); |
| 360 | + } else { |
| 361 | + attachMeasuredRootViewToInstance(rootView, mCurrentReactContext.getCatalystInstance()); |
| 362 | + } |
268 | 363 | } |
269 | 364 | } |
270 | 365 |
|
@@ -300,53 +395,51 @@ public void showDevOptionsDialog() { |
300 | 395 | } |
301 | 396 |
|
302 | 397 | private void onReloadWithJSDebugger(ProxyJavaScriptExecutor proxyExecutor) { |
303 | | - recreateReactContext( |
| 398 | + recreateReactContextInBackground( |
304 | 399 | proxyExecutor, |
305 | 400 | JSBundleLoader.createRemoteDebuggerBundleLoader( |
306 | 401 | mDevSupportManager.getJSBundleURLForRemoteDebugging())); |
307 | 402 | } |
308 | 403 |
|
309 | 404 | private void onJSBundleLoadedFromServer() { |
310 | | - recreateReactContext( |
| 405 | + recreateReactContextInBackground( |
311 | 406 | new JSCJavaScriptExecutor(), |
312 | 407 | JSBundleLoader.createCachedBundleFromNetworkLoader( |
313 | 408 | mDevSupportManager.getSourceUrl(), |
314 | 409 | mDevSupportManager.getDownloadedJSBundleFile())); |
315 | 410 | } |
316 | 411 |
|
317 | | - private void initializeReactContext() { |
318 | | - if (mUseDeveloperSupport) { |
319 | | - if (mDevSupportManager.hasUpToDateJSBundleInCache()) { |
320 | | - // If there is a up-to-date bundle downloaded from server, always use that |
321 | | - onJSBundleLoadedFromServer(); |
322 | | - return; |
323 | | - } else if (mBundleAssetName == null || |
324 | | - !mDevSupportManager.hasBundleInAssets(mBundleAssetName)) { |
325 | | - // Bundle not available in assets, fetch from the server |
326 | | - mDevSupportManager.handleReloadJS(); |
327 | | - return; |
328 | | - } |
329 | | - } |
330 | | - // Use JS file from assets |
331 | | - recreateReactContext( |
332 | | - new JSCJavaScriptExecutor(), |
333 | | - JSBundleLoader.createAssetLoader( |
334 | | - mApplicationContext.getAssets(), |
335 | | - mBundleAssetName)); |
336 | | - } |
337 | | - |
338 | | - private void recreateReactContext( |
| 412 | + private void recreateReactContextInBackground( |
339 | 413 | JavaScriptExecutor jsExecutor, |
340 | 414 | JSBundleLoader jsBundleLoader) { |
341 | 415 | UiThreadUtil.assertOnUiThread(); |
342 | | - if (mCurrentReactContext != null) { |
343 | | - tearDownReactContext(mCurrentReactContext); |
| 416 | + |
| 417 | + ReactContextInitParams initParams = new ReactContextInitParams(jsExecutor, jsBundleLoader); |
| 418 | + if (!mIsContextInitAsyncTaskRunning) { |
| 419 | + // No background task to create react context is currently running, create and execute one. |
| 420 | + ReactContextInitAsyncTask initTask = new ReactContextInitAsyncTask(); |
| 421 | + initTask.execute(initParams); |
| 422 | + mIsContextInitAsyncTaskRunning = true; |
| 423 | + } else { |
| 424 | + // Background task is currently running, queue up most recent init params to recreate context |
| 425 | + // once task completes. |
| 426 | + mPendingReactContextInitParams = initParams; |
344 | 427 | } |
345 | | - mCurrentReactContext = createReactContext(jsExecutor, jsBundleLoader); |
| 428 | + } |
| 429 | + |
| 430 | + private void setupReactContext(ReactApplicationContext reactContext) { |
| 431 | + UiThreadUtil.assertOnUiThread(); |
| 432 | + Assertions.assertCondition(mCurrentReactContext == null); |
| 433 | + mCurrentReactContext = Assertions.assertNotNull(reactContext); |
| 434 | + CatalystInstance catalystInstance = |
| 435 | + Assertions.assertNotNull(reactContext.getCatalystInstance()); |
| 436 | + |
| 437 | + catalystInstance.initialize(); |
| 438 | + mDevSupportManager.onNewReactContextCreated(reactContext); |
| 439 | + moveReactContextToCurrentLifecycleState(reactContext); |
| 440 | + |
346 | 441 | for (ReactRootView rootView : mAttachedRootViews) { |
347 | | - attachMeasuredRootViewToInstance( |
348 | | - rootView, |
349 | | - mCurrentReactContext.getCatalystInstance()); |
| 442 | + attachMeasuredRootViewToInstance(rootView, catalystInstance); |
350 | 443 | } |
351 | 444 | } |
352 | 445 |
|
@@ -430,10 +523,6 @@ private ReactApplicationContext createReactContext( |
430 | 523 | } |
431 | 524 |
|
432 | 525 | reactContext.initializeWithInstance(catalystInstance); |
433 | | - catalystInstance.initialize(); |
434 | | - mDevSupportManager.onNewReactContextCreated(reactContext); |
435 | | - |
436 | | - moveReactContextToCurrentLifecycleState(reactContext); |
437 | 526 |
|
438 | 527 | return reactContext; |
439 | 528 | } |
|
0 commit comments