【工作总结】系统签名app运行webview(Android5.0+)闪退问题

image.png

系统签名app运行webview(Android5.0+)闪退问题


前言:设置 -> 帮助中心采用加载本地文件显示device常见问题解答。
需求 update :不同的项目显示不同内容,后台获取显示内容

实现分析:

  • webView 实现

webView 简单使用参考:
Android开发:最全面、最易懂的Webview使用详解

开发平台

Android 4.4 项目平台开发,如期开发完成,开发效果如下:

device-2017-06-29-143438.png-1407.5kB
device-2017-06-29-143438.png-1407.5kB

适配致命问题

其他项目平台(Android 6.0)运行,直接闪退,打印如下:


image_1bjp9lvav1cb31hvc1pl21oo912dul.png-94.1kB
image_1bjp9lvav1cb31hvc1pl21oo912dul.png-94.1kB

【问题分析】:

  • Android 4.4 平台正常运行,Android 6.0 提示 XMl中 WebView 加载失败,难道Android 6.0对于webview中格式要求严格些(以前遇到过类似问题)?

【尝试解决方案】:

  • 加载WebView xml布局检查与分析,加载xml 中 WebView 照样失败;
  • 既然加载 xml 中 webview失败,同事建议采用Java 代码动态加载webview,无需加载xml 中 WebView 控件,就不会出现上述闪退打印错误(曲线救国,心中泛起了一丝笑容)

测试Java 动态加载webview ,Android 6.0 运行,继续闪退,串口查询错误日志:

 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 不允许运行在特许进程(系统进程),纳闷了,webview 凭什么不允许运行在系统进程,Android 4.4 运行的不是好好的吗?

寻求万能 Google 与 stackoverflow Help,在查阅到很多人遇到了此问题,分析apk 使用了android:sharedUserId="android.uid.system"申请了系统权限,去掉即可解决问题,(没有解释原因)自己尝试写了一个Demo,确实能规避问题。
提取Google分析出来的解决方案:

  1. 去掉android:sharedUserId="android.uid.system"
  2. 单独写一个apk 运行webview

设置很多操作需要使用系统进程调用,第一种方案不能采用,与产品经理协商,把设置中帮助中心模块单独抽取为单独apk,测试ok

虽然实现了功能,每次出设置需要出2个apk,麻烦。去掉申请系统权限可以正常运行webview原因不知道,利用闲余时间查Google与查看webview源码,WebViewFactory getProvider() 方法中异常打印正是闪退异常打印:

image_1bjpirdfqe3g1p2p1ihh1j2dl9412.png-33.3kB
image_1bjpirdfqe3g1p2p1ihh1j2dl9412.png-33.3kB

源码可以看出,首次使用webview时,系统会检测uid == android.os.Process.ROOT_UID || uid == android.os.Process.SYSTEM_UIDuid 为系统进程或者root进程,直接抛出异常。

为什么会有这种安全机制呢?因为webview允许运行js,如果用户通过js注入安全代码,那么js就可以肆无忌惮的使用系统权限,这无疑是一个漏洞,可谓门户大开。

联想到在framework层去掉加载webview时候对系统进程与root进程检测判断,(实现较为复杂,每个项目都需去掉,且不知是否会引出其他异常,没有验证,后续有时间去验证。。。)

抛出异常之前会检测sProviderInstance是否为空:
if (sProviderInstance != null) return sProviderInstance;
sProviderInstance 是WebViewFactoryProvider 对象,提供创建webview内核机制。WebView Android 4.4 之前使用内核webkit,Android 5.0 之后使用chromium内核,Google 使用了工厂方法模式,动态切换内核实现方式。能否一开始就主动创建sProviderInstance,放到WebViewFactory中,从而达到欺骗API 绕过系统检查?

联想到Hook思想:
Hook 英文翻译过来就是「钩子」的意思,那我们在什么时候使用这个「钩子」呢?在 Android 操作系统中系统维护着自己的一套事件分发机制。应用程序,包括应用触发事件和后台逻辑处理,也是根据事件流程一步步地向下执行。而「钩子」的意思,就是在事件传送到终点前截获并监控事件的传输,像个钩子钩上事件一样,并且能够在钩上事件时,处理一些自己特定的事件。

image_1bjpldukg17dj1i3u1hc61elr1mou1f.png-15.5kB
image_1bjpldukg17dj1i3u1hc61elr1mou1f.png-15.5kB

参考:Hook技术及其简单实战

系统创建sProviderInstance,系统使用getProviderClass()创建,利用反射:

private static Class<WebViewFactoryProvider> getProviderClass() {
        Context webViewContext = null;
        Application initialApplication = AppGlobals.getInitialApplication();

        try {
            Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW,
                    "WebViewFactory.getWebViewContextAndSetProvider()");
            try {
                webViewContext = getWebViewContextAndSetProvider();
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
            }
            Log.i(LOGTAG, "Loading " + sPackageInfo.packageName + " version " +
                    sPackageInfo.versionName + " (code " + sPackageInfo.versionCode + ")");

            Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getChromiumProviderClass()");
            try {
                initialApplication.getAssets().addAssetPathAsSharedLibrary(
                        webViewContext.getApplicationInfo().sourceDir);
                ClassLoader clazzLoader = webViewContext.getClassLoader();

                Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.loadNativeLibrary()");
                loadNativeLibrary(clazzLoader);
                Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);

                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 (ClassNotFoundException e) {
                Log.e(LOGTAG, "error loading provider", e);
                throw new AndroidRuntimeException(e);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
            }
        } catch (MissingWebViewPackageException 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 的过程都可以用反射搞定,自己写一个HookwebView 创建sProviderInstance;

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.d(TAG,"sProviderInstance isn't null");
                return;
            }
            Method getProviderClassMethod;
            if (sdkInt > 22) {
                getProviderClassMethod = factoryClass.getDeclaredMethod("getProviderClass");
            } else if (sdkInt == 22) {
                getProviderClassMethod = factoryClass.getDeclaredMethod("getFactoryClass");
            } else {
                Log.d(TAG,"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());
                field.set("sProviderInstance", sProviderInstance);
            }
            Log.d(TAG,"Hook done!");
        } catch (Throwable e) {
        }
    }

在使用WebView 即setContentView()之前调用hookWebView()方法,先Hook WebViewFactory,创建 sProviderInstance 对象,从而绕过系统检查。经过测试,该方案完美解决了在高版本系统中运行系统签名的webview闪退问题;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 173,602评论 25 708
  • 我们认识十年了。 生活不止有眼前的苟且,还有前任的喜帖。大红色的喜帖格外的刺眼,一点也不亚于今天的32°大...
    糖炒栗子哦阅读 211评论 0 1
  • 跟丈夫吵架,他的脾气很爆裂,常常像是间接性的神经病,隔段时间就会发作一次,把我说的极为不堪。 每次都哭到哽咽,起初...
    子川vs玄冰阅读 116评论 2 0
  • 《春夏秋冬》四部曲之《冬》 帝都168年9月,一场蓄谋已久的战争拉开序幕,战火连绵三个月,硝烟弥漫整个帝都...
    橙子Cat阅读 321评论 2 4