Android startup optimization case: WebView unexpected initialization troubleshooting

When I was doing startup optimization at the end of last year, I had a rather interesting case to share with you. I hope you can get from my sharing how it looked low, crude and efficient< when I was doing some troubleshooting and repairs. /strong>’s.

1. Phenomenon

When we used Perfetto to observe the performance of the app startup process, we found a period of unexpected Webview initialization time of tens of milliseconds to hundreds of milliseconds in the UI thread (machine environment: Xiaomi 10 pro), online This code may take longer to execute on the user’s machine.

Why is it said unexpected:

  • The homepage does not use WebView or preload
  • The initialization of the X5 kernel is also after the startup process

2. Follow the clues

Generally, when we discover this kind of problem, how should we deal with it?

Understand the process. If when troubleshooting startup performance, you find that the main (sub) thread takes longer than expected, the first step is to find out how this time-consuming code is called.

Discover the problem as soon as possible, when we know how the code is called, we can find ways to repair it. If it is because the project code is called at the wrong time, then postpone or remove the relevant transfer

WebViewChromiumAwInit.java

Then let’s start with the first step and understand the process. We can see that the system method called by the time-consuming code block in the picture is:

WebViewChromiumAwInit.startChromiumLocked, since Perfetto cannot see App-related stack information, we cannot directly know which line of code is causing it.

Then let’s follow the webview source code to see the specific situation. Click on the WebViewChromiumAwInit.java page to see the relevant code and find that startChromiumLocked is blocked by ensureChromiumStartedLocked Method called:

// This method is not private only because the downstream subclass needs to access it,
// it shouldn't be accessed from anywhere else.
/* package */
void ensureChromiumStartedLocked(boolean fromThreadSafeFunction) {
        assert Thread.holdsLock(mLock);
        if (mInitState == INIT_FINISHED) { // Early-out for the common case.
            return;
        }
        if (mInitState == INIT_NOT_STARTED) {
            // If we're the first thread to enter ensureChromiumStartedLocked, we need to determine
            // which thread will be the UI thread; declare init has started so that no other thread
            // will try to do this.
            mInitState = INIT_STARTED;
            setChromiumUiThreadLocked(fromThreadSafeFunction);
        }
        if (ThreadUtils.runningOnUiThread()) {
            // If we are currently running on the UI thread then we must do init now. If there was
            // already a task posted to the UI thread from another thread to do it, it will just
            // no-op when it runs.
            startChromiumLocked();
            return;
        }
        mIsPostedFromBackgroundThread = true;
        // If we're not running on the UI thread (because init was triggered by a thread-safe
        // function), post init to the UI thread, since init is *not* thread-safe.
        AwThreadUtils.postToUiThreadLooper(new Runnable() {
            @Override
            public void run() {
                synchronized (mLock) {
                    startChromiumLocked();
                }
            }
        });
        // Wait for the UI thread to finish init.
        while (mInitState != INIT_FINISHED) {
            try {
                mLock.wait();
            } catch (InterruptedException e) {
                // Keep trying; we can't abort init as WebView APIs do not declare that they throw
                // InterruptedException.
            }
        }
    }

So who calls the ensureChromiumStartedLocked method? We can roughly find the following suspects in the WebViewChromiumAwInit.java file. The first reaction is "There are too many, how can we troubleshoot this".

-getAwTracingController
-getAwProxyController
-startYourEngines
-getStatics
-getDefaultGeolocationPermissions
-getDefaultServiceWorkerController
-getWebIconDatabase
-getDefaultWebStorage
-getDefaultWebViewDatabase

public class WebViewChromiumAwInit {
    public AwTracingController getAwTracingController() {
        synchronized (mLock) {
            if (mAwTracingController == null) {
                ensureChromiumStartedLocked(true);
            }
        }
        return mAwTracingController;
    }
    public AwProxyController getAwProxyController() {
        synchronized (mLock) {
            if (mAwProxyController == null) {
                ensureChromiumStartedLocked(true);
            }
        }
        return mAwProxyController;
    }
    void startYourEngines(boolean fromThreadSafeFunction) {
        synchronized (mLock) {
            ensureChromiumStartedLocked(fromThreadSafeFunction);
        }
    }
    
    public SharedStatics getStatics() {
        synchronized (mLock) {
            if (mSharedStatics == null) {
                ensureChromiumStartedLocked(true);
            }
        }
        return mSharedStatics;
    }

    public GeolocationPermissions getDefaultGeolocationPermissions() {
        synchronized (mLock) {
            if (mDefaultGeolocationPermissions == null) {
                ensureChromiumStartedLocked(true);
            }
        }
        return mDefaultGeolocationPermissions;
    }

    public AwServiceWorkerController getDefaultServiceWorkerController() {
        synchronized (mLock) {
            if (mDefaultServiceWorkerController == null) {
                ensureChromiumStartedLocked(true);
            }
        }
        return mDefaultServiceWorkerController;
    }
    public android.webkit.WebIconDatabase getWebIconDatabase() {
        synchronized (mLock) {
            ensureChromiumStartedLocked(true);
            if (mWebIconDatabase == null) {
                mWebIconDatabase = new WebIconDatabaseAdapter();
            }
        }
        return mWebIconDatabase;
    }

    public WebStorage getDefaultWebStorage() {
        synchronized (mLock) {
            if (mDefaultWebStorage == null) {
                ensureChromiumStartedLocked(true);
            }
        }
        return mDefaultWebStorage;
    }

    public WebViewDatabase getDefaultWebViewDatabase(final Context context) {
        synchronized (mLock) {
            ensureChromiumStartedLocked(true);
            if (mDefaultWebViewDatabase == null) {
                mDefaultWebViewDatabase = new WebViewDatabaseAdapter(mFactory,
                        HttpAuthDatabase.newInstance(context, HTTP_AUTH_DATABASE_FILE),
                        mDefaultBrowserContext);
            }
        }
        return mDefaultWebViewDatabase;
    }
}

WebViewChromiumFactoryProvider.java

After the simple analysis above, we roughly know that WebViewChromiumAwInit.startChromiumLocked is called by the ensureChromiumStartedLocked method, and the ensureChromiumStartedLocked method will be called by the following method call, then our next work needs to find out who has called the following methods.

-getAwTracingController
-getAwProxyController
-startYourEngines
-getStatics
-getDefaultGeolocationPermissions
-getDefaultServiceWorkerController
-getWebIconDatabase
-getDefaultWebStorage
-getDefaultWebViewDatabase

Here, I would like to share one of my homespun methods. We need to find out where these methods are called. Then we find a method that we don’t know and doesn’t look like it will be mentioned by others. We google it and we selected at a glance. getDefaultServiceWorkerController method, no way, who told me that I don’t know you. Although the method is stupid, it cannot maintain efficiency. So, we found it – WebViewChromiumFactoryProvider.java

Let’s take a rough look at what role WebViewChromiumFactoryProvider plays. WebViewChromiumFactoryProvider implements the WebViewFactoryProvider interface. A simple understanding is that it is the factory of WebView , if the App wants to create a WebView, it will createWebView through the implementation class of the WebViewFactoryProvider interface, so it is actually a factory pattern. Ensure compatibility, portability and extensibility by abstracting specification APIs.

We also saw the calls to several methods listed above in this file as we wished. WebViewChromiumFactoryProvider In the implementation of the interface method, a series of methods in WebViewChromiumAwInit are called, as follows:

//WebViewChromiumFactoryProvider.java
@Override
public WebViewProvider createWebView(WebView webView, WebView.PrivateAccess privateAccess) {
    return new WebViewChromium(this, webView, privateAccess, mShouldDisableThreadChecking);
}

//We intercept a section
    @Override
    public GeolocationPermissions getGeolocationPermissions() {
        return mAwInit.getDefaultGeolocationPermissions();
    }
    @Override
    public CookieManager getCookieManager() {
        return mAwInit.getDefaultCookieManager();
    }
    @Override
    public ServiceWorkerController getServiceWorkerController() {
        synchronized (mAwInit.getLock()) {
            if (mServiceWorkerController == null) {
                mServiceWorkerController = new ServiceWorkerControllerAdapter(
                        mAwInit.getDefaultServiceWorkerController());
            }
        }
        return mServiceWorkerController;
    }
    @Override
    public TokenBindingService getTokenBindingService() {
        return null;
    }
    @Override
    public android.webkit.WebIconDatabase getWebIconDatabase() {
        return mAwInit.getWebIconDatabase();
    }
    @Override
    public WebStorage getWebStorage() {
        return mAwInit.getDefaultWebStorage();
    }
    @Override
    public WebViewDatabase getWebViewDatabase(final Context context) {
        return mAwInit.getDefaultWebViewDatabase(context);
    }
    WebViewDelegate getWebViewDelegate() {
        return mWebViewDelegate;
    }
    WebViewContentsClientAdapter createWebViewContentsClientAdapter(WebView webView,
            Context context) {
        try (ScopedSysTraceEvent e = ScopedSysTraceEvent.scoped(
                     "WebViewChromiumFactoryProvider.insideCreateWebViewContentsClientAdapter")) {
            return new WebViewContentsClientAdapter(webView, context, mWebViewDelegate);
        }
    }
    void startYourEngines(boolean onMainThread) {
        try (ScopedSysTraceEvent e1 = ScopedSysTraceEvent.scoped(
                     "WebViewChromiumFactoryProvider.startYourEngines")) {
            mAwInit.startYourEngines(onMainThread);
        }
    }
    boolean hasStarted() {
        return mAwInit.hasStarted();
    }

3. Identify the problem

We have a clearer idea above by reading the specific code implementations of the two files WebViewChromiumFactoryProvider.java and WebViewChromiumAwInit.java.

During the initialization process, the App calls a method of the WebViewFactoryProvider interface implementation class. This method calls one or more of the following methods of WebViewChromiumAwInit. In fact, the problem becomes clear. We only need to find out which line of code during the startup phase of our app will call a certain interface method of the WebViewFactoryProvider interface.

-getAwTracingController
-getAwProxyController
-startYourEngines
-getStatics
-getDefaultGeolocationPermissions
-getDefaultServiceWorkerController
-getWebIconDatabase
-getDefaultWebStorage
-getDefaultWebViewDatabase

Since the code of WebView will not be packaged into the app, the WebView kernel used by the app uses the Android system which is responsible for the built-in and upgraded WebView Kernel code, so it is not possible to make hook calls through transform. Here we use dynamic proxy to hook the WebViewFactoryProvider interface method. We generate a proxy object through dynamic proxy and use reflection. , replace the sProviderInstance object of android.webkit.WebViewFactory.

 ##WebViewFactory
    @SystemApi
    public final class WebViewFactory{
        //...
        @UnsupportedAppUsage
        private static WebViewFactoryProvider sProviderInstance;
        //...
    }
    
    
    ##Dynamic proxy
    try {
        Class clas = Class.forName("android.webkit.WebViewFactory");
        Method method = clas.getDeclaredMethod("getProvider");
        method.setAccessible(true);
        Object obj = method.invoke(null);

        Object hookService = Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getSuperclass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Log.d("zttt", "hookService method: " + method.getName());
                        new RuntimeException(method.getName()).printStackTrace();
                        return method.invoke(obj, args);
                    }
                });

        Field field = clas.getDeclaredField("sProviderInstance");
        field.setAccessible(true);
        field.set(null, hookService);
    } catch (Exception e) {
        e.printStackTrace();
    }

After replacing sProviderInstance, we can add breakpoints to our proxy logic for debugging, and finally found the initiator of the unexpected initialization of WebView: WebSettings.getDefaultUserAgent.

4. Solving problems

The matter is solved here. You only need to perform a compile-time Hook on WebSettings.getDefaultUserAgent and redirect to the relevant method with cache defaultUserAgent. If there is a local cache, Read it directly, or read it immediately if it is not available locally. Thanks to the easy-to-use configured Hook framework I implemented in the project before, this small hook work can be completed in less than a minute.

Of course, there is another issue that needs to be considered here, that is, when the defaultUserAgent of the user’s machine changes, how can the local cache be updated in a timely manner and the new defaultUserAgent be used in network requests? . Our approach is:

  • When there is no local cache, immediately call WebSettings.getDefaultUserAgent to get the value and update the cache;

  • Each time the App startup phase ends, WebSettings.getDefaultUserAgent will be called in the child thread to get the value and update the cache.

After processing in this way, the impact of changes in defaultUserAgent is minimized. The system WebView upgrade itself is extremely infrequent. In this case, we discarded the first few network requests the next time the App is opened. The correctness of defaultUserAgent is also reasonable. This is also a classic case for us to consider the “risk-benefit ratio”.

5. Confirm that the problem is resolved

Through the above hook, we repackage and run the app, and the relevant time-consuming is no longer observed during the startup phase.

Get it done and call it a day. Not only is it efficient to solve the problem, but it is also efficient to write a blog. It is completed in a while. It is like a product before the quarterly performance appraisal. The efficiency of formulating the plan and launching it is just one word, whoosh.

In order to help everyone understand performance optimization more comprehensively and clearly, we have prepared relevant core notes (including underlying logic): https://qr18.cn/FVlo89

Core notes on performance optimization: https://qr18.cn/FVlo89

Start optimization

Memory optimization

UI optimization

Web Optimization

Bitmap optimization and image compression optimization: https://qr18.cn/FVlo89

Multi-thread concurrency optimization and data transmission efficiency optimization

Volume package optimization

“Android Performance Monitoring Framework”: https://qr18.cn/FVlo89

“Android Framework Learning Manual”: https://qr18.cn/AQpN4J

  1. Boot Init process
  2. Start the Zygote process on boot
  3. Start the SystemServer process on boot
  4. Binder driver
  5. AMS startup process
  6. PMS startup process
  7. Launcher startup process
  8. Android four major components
  9. Android system service – Distribution process of Input event
  10. Android underlying rendering – screen refresh mechanism source code analysis
  11. Android source code analysis in practice

syntaxbug.com © 2021 All Rights Reserved.