您好,登錄后才能下訂單哦!
本篇文章為大家展示了Android中怎么實現多語言動態更新,內容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細介紹希望你能有所收獲。
翻譯的流程是客戶端開發編寫中文文案---翻譯成英文----外包翻譯根據英文字符串翻譯小語種,在這個流程中,有些多義詞和一些涉及語境的詞就很容易翻譯錯誤。
前面說了,翻譯公司提供回來的字符串我們都看不懂,錯了也不知道,幾乎都是上線之后,用戶反饋過來,我們才知道。
因此小語種的翻譯bug一直是項目里面比較多的一類bug,于是就需要探索一種可以用于動態更新翻譯字符串的方案。
在Android中,多語言字符串都是以各種不同文件夾下的xml保存的,每種文件夾中的限定符表示一種語言,這個一般Android的開發人員都是了解的。
如下圖所示
String文件作為Resource的一種,在使用時不管是layout中使用還是在java代碼中使用其實都是調用Resource的各種方法。
那么其實翻譯語言的動態更新實際上是Resource資源的替換更新。
在早些年的開發經驗中,我們都知道有一種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的源碼看起。
1、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; 。。。。。。。。。。。 }
在Context創建之初,Resource就已經創建完成。
這里有兩個地方涉及到了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
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());
其實調用的都是mResourcesManager.getTopLevelResources
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."); } } } }
緩存的另一個作用就是configuration變化的時候 可以從緩存里面找到所有當前正在激活狀態的Resource。
并且調用這些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中取出的大資源進行緩存,避免讀取耗時和內存占用
看完6.0的源碼我們再找一份9.0的代碼來看下,9.0的資源管理基本上與7.0一脈相承,因此我們直接使用了9.0的源碼來進行分析。
相比于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。
2、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提供的諸多方法。
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 { } }
getOrCreateResources 我在各行代碼處都寫了注釋,大家注意看代碼中的注釋,部分注釋是對代碼中引文注釋的翻譯。
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中保存的內容打印出來。
3、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
可以看到當前包的Resources已經被加入到mActiveResources中了。
再修改代碼:
在打印之前添加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數據打印出來。
編寫打印代碼。
4、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(); }
打印輸出在這份打印代碼中 我們輸出了mResourceImpls和mActivityResourceReferences中的數據 不理解這兩個緩存作用的可以去看之前的文章。
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
根據 mActivityResourceReferences中AcitvityResource 我們找到對應的ResourcesImpl并且根據ResourceKey得知了ResourcesImpl中的內容。
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}}
打印輸出下面我們在打印代碼之前添加初始化webView的源碼 WebView webView = new WebView(context);
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
同樣 根據 mActivityResourceReferences中AcitvityResource 我們找到對應的ResourcesImpl并且根據ResourceKey得知了ResourcesImpl中的內容。
對比沒有添加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); } }
最終調用的方法是 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); } } } } }
當appendLibAssetForMainAssetPath方法被調用時,邏輯順序如下好吧,不喜歡看源碼,還是來個畫個流程圖吧。
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)); } }
至此,webView崩潰問題解決。
為什么要在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上即可。
上述內容就是Android中怎么實現多語言動態更新,你們學到知識或技能了嗎?如果還想學到更多技能或者豐富自己的知識儲備,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。