解決Android WebView 執行在系統程序引發的異常
因為最近有個需求是在系統應用中使用 WebView,所以配置了 android:sharedUserId="android.uid.system", 讓應用共享系統程序。但是測試的時候就 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)
就是說為了安全性考慮,不允許在享有特權的程序也就是系統程序裡面使用 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); } } }
可以看出,首次使用時,系統會進行檢查,如果 UID 是 root 程序或者系統程序,直接丟擲異常。sProviderInstance 是 WebViewFactoryProvider 的物件,主要提供建立 WebView 核心的機制。WebView在 Android 4.4 之前使用的是 Webkit 核心,在 Android 4.4 以後切換到了 Chromium 核心。Google 使用了工廠方法模式,優雅地切換 WebView 核心的實現方式。我們注意到只有 sProviderInstance 為空的時候系統才去檢查程序,然後建立 sProviderInstance物件。所以這給了我們一個啟發 ---- 能不能一開始就主動建立 sProviderInstance 物件,把她塞到 WebViewFactory 類裡面,從而欺騙 API 繞過系統檢查呢?
下面就要用到 Hook 的思想了,首先要找到一個合適的點,靜態變數、單例是最佳選擇,剛剛好 sProviderInstance 是靜態的。那就開始拿它開刀,看看系統是怎麼建立 sProviderInstance 的,我們自己也模仿它這麼做。其實系統也是通過反射來做的,這是 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 的類,可以看到系統會首先載入 CHROMIUM_WEBVIEW_FACTORY,也就是使用 Chrome 核心的 WebView。這個方法是靜態的,我們就可以用反射呼叫了。整個建立 sProviderInstance 的過程都可以用反射搞定,其他細節就不多說了。需要注意的是 API 21 以上在使用 WebView 時系統才會檢查程序。但是 API 22 和 22 以上原始碼還是有差別,這裡只是方法名字的改動,我們根據版本處理一下就好。
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,建立 sProviderInstance 物件,從而繞過系統檢查。經過測試,該方案完美解決了我們的問題 ~
【附錄】

資料圖
需要資料的朋友可以加入Android架構交流QQ群聊:513088520
點選連結加入群聊【Android移動架構總群】: 加入群聊
獲取免費學習視訊,學習大綱另外還有像高階UI、效能優化、架構師課程、NDK、混合式開發(ReactNative+Weex)等Android高階開發資料免費分享。