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
- Boot Init process
- Start the Zygote process on boot
- Start the SystemServer process on boot
- Binder driver
- AMS startup process
- PMS startup process
- Launcher startup process
- Android four major components
- Android system service – Distribution process of Input event
- Android underlying rendering – screen refresh mechanism source code analysis
- Android source code analysis in practice