WebViewFactory.getProvider耗时问题的一种解决思路

背景

最近在做外部拉起视频底层页整个链路上的一个耗时优化,发现在这个过程中有一个地方挺耗时的,如下所示:
trace
cost

可以看到这个WebViewFactory.getProvider()方法耗时了240ms!

找到WebViewFactory的源码如下所示:

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
static WebViewFactoryProvider getProvider() {
synchronized (sProviderLock) {
// For now the main purpose of this function (and the factory abstraction) is to keep
// us honest and minimize usage of WebView internals when binding the proxy.
if (sProviderInstance != null) return sProviderInstance;

final int uid = android.os.Process.myUid();
if (uid == android.os.Process.ROOT_UID || uid == android.os.Process.SYSTEM_UID
|| uid == android.os.Process.PHONE_UID || uid == android.os.Process.NFC_UID
|| uid == android.os.Process.BLUETOOTH_UID) {
throw new UnsupportedOperationException(
"For security reasons, WebView is not allowed in privileged processes");
}

if (!isWebViewSupported()) {
// Device doesn't support WebView; don't try to load it, just throw.
throw new UnsupportedOperationException();
}

if (sWebViewDisabled) {
throw new IllegalStateException(
"WebView.disableWebView() was called: WebView is disabled");
}

Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getProvider()");
try {
Class<WebViewFactoryProvider> providerClass = getProviderClass();
Method staticFactory = null;
try {
staticFactory = providerClass.getMethod(
CHROMIUM_WEBVIEW_FACTORY_METHOD, WebViewDelegate.class);
} catch (Exception e) {
if (DEBUG) {
Log.w(LOGTAG, "error instantiating provider with static factory method", e);
}
}

Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactoryProvider invocation");
try {
sProviderInstance = (WebViewFactoryProvider)
staticFactory.invoke(null, new WebViewDelegate());
if (DEBUG) Log.v(LOGTAG, "Loaded provider: " + sProviderInstance);
return sProviderInstance;
} catch (Exception e) {
Log.e(LOGTAG, "error instantiating provider", e);
throw new AndroidRuntimeException(e);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
}
} finally {
Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
}
}
}

从代码可以看到这个getProvider里面有做处理:如果已经获取到了WebViewFactoryProvider就直接返回,也就是说只有第一次才会往下走,才会耗时。并且从WebViewFactory注释来看:

1
Top level factory, used creating all the main WebView implementation classes.

这个WebViewFactory应该是跟WebView相关的,WebView的相关操作最终都会走到这个WebViewFactroy里面来。如果是这样的话,那我们能不能在App启动的时候在子线程提前调用这个getProvider方法来初始化一下这个WebViewFactoryProvider,并且这个getProvider是线程安全的方法。这样UI线程真正使用WebView的时候,这个getProvider方法就能直接返回了?答案是可以的,但是这个getProvider并不是一个public方法,只能通过反射调用了,代码如下所示:

解决方案

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
public class WebViewHookManager {
private static final String TAG = "WebViewHookManager";

/**
* 提前在子线程里面调用WebViewFactory.getProvider方法,解决第一次调用耗时问题
*
* @param isMainProcess 是否是主进程
*/
public static void initWebViewGetProvider(boolean isMainProcess) {
if (isMainProcess) {
ThreadManager.getInstance().post(() -> {
try {
@SuppressLint("PrivateApi")
Class<?> webViewFactoryClass = Class.forName("android.webkit.WebViewFactory");
@SuppressLint("DiscouragedPrivateApi")
Method method = webViewFactoryClass.getDeclaredMethod("getProvider");
method.setAccessible(true);
method.invoke(webViewFactoryClass);
} catch (Exception e) {
QQLiveLog.e(TAG, e, "WebViewFactory.getProvider");
}
});
}
}
}

遇到的问题

理论上我们在App启动的时候调用一下上述initWebViewGetProvider方法就可以了,但是实际上并不是如此。这个方法调用时机是有讲究的,不能放在Application.attachBaseContext方法里面调用,原因我们得从源码来查找答案。

在getProvider方法里面调用了一个方法isWebViewSupported:

1
2
3
4
5
6
7
8
9
private static boolean isWebViewSupported() {
// No lock; this is a benign race as Boolean's state is final and the PackageManager call
// will always return the same value.
if (sWebViewSupported == null) {
sWebViewSupported = AppGlobals.getInitialApplication().getPackageManager()
.hasSystemFeature(PackageManager.FEATURE_WEBVIEW);
}
return sWebViewSupported;
}

在这个方法里面会通过AppGlobals.getInitialApplication()来获取到一个Application,全局搜索AppGlobals这个类来看下这个方法实现:

1
2
3
4
5
6
7
/**
* Return the first Application object made in the process.
* NOTE: Only works on the main thread.
*/
public static Application getInitialApplication() {
return ActivityThread.currentApplication();
}

可以看到这个方法调用了 ActivityThread.currentApplication()方法,继续来看下这个方法:

1
2
3
4
5
6
7
Application mInitialApplication;


public static Application currentApplication() {
ActivityThread am = currentActivityThread();
return am != null ? am.mInitialApplication : null;
}

可以看到这个currentApplication返回的是ActivityThread里面的成员变量mInitialApplication,继续来看下这个mInitialApplication成员变量赋值的地方,在ActivityThread的handleBindApplication方法里面有如下代码片段:

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
try {
// If the app is being launched for full backup or restore, bring it up in
// a restricted environment with the base application class.
app = data.info.makeApplication(data.restrictedBackupMode, null);

// Propagate autofill compat state
app.setAutofillCompatibilityEnabled(data.autofillCompatibilityEnabled);

mInitialApplication = app;

// don't bring up providers in restricted mode; they may depend on the
// app's custom Application class
if (!data.restrictedBackupMode) {
if (!ArrayUtils.isEmpty(data.providers)) {
installContentProviders(app, data.providers);
// For process that contains content providers, we want to
// ensure that the JIT is enabled "at some point".
mH.sendEmptyMessageDelayed(H.ENABLE_JIT, 10*1000);
}
}

// Do this after providers, since instrumentation tests generally start their
// test thread at this point, and we don't want that racing.
try {
mInstrumentation.onCreate(data.instrumentationArgs);
}
catch (Exception e) {
throw new RuntimeException(
"Exception thrown in onCreate() of "
+ data.instrumentationName + ": " + e.toString(), e);
}
try {
mInstrumentation.callApplicationOnCreate(app);
} catch (Exception e) {
if (!mInstrumentation.onException(app, e)) {
throw new RuntimeException(
"Unable to create application " + app.getClass().getName()
+ ": " + e.toString(), e);
}
}
}

这段代码主要分为3部分:
1、app = data.info.makeApplication(data.restrictedBackupMode, null)这句代码里面就会去创建一个Application,并且在里面会调用Application的attachBaseContext方法。执行完这个makeApplication方法之后,这个mInitialApplication才会进行赋值,因此不能在attachBaseContext就去调用WebViewFactory.getProvider方法,因为这个时候ActivityThread里面的mInitialApplication还没有初始化
2、installContentProviders方法会挨个执行ContentProvider的onCreate方法
3、最后mInstrumentation.callApplicationOnCreate(app)会执行Application的onCreate方法,因此我们可以在Application的onCreate方法里面调用WebViewFactory.getProvider方法

从上述代码可以看出这三步的一个执行顺序如下:
Application.attachBaseContext -> ContentProvider.onCreate -> Application.onCreate

效果

通过日志观察WebViewFactory的日志打印,可以看到代码已经执行成功了:
log

但是目前这个getProvider接口已经被列入了greylist里面了,在后续的版本不敢保证还能继续生效,这里只是提供一种解决问题的思路
greylist

优化之后的效果如下所示,可以看到UI线程WebViewFactory.getProvider()方法耗时已经没有了:
optimized