# WebView 预加载

# WebView 的问题

很多 Android 开发者都碰到类似这样的 ANR :

"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)

从堆栈来看,根本原因在于 WebView 在实例化的时候,需要先初始化 Chromium 引擎,而 Chromium 引擎又是一个重量级的组件,而且很多初始化的工作都需要在主线程中完成,这样就很容易造成主线程卡顿甚至 ANR

# 预加载 Chromium 引擎

有人提出了通过 WebView 或者 WebSettings 中的静态方法来间接的初始化 WebView,比如:

  • WebSettings.getDefaultUserAgent(Context)

    /**
     * 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);
    }
    
  • WebViewFactoryProvider.Statics 中的其它方法

这些静态方法只能完成一小部分初始化的工作,对于最终的优化结果来说,并不会有明显的作用,主要的初始化工作是由 WebViewChromiumAwInit 来完成的,整个 Chromium 引擎的启动流程如下:

以下是启动 Chromium 引擎的关键代码:

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);
        }
    }

}

为了避免线程上下文切换导致的副作用,Booster 采用了完全在主线程中初始化的方案:

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);
        }
    }
}

ShadowWebView 通过 WebViewTransformerApplication onCreate() 回调中注入 ShadowWebView.preloadWebView(),当主线程 IDLE 时,启动 Chromium 引擎。

该方案的优点是无侵入、接入成本低,缺点是由于这种方式反射了非公开 API,可能存在兼容性问题。