最近在搞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使用就没问题了
以上只是自己的猜想,具体的源码部分也没有细致的深入去看,希望有懂行的能在评论区告知