本文首發於 vivo互聯網技術 微信公衆號
連接: mp.weixin.qq.com/s/jG8rAjQ8Q…
做者:陳龍java
最近作的項目須要支持幾十種語言,不少小語種在不認識的人看來跟亂碼同樣,翻譯通常是由翻譯公司翻譯的,翻譯完成後再導入到項目裏面,這就容易存在一些問題。android
翻譯的流程是客戶端開發編寫中文文案---翻譯成英文----外包翻譯根據英文字符串翻譯小語種,在這個流程中,有些多義詞和一些涉及語境的詞就很容易翻譯錯誤。web
前面說了,翻譯公司提供回來的字符串咱們都看不懂,錯了也不知道,幾乎都是上線以後,用戶反饋過來,咱們才知道。chrome
所以小語種的翻譯bug一直是項目裏面比較多的一類bug,因而就須要探索一種能夠用於動態更新翻譯字符串的方案。apache
在Android中,多語言字符串都是以各類不一樣文件夾下的xml保存的,每種文件夾中的限定符表示一種語言,這個通常Android的開發人員都是瞭解的。api
以下圖所示瀏覽器
String文件做爲Resource的一種,在使用時無論是layout中使用仍是在java代碼中使用其實都是調用Resource的各類方法。緩存
那麼其實翻譯語言的動態更新其實是Resource資源的替換更新。bash
在早些年的開發經驗中,咱們都知道有一種Android換主題的方案來給應用進行資源替換,簡單來說方案流程以下:微信
使用addAssertPath方法加載sd卡中的apk包,構建AsserManager實例。
AsserManager構建PlugResource實例。
使用裝飾者模式編寫ProxyResource,在各個獲取資源的方法中優先獲取PlugResource,獲取不到再從備份的AppResource中獲取。
替換Application和Activity中的Resource對象爲ProxyResource。
繼承LayoutInflater.Factory,攔截layout生成過程,並將資源獲取指向ProxyResource,完成layout初始化。
既然有可參考的方案,那就能夠直接開工了。
事實上在後續的開發過程當中遇到不少細節問題,但萬事開頭難,咱們能夠先從第一步開始作起。
AssetManager mLoadedAssetManager = AssetManager.class.newInstance();
Reflector.with(mLoadedAssetManager).method("addAssetPath", String.class).call(textResPath);
Resources textResPackResources = new Resources(mLoadedAssetManager, appResources.getDisplayMetrics(), appResources.getConfiguration());複製代碼
public class TextRepairProxyResourcess extends Resources {
private static final String TAG = "TextRepairProxyResourcess";
private Resources mResPackResources;
private Resources mAppResources;
private String mResPackPkgName;
public TextRepairProxyResourcess(AssetManager assets, DisplayMetrics metrics, Configuration config) {
super(assets, metrics, config);
}
public void prepare(Resources plugResources, Resources appResources, String pkgName) {
mResPackResources = plugResources;
mAppResources = appResources;
mResPackPkgName = pkgName;
}
private void printLog(String tag, CharSequence messgae) {
if (BuildConfig.DEBUG) {
VLog.d(tag, messgae + "");
}
}
@NonNull
@Override
public CharSequence getText(int resId) throws NotFoundException {
if (!checkNull()) {
return super.getText(resId);
} else if (!checkTextRepairOn()) {
return mAppResources.getText(resId);
} else {
CharSequence charSequence;
try {
int plugId = getIdentifier(resId);
if (plugId == 0) {
charSequence = mAppResources.getText(resId);
printLog(TAG, "getText res from app ---" + charSequence);
} else {
charSequence = mResPackResources.getText(plugId);
printLog(TAG, "getText res from plug ---" + charSequence);
}
} catch (Throwable e) {
charSequence = mAppResources.getText(resId);
if (BuildConfig.DEBUG) {
e.printStackTrace();
}
}
return charSequence;
}
}
@NonNull
@Override
public CharSequence[] getTextArray(int resId) throws NotFoundException {
.............
}
@NonNull
@Override
public String[] getStringArray(int resId) throws NotFoundException {
.............
}
@NonNull
@Override
public String getString(int resId) throws NotFoundException {
.............
}
@NonNull
@Override
public CharSequence getQuantityText(int resId, int quantity) throws NotFoundException {
.............
}
@NonNull
@Override
public String getQuantityString(int resId, int quantity, Object... formatArgs) throws NotFoundException {
.............
}
public int getIdentifier(int resId) {
if (!checkNull()) {
return 0;
} else {
// 有些狀況就是很特殊 好比webView的34800147資源 使用mAppResources.getResourceEntryName會拋出
// notfound 異常 可是使用getString 卻又能夠拿到這個資源的字符串
try {
String resName = mAppResources.getResourceEntryName(resId);
String resType = mAppResources.getResourceTypeName(resId);
int plugId = mResPackResources.getIdentifier(resName, resType, mResPackPkgName);
return plugId;
} catch (Throwable e) {
return 0;
}
}
}
/**
* 有些方法是在super的構造方法裏面調用的 須要判空處理
*
* @return
*/
private boolean checkNull() {
if (mAppResources != null && mResPackResources != null) {
return true;
} else {
return false;
}
}
/**
* 有些方法是在super的構造方法裏面調用的 須要判空處理
*
* @return
*/
private boolean checkTextRepairOn() {
return TextRepairConfig.getInstance().isTextRepairOnThisSystem();
}
}複製代碼
Reflector.with(appContext).field("mResources").set(textRepairProxyResourcess);複製代碼
Reflector.with(activityContext).field("mResources").set(textRepairProxyResourcess);複製代碼
public class TextRepairFactory implements LayoutInflater.Factory2 {
private static final HashMap<String, Constructor<? extends View>> mConstructorMap = new HashMap<>();
/**
* 系統調用的是兩個參數的構造方法,咱們也調用這個構造方法
*/
private static final Class<?>[] mConstructorSignature = new Class[] { Context.class, AttributeSet.class };
/**
* 通常 Android 系統的 View 都存儲在這幾個包下面
*/
private final String[] a = new String[] { "android.widget.", "android.view.", "android.webkit." };
// 屬性處理類
TextRepairAttribute mTextRepairAttribute;
public TextRepairFactory() {
mTextRepairAttribute = new TextRepairAttribute();
}
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
/*
* 咱們模仿源碼那樣來建立 View
*/
View view = createViewFormTag(name, context, attrs);
/*
* 這裏若是 View 返回的是 null 的話,就是自定義控件,
* 自定義控件不須要咱們進行拼接,能夠直接拿到全類名
*/
if (view == null) {
view = createView(name, context, attrs);
}
if (view != null) {
mTextRepairAttribute.load(view, attrs);
}
return view;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
private View createView(String name, Context context, AttributeSet attrs) {
Constructor<? extends View> constructor = findConstructor(context, name);
try {
return constructor.newInstance(context, attrs);
} catch (Throwable e) {
}
return null;
}
private Constructor<? extends View> findConstructor(Context context, String name) {
Constructor<? extends View> constructor = mConstructorMap.get(name);
if (null == constructor) {
try {
// 經過反射來獲取 View 實例對象
Class<? extends View> clazz = context.getClassLoader().loadClass(name).asSubclass(View.class);
constructor = clazz.getConstructor(mConstructorSignature);
// 緩存View的class對象
mConstructorMap.put(name, constructor);
} catch (Throwable e) {
}
}
return constructor;
}
private View createViewFormTag(String name, Context context, AttributeSet attrs) {
// 包含自定義控件
if (-1 != name.indexOf('.')) {
return null;
}
View view = null;
for (int i = 0; i < a.length; i++) {
view = createView(a[i] + name, context, attrs);
if (view != null) {
break;
}
}
return view;
}
}複製代碼
public class TextRepairActivityLifecycle implements Application.ActivityLifecycleCallbacks {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
LayoutInflater layoutInflater = LayoutInflater.from(activity);
TextRepairFactory textRepairFactory = new TextRepairFactory();
LayoutInflaterCompat.setFactory2(layoutInflater, textRepairFactory);
}
}複製代碼
上述幾段代碼就已經構成了資源替換的雛形,基本上完成了一個基礎的資源替換流程。
再後續的調試點檢過程種,我發現這纔剛剛開始入坑。
demo一跑起來就發現log中打印諸多告警信息。
由於是使用反射的方法將Resource替換,所以也觸發了Google的Api限制調用機制,因而研究了一下Api的限制調用。
結論:
系統簽名應用暫時沒有限制,由於demo使用的是調試簽名,換用系統簽名以後,告警消失。
使用sd卡中的plugapk包生成PlugResources,主要是在生成assetManager過程,該過程耗時10-15ms,對於頁面啓動來講,這個時間仍是太長了,因而嘗試將AssetManager緩存起來,縮短了時間。
在反射替換resource完成後,調用PlugResources的getText方法,要先從本地Resources中根據Id獲取原資源的name和type,而後在使用name和type調用getIndentifier獲取PlugResources中的resId,這個過程耗時較長,雖然也是納秒級別的,但其耗時比不hook場景下高一個數據級。
然而幸運的是,在頁面流暢性性能測試中,並無發現流暢性有所降低,頁面啓動速度也沒有明顯的降低。
真正的大坑來了。
解決完以前的問題以後,開始進入monkey測試,在測試中發現7.0以上的機器,只要在webView界面長按內容彈出複製粘貼對話框,就會崩潰從日誌裏面能夠看出來是找不到webView的資源致使的,若是我try住這個崩潰,原資源位置顯示的字符串就會變成相似@1232432這種id標籤。
google搜索了半天,發現相關資料甚少,看來是須要從源碼層面瞭解webView資源加載的相關邏輯才行。
看源碼,老是須要帶着問題去看,目標纔夠清晰。
想要獲得答案 ,就得閱讀6.0和7.0以上的Resource源碼,先從6.0的源碼看起。
一、6.0資源管理源碼解析
Context初始化
private ContextImpl(ContextImpl container, ActivityThread mainThread,
LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,
Display display, Configuration overrideConfiguration, int createDisplayWithId) {
mOuterContext = this;
mMainThread = mainThread;
mActivityToken = activityToken;
mRestricted = restricted;
。。。。。。。。。。
Resources resources = packageInfo.getResources(mainThread);
if (resources != null) {
if (displayId != Display.DEFAULT_DISPLAY
|| overrideConfiguration != null
|| (compatInfo != null && compatInfo.applicationScale
!= resources.getCompatibilityInfo().applicationScale)) {
resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(),
packageInfo.getApplicationInfo().sharedLibraryFiles, displayId,
overrideConfiguration, compatInfo);
}
}
mResources = resources;
。。。。。。。。。。。
}複製代碼
這裏有兩個地方涉及到了Resource建立
resources =packageInfo.getResources(mainThread);
resources =mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
先從packageInfo.getResources(mainThread); 提及packageInfo 其實就是LoadedApk
packageInfo 的 getResources 方法
public Resources getResources(ActivityThread mainThread) {
if (mResources == null) {
mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, null, this);
}
return mResources;
}複製代碼
ActivityThread 的 getTopLevelResources 方法
Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs,
String[] libDirs, int displayId, Configuration overrideConfiguration,
LoadedApk pkgInfo) {
return mResourcesManager.getTopLevelResources(resDir, splitResDirs, overlayDirs, libDirs,
displayId, overrideConfiguration, pkgInfo.getCompatibilityInfo());複製代碼
Android M 的ResourcesManager寫的比較簡單
其內部有一個Resource緩存
getTopLevelResource 方法會使用傳入的參數 組裝一個key
ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfigCopy, scale);
使用這個key去緩存裏面找,找到了就拿出來用。
WeakReference<Resources> wr = mActiveResources.get(key);
找不到就新建立一個assets 來生成一個Resource實例
AssetManager assets = new AssetManager();
if (resDir != null) {
if (assets.addAssetPath(resDir) == 0) {
return null;
}
}
if (splitResDirs != null) {
for (String splitResDir : splitResDirs) {
if (assets.addAssetPath(splitResDir) == 0) {
return null;
}
}
}
if (overlayDirs != null) {
for (String idmapPath : overlayDirs) {
assets.addOverlayPath(idmapPath);
}
}
if (libDirs != null) {
for (String libDir : libDirs) {
if (libDir.endsWith(".apk")) {
// Avoid opening files we know do not have resources,
// like code-only .jar files.
if (assets.addAssetPath(libDir) == 0) {
Log.w(TAG, "Asset path '" + libDir +
"' does not exist or contains no resources.");
}
}
}
}複製代碼
而且調用這些Resource的public void updateConfiguration(Configuration config,DisplayMetrics metrics, CompatibilityInfo compat) {方法,最終生效的是對Resource中的mAssets的configuration
再來看一下Resource.java
其核心包含兩個部分
1:封裝Assets,講全部資源調用最終都是調用到mAssets的方法
public CharSequence getText(@StringRes int id) throws NotFoundException {
CharSequence res = mAssets.getResourceText(id);
if (res != null) {
return res;
}
throw new NotFoundException("String resource ID #0x" + Integer.toHexString(id));
}複製代碼
2:提供緩存
private static final LongSparseArray<ConstantState>[] sPreloadedDrawables;
private static final LongSparseArray<ConstantState> sPreloadedColorDrawables = new LongSparseArray<>();
private static final LongSparseArray<android.content.res.ConstantState<ColorStateList>> sPreloadedColorStateLists = new LongSparseArray<>();
private final DrawableCache mDrawableCache = new DrawableCache(this);
private final DrawableCache mColorDrawableCache = new DrawableCache(this);
private final ConfigurationBoundResourceCache<ColorStateList> mColorStateListCache = new ConfigurationBoundResourceCache<>(this);
private final ConfigurationBoundResourceCache<Animator> mAnimatorCache = new ConfigurationBoundResourceCache<>(this);
private final ConfigurationBoundResourceCache<StateListAnimator> mStateListAnimatorCache = new ConfigurationBoundResourceCache<>(this);
將從mAsserts中取出的大資源進行緩存,避免讀取耗時和內存佔用複製代碼
相比於Android6.0 ,9.0源碼中Resources中不在維護AssertManager 而是將AssertManager與其餘的一些緩存 封裝成了一個ResourcesImpl。
public class Resources {
static final String TAG = "Resources";
static Resources mSystem = null;
private ResourcesImpl mResourcesImpl;
private TypedValue mTmpValue = new TypedValue();
final ClassLoader mClassLoader;複製代碼
public class ResourcesImpl {
private static final LongSparseArray<Drawable.ConstantState>[] sPreloadedDrawables;
private static final LongSparseArray<Drawable.ConstantState> sPreloadedColorDrawables = new LongSparseArray<>();
private static final LongSparseArray<android.content.res.ConstantState<ComplexColor>> sPreloadedComplexColors = new LongSparseArray<>();
// These are protected by mAccessLock.
private final Configuration mTmpConfig = new Configuration();
private final DrawableCache mDrawableCache = new DrawableCache();
private final DrawableCache mColorDrawableCache = new DrawableCache();
private final ConfigurationBoundResourceCache<ComplexColor> mComplexColorCache = new ConfigurationBoundResourceCache<>();
private final ConfigurationBoundResourceCache<Animator> mAnimatorCache = new ConfigurationBoundResourceCache<>();
private final ConfigurationBoundResourceCache<StateListAnimator> mStateListAnimatorCache = new ConfigurationBoundResourceCache<>();
final AssetManager mAssets;
private final DisplayMetrics mMetrics = new DisplayMetrics();
private final DisplayAdjustments mDisplayAdjustments;
private PluralRules mPluralRule;
private final Configuration mConfiguration = new Configuration();
}複製代碼
ResourcesImpl 承擔着老版本里面Resources的職責, 包裝AssertManager 和 維護數據緩存。
而Resources的代碼也變的更加簡單,其方法調用最終都是交給了ResourcesImpl來實現。
不變的是Resources的管理仍是要交給ResourcesManager來管理的,跟Android6.0同樣ResourcesManager是一個單例模式。
那麼9.0的ResourcesManager與6.0的ResourcesManager有和不一樣?
仍是從應用啓動開始看起,仍是熟悉的ContextImpl。
二、9.0資源管理源碼解析
static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0, null);
context.setResources(packageInfo.getResources());
return context;
}
複製代碼
static ContextImpl createActivityContext(ActivityThread mainThread, LoadedApk packageInfo, ActivityInfo activityInfo, IBinder activityToken, int displayId, Configuration overrideConfiguration) {
。。。。。。。。
ContextImpl context = new ContextImpl(null, mainThread, packageInfo, activityInfo.splitName, activityToken, null, 0, classLoader);
final ResourcesManager resourcesManager = ResourcesManager.getInstance();
context.setResources(resourcesManager.createBaseActivityResources(activityToken, packageInfo.getResDir(), splitDirs, packageInfo.getOverlayDirs(), packageInfo.getApplicationInfo().sharedLibraryFiles, displayId, overrideConfiguration, compatInfo, classLoader));
context.mDisplay = resourcesManager.getAdjustedDisplay(displayId, context.getResources());
return context;
}複製代碼
不管是生成Application的Resource仍是生成Activity的Resource最終調用的是ResourceManager中的方法區別。在於一個調用的是
ResourcesManager.getInstance().getResources ,另外一個調用的是resourcesManager.createBaseActivityResources。
OK 咱們看一下ResourcesManager的源碼。
先看下它提供的各類屬性,咱們挑重要的放上來。
/**
* ResourceImpls及其配置的映射。這些都是佔用較大內存的數據
* 應該儘量重用。全部的由ResourcesManager生成的ResourcesImpl都會被緩存在這個map中
*/
private final ArrayMap<ResourcesKey, WeakReference<ResourcesImpl>> mResourceImpls = new ArrayMap<>();
/**
*能夠重用的資源引用列表。注意一下 這個list裏面存儲的並非Activity的Resources緩存,按照個人理解,全部非Activcity的Resource都會被緩存在此處,好比Application的Resource
*/
private final ArrayList<WeakReference<Resources>> mResourceReferences = new ArrayList<>();
/**
* 每一個Activity都有一個基本覆蓋配置,該配置應用於每一個Resources對象,而這些對象又能夠指定本身的覆蓋配置。
這個緩存裏面保存的都是Actrivity的Resource的緩存,ActivityResources是一個對象,裏面包含了一個Activity所擁有的Configuration和全部可能擁有過的Resources,好比一個Activity,在某些狀況下他的ResourcesImpl發生了變化,那麼這個時候就ActivityResources就可能會持有多個Resource引用
*/
private final WeakHashMap<IBinder, ActivityResources> mActivityResourceReferences = new WeakHashMap<>();
/**
* 緩存的ApkAssets,這個能夠先不看
*/
private final LruCache<ApkKey, ApkAssets> mLoadedApkAssets = new LruCache<>(3);
/**
* 這也是ApkAssets的一個緩存 這個也能夠先不看
*/
private final ArrayMap<ApkKey, WeakReference<ApkAssets>> mCachedApkAssets = new ArrayMap<>();
private static class ApkKey {
public final String path;
public final boolean sharedLib;
public final boolean overlay;
}
/**
* 與Activity關聯的資源和基本配置覆蓋。
*/
private static class ActivityResources {
public final Configuration overrideConfig = new Configuration();
//按照常規的理解 一個Activity只有一個Resources 可是這裏卻使用了一個list來存儲,這是考慮若是Activity發生變化,從新生成了Resource,這個列表就會將Activity歷史使用過的Resources都存在裏面,固然,若是沒有人再持有這些Resources,就會被回收
public final ArrayList<WeakReference<Resources>> activityResources = new ArrayList<>();
}複製代碼
ResourceManager提供了以下以寫public方法供調用。
先看getResources和createBaseActivityResources 最終都是使用一個ResourcesKey去調用getOrCreateResources。
Resources getResources(@Nullable IBinder activityToken, @Nullable String resDir, @Nullable String[] splitResDirs, @Nullable String[] overlayDirs, @Nullable String[] libDirs, int displayId, @Nullable Configuration overrideConfig, @NonNull CompatibilityInfo compatInfo, @Nullable ClassLoader classLoader) {
try {
final ResourcesKey key = new ResourcesKey(resDir, splitResDirs, overlayDirs, libDirs, displayId, overrideConfig != null ? new Configuration(overrideConfig) : null,compatInfo);
classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
return getOrCreateResources(activityToken, key, classLoader);
} finally {
}
}複製代碼
Resources createBaseActivityResources(@NonNull IBinder activityToken, @Nullable String resDir, @Nullable String[] splitResDirs, @Nullable String[] overlayDirs, @Nullable String[] libDirs, int displayId, @Nullable Configuration overrideConfig, @NonNull CompatibilityInfo compatInfo, @Nullable ClassLoader classLoader) {
try {
final ResourcesKey key = new ResourcesKey(resDir, splitResDirs, overlayDirs, libDirs, displayId, overrideConfig != null ? new Configuration(overrideConfig) : null, compatInfo);
classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
synchronized (this) {
// 強制建立ActivityResources對象並放到緩存裏面
getOrCreateActivityResourcesStructLocked(activityToken);
}
// 更新任何現有的Activity Resources引用。
updateResourcesForActivity(activityToken, overrideConfig, displayId, false /* movedToDifferentDisplay */);
// 如今請求一個實際的Resources對象。
return getOrCreateResources(activityToken, key, classLoader);
} finally {
}
}複製代碼
private @Nullable
Resources getOrCreateResources(@Nullable IBinder activityToken, @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
synchronized (this) {
if (activityToken != null) {
final ActivityResources activityResources = getOrCreateActivityResourcesStructLocked(activityToken);
// 清理已經被回收的緩存
ArrayUtils.unstableRemoveIf(activityResources.activityResources, sEmptyReferencePredicate);
// Rebase the key's override config on top of the Activity's base override.
if (key.hasOverrideConfiguration() && !activityResources.overrideConfig.equals(Configuration.EMPTY)) {
final Configuration temp = new Configuration(activityResources.overrideConfig);
temp.updateFrom(key.mOverrideConfiguration);
key.mOverrideConfiguration.setTo(temp);
}
//根據對應的key 去獲取一個ResourcesImpl 有多是新的也有多是緩存裏面的
ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
if (resourcesImpl != null) {
//使用ResourcesImpl 去生成一個Resources
return getOrCreateResourcesForActivityLocked(activityToken, classLoader, resourcesImpl, key.mCompatInfo);
}
// We will create the ResourcesImpl object outside of holding this lock.
} else {
// 清理 由於mResourceReferences裏面放的都是弱引用,要判斷這些弱引用是否都已經被釋放,若是釋放的話就要從Array裏面移除掉
ArrayUtils.unstableRemoveIf(mResourceReferences, sEmptyReferencePredicate);
// 不依賴於Activity,找到具備正確ResourcesImpl的共享資源 這裏就是根據key去mResourceImpls的緩存裏面找
ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
if (resourcesImpl != null) {
//若是找到resourcesImpl的話就去從mResourceReferences看有沒有可用的resources 若是類加載器和ResourcesImpl相同,則獲取現有的Resources對象,不然會建立一個新的Resources對象。
return getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
}
// 咱們將在持有此鎖以外建立ResourcesImpl對象。
}
// 若是咱們走到了這裏,咱們找不到合適的ResourcesImpl來使用,因此如今建立一個。
ResourcesImpl resourcesImpl = createResourcesImpl(key);
if (resourcesImpl == null) {
return null;
}
// 將此ResourcesImpl添加到緩存中。
mResourceImpls.put(key, new WeakReference<>(resourcesImpl));
final Resources resources;
if (activityToken != null) {
//從mActivityResourceReferences 裏面去找 看有沒有合適的Resources可用 若是沒有就構建一個Resources兵添加到mActivityResourceReferences裏面
resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader, resourcesImpl, key.mCompatInfo);
} else {
//使用建立出來的ResourcesImpl去匹配一個Resource,具體是從緩存mResourceReferences裏面取(若是有的話)仍是建立新的由下面的方法決定
resources = getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
}
return resources;
}
}複製代碼
畫個流程圖看下
看完這個圖基本上大致的邏輯就通咱們使用以下的代碼 hook 系統ResourcesManger的幾個緩存 看一下當一個App啓動而且打開一個Activity時,這些緩存裏面都包含了哪些對象。
try {
System.out.println("Application = " + getApplicationContext().getResources() + " 持有 " + Reflector.with(getApplicationContext().getResources()).method("getImpl").call());
System.out.println("Activity = " + getResources() + " 持有 " + Reflector.with(getResources()).method("getImpl").call());
System.out.println("System = " + Resources.getSystem() + " 持有 " + Reflector.with(Resources.getSystem()).method("getImpl").call());
ResourcesManager resourcesManager = ResourcesManager.getInstance();
System.out.println("--------------------------------mResourceImpls----------------------------------------------");
ArrayMap<ResourcesKey, WeakReference<ResourcesImpl>> mResourceImpls = Reflector.with(resourcesManager).field("mResourceImpls").get();
Iterator<ResourcesKey> resourcesKeyIterator = mResourceImpls.keySet().iterator();
while (resourcesKeyIterator.hasNext()) {
ResourcesKey key = resourcesKeyIterator.next();
WeakReference<ResourcesImpl> value = mResourceImpls.get(key);
System.out.println("key = " + key);
System.out.println("value = " + value.get());
}
System.out.println("-----------------------------------mResourceReferences-------------------------------------------");
ArrayList<WeakReference<Resources>> mResourceReferences = Reflector.with(resourcesManager).field("mResourceReferences").get();
for (WeakReference<Resources> weakReference : mResourceReferences) {
Resources resources = weakReference.get();
if (resources != null) {
System.out.println(resources + " 持有 " + Reflector.with(resources).method("getImpl").call());
}
}
System.out.println("-------------------------------------mActivityResourceReferences-----------------------------------------");
WeakHashMap<IBinder, Object> mActivityResourceReferences = Reflector.with(resourcesManager).field("mActivityResourceReferences").get();
Iterator<IBinder> iBinderIterator = mActivityResourceReferences.keySet().iterator();
while (iBinderIterator.hasNext()) {
IBinder key = iBinderIterator.next();
Object value = mActivityResourceReferences.get(key);
System.out.println("key = " + key);
System.out.println("value = " + value);
Object overrideConfig = Reflector.with(value).field("overrideConfig").get();
System.out.println("overrideConfig = " + overrideConfig);
Object activityResources = Reflector.with(value).field("activityResources").get();
try {
ArrayList<WeakReference<Resources>> list = (ArrayList<WeakReference<Resources>>) activityResources;
for (WeakReference<Resources> weakReference : list) {
Resources resources = weakReference.get();
System.out.println("activityResources = " + resources + " 持有 " + Reflector.with(resources).method("getImpl").call());
}
} catch (Reflector.ReflectedException e) {
e.printStackTrace();
}
}
} catch (Exception e) {
e.printStackTrace();
}複製代碼
打印出來的結果以下圖:
分析完兩個不一樣api level的資源管理源碼,咱們再來分析一下兩個不一樣apiLevel在加載完成一個webView組件以後Resource的區別。
先說如下6.0的 。
根據6.0 ResourceManager的代碼 咱們先作一個測試:
編寫以下代碼 咱們將mActiveResources中保存的內容打印出來。
三、6.0 web資源注入分析
ResourcesManager resourcesManager = ResourcesManager.getInstance();
//6.0打印
try {
ArrayMap<Object, WeakReference<Object>> map = Reflector.with(resourcesManager).field("mActiveResources").get();
for (int i = 0; i < map.size(); i++) {
Object a = map.keyAt(i);
Object b = map.valueAt(i).get();
System.out.println(Reflector.with(a).field("mResDir").get());
System.out.println(b.toString());
}
} catch (Exception e) {
e.printStackTrace();
}複製代碼
10-12 15:47:02.816 10785-10785/com.xxxx.res_manager_study I/System.out: /data/app/com.xxxx.res_manager_study-1/base.apk
10-12 15:47:02.816 10785-10785/com.xxxx.res_manager_study I/System.out: android.content.res.Resources@f911117複製代碼
再修改代碼:
在打印以前添加webView初始化 WebView webView = new WebView(context);
打印輸出:
10-12 15:48:48.586 10985-10985/com.xxxx.res_manager_study I/System.out: /data/app/com.google.android.webview-1/base.apk
10-12 15:48:48.586 10985-10985/com.xxxx.res_manager_study I/System.out: android.content.res.Resources@9bc9c4
10-12 15:48:48.586 10985-10985/com.xxxx.res_manager_study I/System.out: /data/app/com.xxxx.res_manager_study-2/base.apk
10-12 15:48:48.586 10985-10985/com.xxxx.res_manager_study I/System.out: android.content.res.Resources@b66d0ad複製代碼
能夠看到添加了webView初始化代碼以後 mActiveResources中增長了一個Resources實例,該實例指向webView組件安裝路徑。
WebView就是從這個Resources取到了本身所須要的資源。這也是7.0如下版本中替換Activity和Application的Resources不會出現Web組件崩潰的緣由,由於在這個level的系統中,web組件資源與主apk資源是分離的。
OK 分析完6.0的再看9.0的。
9.0的ResourceManager相對複雜,咱們也是使用反射的方法將兩種狀況下的ResourceManager數據打印出來。
編寫打印代碼。
四、9.0 web資源注入分析
System.out.println(" 打印 mResourceImpls 中緩存的 ResourceImpl");
ResourcesManager resourcesManager = ResourcesManager.getInstance();
// 9.0源碼
try {
ArrayMap map = Reflector.with(resourcesManager).field("mResourceImpls").get();
for (int i = 0; i < map.size(); i++) {
Object key = map.keyAt(i);
WeakReference value = (WeakReference) map.get(key);
System.out.println(value.get() + " " + key);
}
} catch (Reflector.ReflectedException e) {
e.printStackTrace();
}
System.out.println(" 打印 mActivityResourceReferences 中緩存的 Activity Resources");
try {
WeakHashMap<Object, Object> map = Reflector.with(resourcesManager).field("mActivityResourceReferences").get();
for (Map.Entry<Object, Object> entry : map.entrySet()) {
Object activityResources = entry.getValue();
ArrayList<WeakReference<Resources>> list = Reflector.with(activityResources).field("activityResources").get();
for (WeakReference<Resources> weakReference : list) {
Resources resources = weakReference.get();
Object resourcesImpl = Reflector.with(resources).field("mResourcesImpl").get();
System.out.println(resourcesImpl);
}
}
} catch (Exception e) {
e.printStackTrace();
}複製代碼
I/System.out: 打印 mResourceImpls 中緩存的 ResourceImpl
I/System.out: android.content.res.ResourcesImpl@c0c1962 ResourcesKey{ mHash=8a5fac6a mResDir=null mSplitDirs=[] mOverlayDirs=[] mLibDirs=[] mDisplayId=0 mOverrideConfig=v28 mCompatInfo={480dpi always-compat}}
I/System.out: android.content.res.ResourcesImpl@4aedaf3 ResourcesKey{ mHash=bafccb1 mResDir=/data/app/com.xxxx.res_manager_study-_k1QRBE8jUyrPTVnJDIbsA==/base.apk mSplitDirs=[] mOverlayDirs=[] mLibDirs=[/system/framework/org.apache.http.legacy.boot.jar] mDisplayId=0 mOverrideConfig=v28 mCompatInfo={480dpi always-compat}}
I/System.out: android.content.res.ResourcesImpl@1b73b0 ResourcesKey{ mHash=30333beb mResDir=/data/app/com.xxxx.res_manager_study-_k1QRBE8jUyrPTVnJDIbsA==/base.apk mSplitDirs=[] mOverlayDirs=[] mLibDirs=[/system/framework/org.apache.http.legacy.boot.jar] mDisplayId=0 mOverrideConfig=en-rUS-ldltr-sw360dp-w360dp-h752dp-normal-long-notround-lowdr-nowidecg-port-notnight-xxhdpi-finger-keysexposed-nokeys-navhidden-nonav-v28 mCompatInfo={480dpi always-compat}}
I/System.out: 打印 mActivityResourceReferences 中緩存的 Activity Resources
I/System.out: android.content.res.ResourcesImpl@1b73b0複製代碼
mResDir=/data/app/com.xxxx.res_manager_study-_k1QRBE8jUyrPTVnJDIbsA==/base.apk
mSplitDirs=[]
mOverlayDirs=[]
mLibDirs=[/system/framework/org.apache.http.legacy.boot.jar]
mDisplayId=0
mOverrideConfig=en-rUS-ldltr-sw360dp-w360dp-h752dp-normal-long-notround-lowdr-nowidecg-port-notnight-xxhdpi-finger-keysexposed-nokeys-navhidden-nonav-v28 mCompatInfo={480dpi always-compat}}複製代碼
I/System.out: 打印 mResourceImpls 中緩存的 ResourceImpl
I/System.out: android.content.res.ResourcesImpl@cbc1adc ResourcesKey{ mHash=8a5fac6a mResDir=null mSplitDirs=[] mOverlayDirs=[] mLibDirs=[] mDisplayId=0 mOverrideConfig=v28 mCompatInfo={480dpi always-compat}}
I/System.out: android.content.res.ResourcesImpl@aa8a10 ResourcesKey{ mHash=25ddf2aa mResDir=/data/app/com.xxxx.res_manager_study-sVY46cDW2JT2hEkohn2GJw==/base.apk mSplitDirs=[] mOverlayDirs=[] mLibDirs=[/system/framework/org.apache.http.legacy.boot.jar,/data/app/com.android.chrome-dO2jAeCdfgkLjVHzK2yx0Q==/base.apk] mDisplayId=0 mOverrideConfig=v28 mCompatInfo={480dpi always-compat}}
I/System.out: android.content.res.ResourcesImpl@e6ea7e5 ResourcesKey{ mHash=4114b0be mResDir=/data/app/com.android.chrome-dO2jAeCdfgkLjVHzK2yx0Q==/base.apk mSplitDirs=[/data/app/com.android.chrome-dO2jAeCdfgkLjVHzK2yx0Q==/split_autofill_assistant.apk,/data/app/com.android.chrome-dO2jAeCdfgkLjVHzK2yx0Q==/split_autofill_assistant.config.en.apk,/data/app/com.android.chrome-dO2jAeCdfgkLjVHzK2yx0Q==/split_autofill_assistant.config.in.apk,/data/app/com.android.chrome-dO2jAeCdfgkLjVHzK2yx0Q==/split_autofill_assistant.config.ms.apk,/data/app/com.android.chrome-dO2jAeCdfgkLjVHzK2yx0Q==/split_autofill_assistant.config.zh.apk,/data/app/com.android.chrome-dO2jAeCdfgkLjVHzK2yx0Q==/split_config.en.apk,/data/app/com.android.chrome-dO2jAeCdfgkLjVHzK2yx0Q==/split_config.in.apk,/data/app/com.android.chrome-dO2jAeCdfgkLjVHzK2yx0Q==/split_config.ms.apk,/data/app/com.android.chrome-dO2jAeCdfgkLjVHzK2yx0Q==/split_config.zh.apk] mOverlayDirs=[] mLibDirs=[] mDisplayId=0 mOverrideConfig=v28 mCompatInfo={480dpi always-compat}}
I/System.out: android.content.res.ResourcesImpl@70dd909 ResourcesKey{ mHash=4a6161e4 mResDir=/data/app/com.xxxx.res_manager_study-sVY46cDW2JT2hEkohn2GJw==/base.apk mSplitDirs=[] mOverlayDirs=[] mLibDirs=[/system/framework/org.apache.http.legacy.boot.jar,/data/app/com.android.chrome-dO2jAeCdfgkLjVHzK2yx0Q==/base.apk] mDisplayId=0 mOverrideConfig=en-rUS-ldltr-sw360dp-w360dp-h752dp-normal-long-notround-lowdr-nowidecg-port-notnight-xxhdpi-finger-keysexposed-nokeys-navhidden-nonav-v28 mCompatInfo={480dpi always-compat}}
I/System.out: android.content.res.ResourcesImpl@81669ae ResourcesKey{ mHash=578cb784 mResDir=/data/app/com.xxxx.res_manager_study-sVY46cDW2JT2hEkohn2GJw==/base.apk mSplitDirs=[] mOverlayDirs=[] mLibDirs=[/system/framework/org.apache.http.legacy.boot.jar] mDisplayId=0 mOverrideConfig=v28 mCompatInfo={480dpi always-compat}}
I/System.out: android.content.res.ResourcesImpl@52334f ResourcesKey{ mHash=7c1026be mResDir=/data/app/com.xxxx.res_manager_study-sVY46cDW2JT2hEkohn2GJw==/base.apk mSplitDirs=[] mOverlayDirs=[] mLibDirs=[/system/framework/org.apache.http.legacy.boot.jar] mDisplayId=0 mOverrideConfig=en-rUS-ldltr-sw360dp-w360dp-h752dp-normal-long-notround-lowdr-nowidecg-port-notnight-xxhdpi-finger-keysexposed-nokeys-navhidden-nonav-v28 mCompatInfo={480dpi always-compat}}
I/System.out: 打印 mActivityResourceReferences 中緩存的 Activity Resources
I/System.out: android.content.res.ResourcesImpl@70dd909複製代碼
對比沒有添加webview 實例化以前的代碼 咱們發現mLibDirs中新增了/data/app/com.android.chrome-dO2jAeCdfgkLjVHzK2yx0Q==/base.apk
結論:9.0源碼中 android將Web組件資源做爲libDir添加至Assert中,用於資源查找,沒有使用Resource分離的方式。
瞭解了這個緣由以後 咱們進一步尋找libDir添加web組件資源的地方。
webView在初始化階段 會調用WebViewDelegate的addWebViewAssetPath方法。
public void addWebViewAssetPath(Context context) {
final String newAssetPath = WebViewFactory.getLoadedPackageInfo().applicationInfo.sourceDir;
final ApplicationInfo appInfo = context.getApplicationInfo();
final String[] libs = appInfo.sharedLibraryFiles;
if (!ArrayUtils.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.
ResourcesManager.getInstance().appendLibAssetForMainAssetPath(
appInfo.getBaseResourcePath(), newAssetPath);
}
}複製代碼
傳入兩個參數 第一個是當前應用的respath 第二個是webView的resPath 具體看以下源碼註釋。
public void appendLibAssetForMainAssetPath(String assetPath, String libAsset) {
synchronized (this) {
// Record which ResourcesImpl need updating
// (and what ResourcesKey they should update to).
final ArrayMap<ResourcesImpl, ResourcesKey> updatedResourceKeys = new ArrayMap<>();
final int implCount = mResourceImpls.size();
//遍歷全部的ResourcesImpl ResourcesImpl是組成Rescource的核心 他們之間的關係是Resource包含ResourcesImpl包含AssertManager
for (int i = 0; i < implCount; i++) {
final ResourcesKey key = mResourceImpls.keyAt(i);
final WeakReference<ResourcesImpl> weakImplRef = mResourceImpls.valueAt(i);
final ResourcesImpl impl = weakImplRef != null ? weakImplRef.get() : null;
//這裏首先進行判斷的ResourcesImpl是否包含assetPath 也就是說若是一個ResourcesImpl的mResDir不是當前應用的 則不會進行處理
if (impl != null && Objects.equals(key.mResDir, assetPath)) {
//還要判斷新的資源路徑是否是已經存在了 若是存在了就不作處理
if (!ArrayUtils.contains(key.mLibDirs, libAsset)) {
final int newLibAssetCount = 1 + (key.mLibDirs != null ? key.mLibDirs.length : 0);
final String[] newLibAssets = new String[newLibAssetCount];
if (key.mLibDirs != null) {
//這裏就將新的路徑添加到須要添加的ResourcesImpl所對應的ResourcesKey的libDir上面了
System.arraycopy(key.mLibDirs, 0, newLibAssets, 0, key.mLibDirs.length);
}
newLibAssets[newLibAssetCount - 1] = libAsset;
updatedResourceKeys.put(impl, new ResourcesKey(key.mResDir, key.mSplitResDirs, key.mOverlayDirs, newLibAssets, key.mDisplayId, key.mOverrideConfiguration, key.mCompatInfo));
}
}
}
redirectResourcesToNewImplLocked(updatedResourceKeys);
}
}複製代碼
//這個方法是更新當前持有ResourcesImpl的Resource
private void redirectResourcesToNewImplLocked(@NonNull final ArrayMap<ResourcesImpl, ResourcesKey> updatedResourceKeys) {
// Bail early if there is no work to do.
if (updatedResourceKeys.isEmpty()) {
return;
}
// Update any references to ResourcesImpl that require reloading.
final int resourcesCount = mResourceReferences.size();
for (int i = 0; i < resourcesCount; i++) {
final WeakReference<Resources> ref = mResourceReferences.get(i);
final Resources r = ref != null ? ref.get() : null;
if (r != null) {
//首先是根據老的ResourcesImpl找到新的ResourcesKey
final ResourcesKey key = updatedResourceKeys.get(r.getImpl());
if (key != null) {
//而後根據新的ResourcesKey生成新的ResourcesImpl
final ResourcesImpl impl = findOrCreateResourcesImplForKeyLocked(key);
if (impl == null) {
throw new Resources.NotFoundException("failed to redirect ResourcesImpl");
}
//最後在替換掉Resources中的ResourcesImpl
r.setImpl(impl);
}
}
}
// Update any references to ResourcesImpl that require reloading for each Activity.
//這邊跟上面是同樣的道理 只不過這裏處理的是全部記錄的Activity的Resource
for (ActivityResources activityResources : mActivityResourceReferences.values()) {
final int resCount = activityResources.activityResources.size();
for (int i = 0; i < resCount; i++) {
final WeakReference<Resources> ref = activityResources.activityResources.get(i);
final Resources r = ref != null ? ref.get() : null;
if (r != null) {
final ResourcesKey key = updatedResourceKeys.get(r.getImpl());
if (key != null) {
final ResourcesImpl impl = findOrCreateResourcesImplForKeyLocked(key);
if (impl == null) {
throw new Resources.NotFoundException("failed to redirect ResourcesImpl");
}
r.setImpl(impl);
}
}
}
}
}複製代碼
WebView就是經過這種方式,在Activity的Resource中加入了WebView的資源。
最終解決方案
這樣其實咱們就已經分析出在7.0以上的機器中長按WebView 由於資源缺失致使崩潰的緣由了。
咱們在資源替換方案中將Context的Resource替換成了咱們的ProxyResources,而ProxyResources其實並無被ResourcesManager管理,也就是說webView資源注入的時候 咱們的ProxyResources並無被更新。
瞭解了所有原理以後 解決方法一目瞭然。
見以下代碼:
// step 4 將代理的Resources合併到ResourcesManager中統一管控 由於咱們的ProxyResourcess的ResPath是應用的path,因此webView資源注入的時候就會同步到這個Res裏面
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
synchronized (ResourcesManager.getInstance()) {
//不用擔憂在list中不斷的添加會形成數量暴增,由於添加的是弱引用,若是頁面被關閉,會自動回收
ArrayList<WeakReference<Resources>> list = Reflector.with(ResourcesManager.getInstance()).field("mResourceReferences").get();
list.add(new WeakReference<Resources>(textRepairProxyResourcess));
}
}複製代碼
爲何要在attachBaseContext中進行反射替換Resource?
回答:
無論替換的是Application仍是Activity的mResources 必定是在attachBaseContext裏面對baseContext進行Hook,直接將Activity或者Application自己進行hook是不成功的 由於Activity或者Application自己並非Context,他只是一個ContextWapper。而ContextWapper中真正的Context其實就是在attachBaseContext時賦值的。
既然已經替換了Activity和Application的Resource,爲何還要使用factory處理layout初始化,難道layout初始化不是使用Activity中的Resource嗎?
回答:
咱們對Activity或者Application的mResources進行了替換,可是若是不實現流程5中的ActivtyLifecycleCallbacks,那麼XML中編寫的text沒法實現替換,緣由在於View使用TypedArray在進行賦值的時候,並非直接使用mResources,而是直接使用mResourcesImpl,因此直接hooke了mResources仍是沒用,其實mResources的getText方法也是調用mResources中的mResourcesImpl的方法。
對於已經使用了換膚模式的app(好比說瀏覽器)如何作String在線更新?
回答:
只須要修改原有換膚模式使用的SkinProxyResource,並getText,getString等方法代理到在線更新的TextProxyResources上便可。
更多內容敬請關注 vivo 互聯網技術 微信公衆號
注:轉載文章請先與微信號:Labs2020 聯繫。