Android N 替换context的mResources后引起WebView崩溃的问题

最近在搞Android的换肤功能

开发初始就希望换肤框架对业务代码有较低的侵入性

因此决定使用对Context的mResources替换的方式,该方式的优点就是原先的代码逻辑无需太多的修改

但是在开发过程中发现Android N 以上的版本在对Context的mResources进行替换后会出现使用WebView的页面在某些场景下会崩溃,比如长按WebView 弹出复制菜单的时候

堆栈日志如下

2019-04-30 12:58:50.123 7660-7660/com.xxxx.xxxx E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.xxxx.xxxx, PID: 7660
    android.content.res.Resources$NotFoundException: Resource ID #0x20c0020
        at android.content.res.ResourcesImpl.getValue(ResourcesImpl.java:245)
        at android.content.res.Resources.getInteger(Resources.java:1167)
        at org.chromium.ui.base.DeviceFormFactor.a(PG:8)
        at org.chromium.content.browser.selection.SelectionPopupControllerImpl.a(PG:212)
        at aAO.onCreateActionMode(PG:15)
        at coj.onCreateActionMode(PG:6)
        at com.android.internal.policy.DecorView$ActionModeCallback2Wrapper.onCreateActionMode(DecorView.java:3092)
        at com.android.internal.policy.DecorView.startActionMode(DecorView.java:1356)
        at com.android.internal.policy.DecorView.startActionModeForChild(DecorView.java:1312)
        at android.view.ViewGroup.startActionModeForChild(ViewGroup.java:996)
        at android.view.ViewGroup.startActionModeForChild(ViewGroup.java:996)
        at android.view.ViewGroup.startActionModeForChild(ViewGroup.java:996)
        at android.view.ViewGroup.startActionModeForChild(ViewGroup.java:996)
        at android.view.View.startActionMode(View.java:6825)
        at clc.a(PG:1)
        at org.chromium.content.browser.selection.SelectionPopupControllerImpl.g(PG:128)
        at cpl.a(PG:31)
        at coG.a(PG:14)
        at aHi.run(Unknown Source:7)
        at android.os.Handler.handleCallback(Handler.java:873)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:224)
        at android.app.ActivityThread.main(ActivityThread.java:7092)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:876)

通过关键字 基本上确认是webView获取资源时抛出异常

通过参考如下两篇资料

https://cloud.tencent.com/developer/article/1358038

http://www.demonk.cn/2018/07/26/webview-in-plugin/

大致了解到是因为webView的资源路径引起的问题

将资源路径添加至AssertManager就可以了

代码如下(获取webview资源路径的部分来自RePlugin

/*
 * Copyright (C) 2005-2017 Qihoo 360 Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed To in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package com.vivo.website.core.utils.textrepair;

import java.lang.reflect.Method;
import java.util.Objects;

import android.app.ResourcesManager;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;

public class WebViewResourceHelper {
    
    private static final String TAG = "WebViewResourceHelper";
    
    private static boolean sInitialed = false;
    
    /**
     * Adds the WebView asset path to {@link android.content.res.AssetManager}.
     */
    public static void addWebViewAssetPath(Context context) {
        if (sInitialed) {
            return;
        }
        try {
            final String newAssetPath = getWebViewResourceDir(context);
            // 6.0
            if (Build.VERSION.SDK_INT <= 23) {
                Reflector.with(context.getAssets()).method("addAssetPath", String.class).call(newAssetPath);
            } else {
                final ApplicationInfo appInfo = context.getApplicationInfo();
                final String[] libs = appInfo.sharedLibraryFiles;
                if (!contains(libs, newAssetPath)) {
                    // Build the new library asset path list.
                    final int newLibAssetsCount = 1 + (libs != null ? libs.length : 0);
                    final String[] newLibAssets = new String[newLibAssetsCount];
                    if (libs != null) {
                        System.arraycopy(libs, 0, newLibAssets, 0, libs.length);
                    }
                    newLibAssets[newLibAssetsCount - 1] = newAssetPath;
                    
                    // Update the ApplicationInfo object with the new list.
                    // We know this will persist and future Resources created via ResourcesManager
                    // will include the shared library because this ApplicationInfo comes from the
                    // underlying LoadedApk in ContextImpl, which does not change during the life of the
                    // application.
                    appInfo.sharedLibraryFiles = newLibAssets;
                    // Update existing Resources with the WebView library.
                    String baseResourcePath = Reflector.with(appInfo).method("getBaseResourcePath").call();
                    ResourcesManager.getInstance().appendLibAssetForMainAssetPath(baseResourcePath, newAssetPath);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        sInitialed = true;
    }
    
    private static String getWebViewResourceDir(Context context) {
        String pkgName = getWebViewPackageName();
        if (TextUtils.isEmpty(pkgName)) {
            return null;
        }
        
        try {
            PackageInfo pi = context.getPackageManager().getPackageInfo(getWebViewPackageName(),
                    PackageManager.GET_SHARED_LIBRARY_FILES);
            return pi.applicationInfo.sourceDir;
        } catch (PackageManager.NameNotFoundException e) {
            Log.e(TAG, "get webview application info failed! ", e);
        } catch (Exception e) {
            // Do Nothing
        }
        
        return null;
    }
    
    private static String getWebViewPackageName() {
        int sdk = Build.VERSION.SDK_INT;
        
        if (sdk <= 20) {
            return null;
        }
        
        switch (sdk) {
        case 21:
        case 22:
            return getWebViewPackageName4Lollipop();
        case 23:
            return getWebViewPackageName4M();
        case 24:
            return getWebViewPackageName4N();
        case 25:
        default:
            return getWebViewPackageName4More();
        }
    }
    
    private static String getWebViewPackageName4Lollipop() {
        try {
            return (String) invokeStaticMethod("android.webkit.WebViewFactory", "getWebViewPackageName", null);
        } catch (Throwable e) {
            //
        }
        return "com.google.android.webview";
    }
    
    private static String getWebViewPackageName4M() {
        return getWebViewPackageName4Lollipop();
    }
    
    private static String getWebViewPackageName4N() {
        try {
            Context c = (Context) invokeStaticMethod("android.webkit.WebViewFactory", "getWebViewContextAndSetProvider",
                    null);
            return c.getApplicationInfo().packageName;
        } catch (Throwable e) {
            //
        }
        return "com.google.android.webview";
    }
    
    private static String getWebViewPackageName4More() {
        return getWebViewPackageName4N();
    }
    
    private static Object invokeStaticMethod(String clzName, String methodName, Class<?>[] methodParamTypes,
            Object... methodParamValues) {
        try {
            Class clz = Class.forName(clzName);
            if (clz != null) {
                Method med = clz.getDeclaredMethod(methodName, methodParamTypes);
                if (med != null) {
                    med.setAccessible(true);
                    Object retObj = med.invoke(null, methodParamValues);
                    return retObj;
                }
            }
        } catch (Exception e) {
            Log.e(TAG, "invokeStaticMethod got Exception:", e);
        }
        return null;
    }
    
    /**
     * Checks that value is present as at least one of the elements of the array.
     *
     * @param array
     *            the array to check in
     * @param value
     *            the value to check for
     * @return true if the value is present in the array
     */
    private static <T> boolean contains(@Nullable T[] array, T value) {
        return indexOf(array, value) != -1;
    }
    
    /**
     * Return first index of {@code value} in {@code array}, or {@code -1} if
     * not found.
     */
    private static <T> int indexOf(@Nullable T[] array, T value) {
        if (array == null) {
            return -1;
        }
        for (int i = 0; i < array.length; i++) {
            if (Objects.equals(array[i], value)) {
                return i;
            }
        }
        return -1;
    }
}

在替换mResources之前先调用addWebViewAssetPath方法将AssertMnagaer填充好就可以了

ResourceManager是系统api 且是hide状态,默认情况下是不可以调用的,但是可以通过AndroidStub方式进行调用,具体方式参考https://blog.csdn.net/binghelonglong123/article/details/88790488

Reflector 是一个反射调用的工具,不用它也可以,这里就不提供源码了

 

这里重点说一下不明白的地方

问题:在webView初始化的时候webView会对系统的Resource添加webView资源路径,那为啥我们使用自己new出来的Resource替换了mResource就不行了呢。即使newResource使用的是context.getResource().getAssert()也不行。

猜想:Resource对象由ResourceManager统一托管,虽然使用原Resource的AssertManager创建了新的Resource,但是该Resource并不在ResourceManager统一托管的范围内,webView添加assertpath后并没有同步到我们的newResource,但是在创建newResource之前先给系统Resource 添加webView的path 再使用系统Resource的AssertManager创建了新的Resource使用就没问题了

以上只是自己的猜想,具体的源码部分也没有细致的深入去看,希望有懂行的能在评论区告知

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值