WebView Preloading

The Problems of WebView

Many Android developers have encountered ANRs similar to the following:

"main" prio=5 tid=1 Waiting
  | group="main" sCount=1 dsCount=0 obj=0x756e2a88 self=0x7253895a00
  | sysTid=5256 nice=-10 cgrp=default sched=0/0 handle=0x72577baa98
  | state=S schedstat=( 362198032 46014107 637 ) utm=24 stm=11 core=3 HZ=100
  | stack=0x7fc2f0d000-0x7fc2f0f000 stackSize=8MB
  | held mutexes=
  at java.lang.Object.wait!(Native method)
  - waiting on <0x0a4a9cf4> (a java.lang.Object)
  at java.lang.Thread.parkFor$(Thread.java:2127)
  - locked <0x0a4a9cf4> (a java.lang.Object)
  at sun.misc.Unsafe.park(Unsafe.java:325)
  at java.util.concurrent.locks.LockSupport.park(LockSupport.java:161)
  at java.util.concurrent.FutureTask.awaitDone(FutureTask.java:421)
  at java.util.concurrent.FutureTask.get(FutureTask.java:163)
  at android.os.AsyncTask.get(AsyncTask.java:514)
  at org.chromium.content.browser.BrowserStartupController.prepareToStartBrowserProcess(BrowserStartupController.java:57)
  at org.chromium.content.browser.BrowserStartupController.startBrowserProcessesSync(BrowserStartupController.java:16)
  at org.chromium.android_webview.AwBrowserProcess$1.run(AwBrowserProcess.java:14)
  at org.chromium.base.ThreadUtils.runOnUiThreadBlocking(ThreadUtils.java:10)
  at com.android.webview.chromium.WebViewChromiumFactoryProvider.startChromiumLocked(WebViewChromiumFactoryProvider.java:219)
  at com.android.webview.chromium.WebViewChromiumFactoryProvider.ensureChromiumStartedLocked(WebViewChromiumFactoryProvider.java:151)
  at com.android.webview.chromium.WebViewChromiumFactoryProvider.startYourEngines(WebViewChromiumFactoryProvider.java:239)
  - locked <0x064f5c1d> (a java.lang.Object)
  at com.android.webview.chromium.WebViewChromium.init(WebViewChromium.java:30)
  at android.webkit.WebView.<init>(WebView.java:636)
  at android.webkit.WebView.<init>(WebView.java:572)
  at android.webkit.WebView.<init>(WebView.java:555)
  at android.webkit.WebView.<init>(WebView.java:542)
  at java.lang.reflect.Constructor.newInstance0!(Native method)
  at java.lang.reflect.Constructor.newInstance(Constructor.java:430)
  at android.view.LayoutInflater.createView(LayoutInflater.java:645)
  at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:787)
  at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:727)
  at android.view.LayoutInflater.rInflate(LayoutInflater.java:858)
  at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:821)
  at android.view.LayoutInflater.inflate(LayoutInflater.java:518)
  - locked <0x09493e92> (a java.lang.Object[])
  at android.view.LayoutInflater.inflate(LayoutInflater.java:426)
  at com.xxx.xxx.xxx.xxx.view.fragment.WebViewFragment.onCreateView(WebViewFragment.java:99)
  at android.support.v4.app.Fragment.performCreateView(Fragment.java:2189)
  at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1299)
  at android.support.v4.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1528)
  at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1595)
  at android.support.v4.app.BackStackRecord.executeOps(BackStackRecord.java:757)
  at android.support.v4.app.FragmentManagerImpl.executeOps(FragmentManager.java:2355)
  at android.support.v4.app.FragmentManagerImpl.executeOpsTogether(FragmentManager.java:2146)
  at android.support.v4.app.FragmentManagerImpl.optimizeAndExecuteOps(FragmentManager.java:2098)
  at android.support.v4.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:2008)
  at android.support.v4.app.FragmentController.execPendingActions(FragmentController.java:388)
  at android.support.v4.app.FragmentActivity.onStart(FragmentActivity.java:607)
  at android.support.v7.app.AppCompatActivity.onStart(AppCompatActivity.java:178)
  at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1248)
  at android.app.Activity.performStart(Activity.java:6699)
  at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2629)
  at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2727)
  at android.app.ActivityThread.-wrap12(ActivityThread.java:-1)
  at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1478)
  at android.os.Handler.dispatchMessage(Handler.java:102)
  at android.os.Looper.loop(Looper.java:154)
  at android.app.ActivityThread.main(ActivityThread.java:6121)
  at java.lang.reflect.Method.invoke!(Native method)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:889)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:779)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

From the stack trace, the root cause is that when WebView is instantiated, the Chromium engine needs to be initialized first, and the Chromium engine is a heavyweight component, and a lot of initialization work needs to be done in the main thread, which is easy to block the main thread and caused ANR finally.

Preload the Chromium Engine

Someone has proposed to initialize WebView indirectly by calling the static methods in WebView or WebSettings, such as:

  • WebSettings.getDefaultUserAgent(Context)open in new window

    /**
     * Returns the default User-Agent used by a WebView.
     * An instance of WebView could use a different User-Agent if a call
     * is made to {@link WebSettings#setUserAgentString(String)}.
     *
     * @param context a Context object used to access application assets
     */
    public static String getDefaultUserAgent(Context context) {
        return WebViewFactory.getProvider().getStatics().getDefaultUserAgent(context);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  • Other methods of WebViewFactoryProvider.Staticsopen in new window

After investication, we have found, these static methods can't initialize the Chromium engine completely, so, there isn't any significant improvement, the main initialization work is done by the WebViewChromiumAwInitopen in new window, the entire startup process of the Chromium engine is shown as below:

The following is the key source code to start the Chromium engine:

public class WebViewChromiumAwInit {

    protected void startChromiumLocked() {
        try (ScopedSysTraceEvent event =
                        ScopedSysTraceEvent.scoped("WebViewChromiumAwInit.startChromiumLocked")) {
            TraceEvent.setATraceEnabled(mFactory.getWebViewDelegate().isTraceTagEnabled());
            assert Thread.holdsLock(mLock) && ThreadUtils.runningOnUiThread();
            // The post-condition of this method is everything is ready, so notify now to cover all
            // return paths. (Other threads will not wake-up until we release |mLock|, whatever).
            mLock.notifyAll();
            if (mStarted) {
                return;
            }
            final Context context = ContextUtils.getApplicationContext();
            BuildInfo.setFirebaseAppId(AwFirebaseConfig.getFirebaseAppId());
            JNIUtils.setClassLoader(WebViewChromiumAwInit.class.getClassLoader());
            ResourceBundle.setAvailablePakLocales(
                    new String[] {}, AwLocaleConfig.getWebViewSupportedPakLocales());
            BundleUtils.setIsBundle(ProductConfig.IS_BUNDLE);
            // We are rewriting Java resources in the background.
            // NOTE: Any reference to Java resources will cause a crash.
            try (ScopedSysTraceEvent e =
                            ScopedSysTraceEvent.scoped("WebViewChromiumAwInit.LibraryLoader")) {
                LibraryLoader.getInstance().ensureInitialized();
            }
            PathService.override(PathService.DIR_MODULE, "/system/lib/");
            PathService.override(DIR_RESOURCE_PAKS_ANDROID, "/system/framework/webview/paks");
            initPlatSupportLibrary();
            doNetworkInitializations(context);
            waitUntilSetUpResources();
            // NOTE: Finished writing Java resources. From this point on, it's safe to use them.
            AwBrowserProcess.configureChildProcessLauncher();
            // finishVariationsInitLocked() must precede native initialization so the seed is
            // available when AwFeatureListCreator::SetUpFieldTrials() runs.
            finishVariationsInitLocked();
            AwBrowserProcess.start();
            AwBrowserProcess.handleMinidumpsAndSetMetricsConsent(true /* updateMetricsConsent */);
            mSharedStatics = new SharedStatics();
            if (BuildInfo.isDebugAndroid()) {
                mSharedStatics.setWebContentsDebuggingEnabledUnconditionally(true);
            }
            mFactory.getWebViewDelegate().setOnTraceEnabledChangeListener(
                    new WebViewDelegate.OnTraceEnabledChangeListener() {
                        @Override
                        public void onTraceEnabledChange(boolean enabled) {
                            TraceEvent.setATraceEnabled(enabled);
                        }
                    });
            mStarted = true;
            RecordHistogram.recordSparseHistogram("Android.WebView.TargetSdkVersion",
                    context.getApplicationInfo().targetSdkVersion);
            try (ScopedSysTraceEvent e = ScopedSysTraceEvent.scoped(
                         "WebViewChromiumAwInit.initThreadUnsafeSingletons")) {
                // Initialize thread-unsafe singletons.
                AwBrowserContext awBrowserContext = getBrowserContextOnUiThread();
                mGeolocationPermissions = new GeolocationPermissionsAdapter(
                        mFactory, awBrowserContext.getGeolocationPermissions());
                mWebStorage =
                        new WebStorageAdapter(mFactory, mBrowserContext.getQuotaManagerBridge());
                mAwTracingController = getTracingController();
                mServiceWorkerController = awBrowserContext.getServiceWorkerController();
                mAwProxyController = new AwProxyController();
            }
            mFactory.getRunQueue().drainQueue();
            maybeLogActiveTrials(context);
        }
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

To avoid the side effects caused by thread context switching, booster-transform-webviewopen in new window initializes the Chromium engine in the main thread when the main thread is idle:

public class ShadowWebView {

    public static void preloadWebView(final Application app) {
        try {
            app.getMainLooper().getQueue().addIdleHandler(new MessageQueue.IdleHandler() {
                @Override
                public boolean queueIdle() {
                    startChromiumEngine();
                    return false;
                }
            });
        } catch (final Throwable t) {
            Log.e(TAG, "Oops!", t);
        }
    }

    private static void startChromiumEngine() {
        try {
            final long t0 = SystemClock.uptimeMillis();
            final Object provider = invokeStaticMethod(Class.forName("android.webkit.WebViewFactory"), "getProvider");
            invokeMethod(provider, "startYourEngines", new Class[]{boolean.class}, new Object[]{true});
            Log.i(TAG, "Start chromium engine complete: " + (SystemClock.uptimeMillis() - t0) + " ms");
        } catch (final Throwable t) {
            Log.e(TAG, "Start chromium engine error", t);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

The ShadowWebView.preloadWebView()open in new window is able to be called when the main thread is idle, this solution was implemented by instrumenting the invocation of ShadowWebView.preloadWebView()open in new window into the application's onCreate() method, it is non-intrusive, and easy to integrate, since it relects non-public APIs, it might have compatibility issues.