因為最近有個需求是在系統(tǒng)應用中使用 WebView崭孤,所以配置了 android:sharedUserId="android.uid.system"
, 讓應用共享系統(tǒng)進程沛膳。但是測試的時候就 crash 了褐鸥,我表示有點方...錯誤日志是這樣的:
java.lang.UnsupportedOperationException: For security reasons, WebView is not allowed in privileged processes
at android.webkit.WebViewFactory.getProvider(WebViewFactory.java:96)
at android.webkit.WebView.getFactory(WebView.java:2194)
at android.webkit.WebView.ensureProviderCreated(WebView.java:2189)
at android.webkit.WebView.setOverScrollMode(WebView.java:2248)
at android.view.View.<init>(View.java:3588)
at android.view.View.<init>(View.java:3682)
at android.view.ViewGroup.<init>(ViewGroup.java:497)
at android.widget.AbsoluteLayout.<init>(AbsoluteLayout.java:55)
at android.webkit.WebView.<init>(WebView.java:544)
at android.webkit.WebView.<init>(WebView.java:489)
at android.webkit.WebView.<init>(WebView.java:472)
at android.webkit.WebView.<init>(WebView.java:459)
at android.webkit.WebView.<init>(WebView.java:449)
就是說為了安全性考慮腰鬼,不允許在享有特權的進程也就是系統(tǒng)進程里面使用 WebView狠毯,異常是在 WebView 初始化的時候拋出的护糖,想要解決這個問題還要看源碼(Read the fucking source code)。
這是 Android 5.1(API 22) 里面的類 WebViewFactory 的 getProvider 方法源碼:
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) {
throw new UnsupportedOperationException(
"For security reasons, WebView is not allowed in privileged processes");
}
Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getProvider()");
try {
Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.loadNativeLibrary()");
loadNativeLibrary();
Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
Class<WebViewFactoryProvider> providerClass;
Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getFactoryClass()");
try {
providerClass = getFactoryClass();
} catch (ClassNotFoundException e) {
Log.e(LOGTAG, "error loading provider", e);
throw new AndroidRuntimeException(e);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
}
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "providerClass.newInstance()");
try {
try {
sProviderInstance = providerClass.getConstructor(WebViewDelegate.class)
.newInstance(new WebViewDelegate());
} catch (Exception e) {
sProviderInstance = providerClass.newInstance();
}
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);
StrictMode.setThreadPolicy(oldPolicy);
}
} finally {
Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
}
}
}
可以看出嚼松,首次使用時,系統(tǒng)會進行檢查锰扶,如果 UID 是 root 進程或者系統(tǒng)進程献酗,直接拋出異常。sProviderInstance 是 WebViewFactoryProvider 的對象坷牛,主要提供創(chuàng)建 WebView 內(nèi)核的機制罕偎。WebView在 Android 4.4 之前使用的是 Webkit 內(nèi)核,在 Android 4.4 以后切換到了 Chromium 內(nèi)核京闰。Google 使用了工廠方法模式颜及,優(yōu)雅地切換 WebView 內(nèi)核的實現(xiàn)方式甩苛。我們注意到只有 sProviderInstance 為空的時候系統(tǒng)才去檢查進程,然后創(chuàng)建 sProviderInstance對象俏站。所以這給了我們一個啟發(fā) ---- 能不能一開始就主動創(chuàng)建 sProviderInstance 對象讯蒲,把她塞到 WebViewFactory 類里面,從而欺騙 API 繞過系統(tǒng)檢查呢肄扎?
下面就要用到 Hook 的思想了墨林,首先要找到一個合適的點,靜態(tài)變量犯祠、單例是最佳選擇旭等,剛剛好 sProviderInstance 是靜態(tài)的。那就開始拿它開刀衡载,看看系統(tǒng)是怎么創(chuàng)建 sProviderInstance 的搔耕,我們自己也模仿它這么做。其實系統(tǒng)也是通過反射來做的痰娱,這是 getFactoryClass 的源碼弃榨,我們來看看。
private static Class<WebViewFactoryProvider> getFactoryClass() throws ClassNotFoundException {
Application initialApplication = AppGlobals.getInitialApplication();
try {
// First fetch the package info so we can log the webview package version.
String packageName = getWebViewPackageName();
sPackageInfo = initialApplication.getPackageManager().getPackageInfo(packageName, 0);
Log.i(LOGTAG, "Loading " + packageName + " version " + sPackageInfo.versionName +
" (code " + sPackageInfo.versionCode + ")");
// Construct a package context to load the Java code into the current app.
Context webViewContext = initialApplication.createPackageContext(packageName,
Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
initialApplication.getAssets().addAssetPath(
webViewContext.getApplicationInfo().sourceDir);
ClassLoader clazzLoader = webViewContext.getClassLoader();
Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "Class.forName()");
try {
return (Class<WebViewFactoryProvider>) Class.forName(CHROMIUM_WEBVIEW_FACTORY, true,
clazzLoader);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
}
} catch (PackageManager.NameNotFoundException e) {
// If the package doesn't exist, then try loading the null WebView instead.
// If that succeeds, then this is a device without WebView support; if it fails then
// swallow the failure, complain that the real WebView is missing and rethrow the
// original exception.
try {
return (Class<WebViewFactoryProvider>) Class.forName(NULL_WEBVIEW_FACTORY);
} catch (ClassNotFoundException e2) {
// Ignore.
}
Log.e(LOGTAG, "Chromium WebView package does not exist", e);
throw new AndroidRuntimeException(e);
}
}
返回值是一個 WebViewFactoryProvider 的類猜揪,可以看到系統(tǒng)會首先加載 CHROMIUM_WEBVIEW_FACTORY惭墓,也就是使用 Chrome 內(nèi)核的 WebView。這個方法是靜態(tài)的而姐,我們就可以用反射調(diào)用了腊凶。整個創(chuàng)建 sProviderInstance 的過程都可以用反射搞定,其他細節(jié)就不多說了拴念。需要注意的是 API 21 以上在使用 WebView 時系統(tǒng)才會檢查進程钧萍。但是 API 22 和 22 以上源碼還是有差別,這里只是方法名字的改動政鼠,我們根據(jù)版本處理一下就好风瘦。
public static void hookWebView() {
int sdkInt = Build.VERSION.SDK_INT;
try {
Class<?> factoryClass = Class.forName("android.webkit.WebViewFactory");
Field field = factoryClass.getDeclaredField("sProviderInstance");
field.setAccessible(true);
Object sProviderInstance = field.get(null);
if (sProviderInstance != null) {
log.debug("sProviderInstance isn't null");
return;
}
Method getProviderClassMethod;
if (sdkInt > 22) {
getProviderClassMethod = factoryClass.getDeclaredMethod("getProviderClass");
} else if (sdkInt == 22) {
getProviderClassMethod = factoryClass.getDeclaredMethod("getFactoryClass");
} else {
log.info("Don't need to Hook WebView");
return;
}
getProviderClassMethod.setAccessible(true);
Class<?> providerClass = (Class<?>) getProviderClassMethod.invoke(factoryClass);
Class<?> delegateClass = Class.forName("android.webkit.WebViewDelegate");
Constructor<?> providerConstructor = providerClass.getConstructor(delegateClass);
if (providerConstructor != null) {
providerConstructor.setAccessible(true);
Constructor<?> declaredConstructor = delegateClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
sProviderInstance = providerConstructor.newInstance(declaredConstructor.newInstance());
log.debug("sProviderInstance:{}", sProviderInstance);
field.set("sProviderInstance", sProviderInstance);
}
log.debug("Hook done!");
} catch (Throwable e) {
log.error(e);
}
}
在使用 WebView 之前,我們先 Hook WebViewFactory公般,創(chuàng)建 sProviderInstance 對象万搔,從而繞過系統(tǒng)檢查。經(jīng)過測試官帘,該方案完美解決了我們的問題 ~