美文网首页Android技术知识Android开发Android进阶之路
解决Android WebView 运行在系统进程引发的异常

解决Android WebView 运行在系统进程引发的异常

作者: Android架构 | 来源:发表于2019-02-18 21:27 被阅读8次

    因为最近有个需求是在系统应用中使用 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高阶开发资料免费分享。

    相关文章

      网友评论

        本文标题:解决Android WebView 运行在系统进程引发的异常

        本文链接:https://www.haomeiwen.com/subject/juozeqtx.html