In this paper, starting from vivo Internet technology WeChat public links: mp.weixin.qq.com/s/jG8rAjQ8Q… Author: Chen Long

Recent projects need to support dozens of languages, many small languages in the people do not know the same as gibber, translation is generally translated by the translation company, after the translation is completed and then imported into the project, which is easy to have some problems.

Problem 1: Translation is prone to error

Translation process is the client development to write Chinese copywriting — translated into English —- outsourcing translation according to the English string translation of small languages, in this process, some polysemy and some words involving context is easy to translate mistakes.

Problem 2: Errors cannot be found in time

As mentioned above, we could not understand the strings provided by the translation company, and we did not know if they were wrong. Almost all of them were not known until the user gave feedback after the launch.

Therefore, the translation bug of minor languages has always been a kind of bug in the project, so it is necessary to explore a scheme that can be used to dynamically update the translation string.

Third, design ideas

In Android, multilingual strings are stored as XML in various folders, and qualifiers in each folder represent a language, which the average Android developer knows.

As shown in the figure below

A String file is a type of Resource, and when you use it in a layout or in Java code, you call various methods of Resource.

In fact, the dynamic update of the translation language is actually the replacement update of the Resource.

In the early years of development experience, we all know that there is an Android theme replacement scheme to replace resources for applications. Briefly speaking, the process of the scheme is as follows:

  1. Use the addAssertPath method to load the APK package in the SD card and build the AsserManager instance.

  2. The AsserManager builds the PlugResource instance.

  3. Proxyresources are written using decorator mode. Plugresources are obtained preferentially in each method of obtaining resources, and cannot be retrieved from the backup AppResource.

  4. Replace the Resource object in Application and Activity with ProxyResource.

  5. Inheriting LayoutInflater.Factory, intercepts layout generation process, and points resource acquisition to ProxyResource to complete layout initialization.

Now that we have a plan, we can get started.

As a matter of fact, we will encounter many details in the subsequent development process, but everything is difficult at the beginning. We can start from the first step.

Fourth, the development of

Process 1: Pull the PlugResources resource from the separate plugapk package

AssetManager mLoadedAssetManager = AssetManager.class.newInstance();
Reflector.with(mLoadedAssetManager).method("addAssetPath", String.class).call(textResPath);
Resources textResPackResources = new Resources(mLoadedAssetManager, appResources.getDisplayMetrics(), appResources.getConfiguration());Copy the code

Process 2: Build your own TextResResources implement getText and other methods to proxy getText methods into the getText of PlugResources

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{/ / in some cases is very special Such as 34800147 resources using webView mAppResources. GetResourceEntryName throws / / notfound anomalies But the use of get string and can get the resource string try { String resName = mAppResources.getResourceEntryName(resId); String resType = mAppResources.getResourceTypeName(resId); int plugId = mResPackResources.getIdentifier(resName, resType, mResPackPkgName);return plugId;
            } catch (Throwable e) {
                return0; }}} /** * Some methods called in the constructor of super need to be nulled ** @return
     */
    private boolean checkNull() {
        if(mAppResources ! = null && mResPackResources ! = null) {return true;
        } else {
            return false; }} /** * Some methods called inside the constructor of super need to be nulled ** @return
     */
    private boolean checkTextRepairOn() {
        returnTextRepairConfig.getInstance().isTextRepairOnThisSystem(); }}Copy the code

When the Application starts, Hook the mResources object of the Application and set the TextResResources object

Reflector.with(appContext).field("mResources").set(textRepairProxyResourcess);Copy the code

Step 4: Hook the mResources object of the Activity and set the TextResResources object when the Activity starts

Reflector.with(activityContext).field("mResources").set(textRepairProxyResourcess);Copy the code

Five processes: Registered ActivtyLifecycleCallbacks LayoutInfater of activity in the onActivityCreated realizes own Factory, Intercepts and ressettext the attributes of the text Attribute in Factory

public class TextRepairFactory implements LayoutInflater.Factory2 { private static final HashMap<String, Constructor<? extends View>> mConstructorMap = new HashMap<>(); Private static final Class<? Private static final Class<? >[] mConstructorSignature = new Class[] { Context.class, AttributeSet.class }; Private final String[] a = new String[] {private final String[] = new String[] {"android.widget."."android.view."."android.webkit."}; // Attribute handling class TextRepairAttribute mTextRepairAttribute; publicTextRepairFactory() { mTextRepairAttribute = new TextRepairAttribute(); } @Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {/* * Create View */ View View = createViewFormTag(name, context, attrs); /* * If the View returns null, it is a custom control. The custom control does not need to be concatenatedif (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) {// Get the View instance object Class<? extends View> clazz = context.getClassLoader().loadClass(name).asSubclass(View.class); constructor = clazz.getConstructor(mConstructorSignature); McOnstructormap. put(name, constructor); } catch (Throwable e) { } }returnconstructor; } private View createViewFormTag(String name, Context Context, AttributeSet attrs) {// Contains custom controlsif(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; }}returnview; }}Copy the code


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); }}Copy the code


But is it really that simple?

The above code has formed the prototype of resource replacement, basically completed a basic resource replacement process.

On subsequent tests, I found that it was just beginning to sink in.

Fifth, explore

Exploration 1: API restriction calls

As soon as demo starts running, many alarm information is printed in the log.

Since the Resource is replaced by reflection, Google’s Api restriction calls mechanism is also triggered, so WE looked at the RESTRICTION calls of the Api.

Conclusion:

There is no restriction on system signature applications because demo uses the debugging signature. After the system signature is changed, the alarm is cleared.

Exploration 2: Performance testing

Generating PlugResources using plugapk package in SD card is mainly in the process of generating assetManager. This process takes 10-15ms, which is still too long for page startup. Therefore, we try to cache assetManager and shorten the time.

After the reflection replaces the resource, the getText method of the PlugResources is called, first retrieving the name and type of the original resource from the local resource based on its Id. Then, getIndentifier is called with name and type to obtain resId in PlugResources. This process takes a long time. Although it is also at the nanosecond level, the time is one data level higher than that in the non-hook scenario.

Fortunately, however, there was no drop in page fluency and no noticeable drop in page launch speed during the page fluency performance tests.

Exploration 3: Compatibility with system versions

Here comes the real big hole.

After solving the previous problem, I started to enter the Monkey test. In the test, I found that the machine above 7.0 would crash as long as I pressed the content in the webView interface to pop up the copy and paste dialog box. It can be seen from the log that it was caused by the failure of finding webView resources. The string displayed at the original resource location becomes an ID tag like @1232432.

Google search for a long time, found that the relevant information is very little, it seems that the need to understand the relevant logic of webView resource loading from the source level.

Look at the source code, always need to take a problem to see, the goal is clear enough.

Question: Why can 6.0 systems use this solution without webView problems while 7.0 and above systems crash? What is the specific difference between 6.0 and 7.0 systems in resource management?

To get the answer, read the 6.0 and 7.0 source code, starting with the 6.0 source code.

1, 6.0 resource management source code analysis

The Context initialization

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; . }Copy the code


When the Context is created, the Resource is created.

There are two places where Resource creation is involved

  1. resources =packageInfo.getResources(mainThread);

  2. resources =mResourcesManager.getTopLevelResources(packageInfo.getResDir(),

Start with packageInfo. GetResources (mainThread); PackageInfo is LoadedApk

The getResources method of packageInfo

public Resources getResources(ActivityThread mainThread) {
if (mResources == null) {
mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, null, this);
}
return mResources;
}Copy the code


Then look at ActivityThread

ActivityThread’s getTopLevelResources method

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());Copy the code


In fact calls are mResourcesManager getTopLevelResources

Android M’s ResourcesManager is easy to write

It has an internal Resource cache

The getTopLevelResource method assembles a key from the parameters passed in

ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfigCopy, scale);

Use this key to look in the cache and pull it out when you find it.

WeakReference<Resources> wr = mActiveResources.get(key);

If not, create a new asset to generate a Resource instance

AssetManager assets = new AssetManager();
if(resDir ! = null) {if (assets.addAssetPath(resDir) == 0) {
returnnull; }}if(splitResDirs ! = null) {for (String splitResDir : splitResDirs) {
if (assets.addAssetPath(splitResDir) == 0) {
returnnull; }}}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."); }}}}Copy the code


Another advantage of the cache is that when the configuration changes, the cache can find all the currently active resources.

And call a public void updateConfiguration of those resources (Configuration config,DisplayMetrics metrics, CompatibilityInfo compat) {method, the final effect is the configuration of the mAssets in the Resource

Take a look at Resource. Java

Its core consists of two parts

1: encapsulates Assets. All resource calls are ultimately called to mAssets methods

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));
}Copy the code

2: provides cache

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); Large resources pulled from mAsserts are cached to avoid read time and memory footprintCopy the code


After looking at 6.0 source code we look for a 9.0 code, 9.0 resource management and 7.0 basically in the same line, so we directly use 9.0 source code to analyze.

In contrast to Android6.0, the AssertManager is not maintained in Resources in 9.0. Instead, the AssertManager is packaged with some other caches as a ResourcesImpl.

public class Resources {
   
    static final String TAG = "Resources";
   
    static Resources mSystem = null;
   
    private ResourcesImpl mResourcesImpl;
   
    private TypedValue mTmpValue = new TypedValue();
   
    final ClassLoader mClassLoader;Copy the code


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();
}Copy the code


ResourcesImpl takes over the responsibilities of Resources in older versions, packaging the AssertManager and maintaining the data cache.

The Resources code is much simpler, and the method calls are ultimately handed over to ResourcesImpl.

The same as Android6.0, ResourcesManager is a singleton.

So how is ResourcesManager 9.0 different from ResourcesManager 6.0?

It’s going to start with the app startup, so it’s going to be ContextImpl.

2, 9.0 resource management source code analysis

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;
}
Copy the code


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;
 }Copy the code


Whether the Resource generated by the Application or the Resource generated by the Activity ultimately calls the difference between ResourceManager methods. Lies in a call to

ResourcesManager. GetInstance (). GetResources, another call is ResourcesManager createBaseActivityResources.

OK let’s take a look at the ResourcesManager source code.

Let’s take a look at the various properties it offers and pick the most important ones.

/** * Mapping of ResourceImpls and its configuration. These are large memory hogs * and should be reused whenever possible. All ResourcesImpl generated by ResourcesManager will be cached in this map */ private final ArrayMap<ResourcesKey, WeakReference<ResourcesImpl>> mResourceImpls = new ArrayMap<>(); /** * List of resource references that can be reused. Note that this list does not store the Activity's Resources cache. As I understand it, all non-ActivCity Resources will be cached here. Application Resource */ private Final ArrayList<WeakReference<Resources>> mResourceReferences = new ArrayList<>(); /** * Each Activity has a basic override configuration that applies to each Resources object, which in turn can specify its own override configuration. The cache contains all the Actrivity Resources. ActivityResources is an object that contains the Configuration that an Activity has and all the Resources that it may have had, such as an Activity. In some cases his ResourcesImpl has changed, Private final WeakHashMap<IBinder, ActivityResources> mActivityResourceReferences = new WeakHashMap<>(); /** * Private final LruCache<ApkKey, ApkAssets> mLoadedApkAssets = new LruCache<>(3); /** * 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; } /** * Overrides the resources and basic configuration associated with the Activity. */ private static class ActivityResources { public final Configuration overrideConfig = new Configuration(); // An Activity has only one Resource, but a list is used to store it. This is because if the Activity changes and Resources are regenerated, This list will store all the Resources that have been used in the Activity's history. Of course, if no one else holds those Resources, Public final ArrayList<WeakReference<Resources>> activityResources = new ArrayList<>(); }Copy the code


With these important attributes in mind, let’s look at the many methods ResourceManager provides.

ResourceManager provides the following public methods to call.

See first getResources and createBaseActivityResources ultimately is to use an getOrCreateResources ResourcesKey to call.

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 {
  
     }
 }Copy the code

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) {/ / forced to create ActivityResources object and into the cache getOrCreateActivityResourcesStructLocked (activityToken); } // Update any existing Activity Resources references. updateResourcesForActivity(activityToken, overrideConfig, displayId,false/* movedToDifferentDisplay */); // Now request an actual Resources object.return getOrCreateResources(activityToken, key, classLoader);
    } finally {
  
    }
}Copy the code


GetOrCreateResources I put comments on all lines of code, and if you look at the comments in the code, some of the comments are translations of the quotes in the code.

private @Nullable
Resources getOrCreateResources(@Nullable IBinder activityToken, @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
    synchronized (this) {
        if(activityToken ! = null) { final ActivityResources activityResources = getOrCreateActivityResourcesStructLocked(activityToken); / / cleaning has been recycling cache 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); } / / according to the corresponding key to get a ResourcesImpl is likely to be new it is possible that the inside of the cache ResourcesImpl ResourcesImpl = findResourcesImplForKeyLocked (key);if(resourcesImpl ! = null) {// Use ResourcesImpl to generate a resourcereturn getOrCreateResourcesForActivityLocked(activityToken, classLoader, resourcesImpl, key.mCompatInfo);
            }
  
            // We will create the ResourcesImpl object outside of holding this lock.
  
        } else{// Clear the weak references in mResourceReferences. If released to remove oil from the Array ArrayUtils. UnstableRemoveIf (mResourceReferences sEmptyReferencePredicate); // Not dependent on the Activity, ResourcesImpl = ResourcesImpl = ResourcesImpl = ResourcesImpl = ResourcesImpl = ResourcesImpl = findResourcesImplForKeyLocked(key);if(resourcesImpl ! = null) {// Resources are available from mResourceReferences if the class loader is the same as resourcesImpl. Otherwise, a new Resources object is created.returngetOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo); } // We will create a ResourcesImpl object in addition to holding this lock. } // If we get here, we can't find a suitable ResourcesImpl to use, so create one now. ResourcesImpl resourcesImpl = createResourcesImpl(key);if (resourcesImpl == null) {
            returnnull; } // Add this ResourcesImpl to the cache. mResourceImpls.put(key, new WeakReference<>(resourcesImpl)); final Resources resources;if(activityToken ! = null) {/ / from mActivityResourceReferences inside go to look have the right Resources available If not just build a Resources added to mActivityResourceReferences inside resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader, resourcesImpl, key.mCompatInfo); }else{// Use the created ResourcesImpl to match a Resource, Concrete from the cache mResourceReferences inside take (if any) or create a new is decided by the following method resources = getOrCreateResourcesLocked (this, resourcesImpl, key.mCompatInfo); }returnresources; }}Copy the code

Let’s draw a flow chart

We use the following code to hook ResourcesManger caches to see what objects are contained in these caches when an App starts and an Activity is started.

try {
    System.out.println("Application = " + getApplicationContext().getResources() + "Hold" + Reflector.with(getApplicationContext().getResources()).method("getImpl").call());
    System.out.println("Activity = " + getResources() + "Hold" + Reflector.with(getResources()).method("getImpl").call());
    System.out.println("System = " + Resources.getSystem() + "Hold" + 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 +"Hold" + 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 + "Hold" + Reflector.with(resources).method("getImpl").call());
            }
        } catch (Reflector.ReflectedException e) {
            e.printStackTrace();
        }
    }
} catch (Exception e) {
    e.printStackTrace();
}Copy the code


The printout looks like this:

After analyzing the Resource management source code of two different API levels, we will analyze the difference between two different apiLevel resources after loading a webView component.

Let’s start with 6.0.

According to 6.0 ResourceManager code we do a test:

Write the following code to print out the content saved in Activeresources.

3. 6.0 Web resource injection analysis

ResourcesManager resourcesManager = ResourcesManager.getInstance(); //6.0 Print 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();
}Copy the code


A printout

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@f911117Copy the code

You can see that the Resources of the current package have been added to active Resources.

Modify the code again:

Add webView before printing initialize webView webView = new webView (Context);

Printout:

10-12 15:48:48. 586, 10985-10985 / com. XXXX. Res_manager_study I/System. Out: / data/app/com. Google. Android. Webview - 1 / base. 10-12 15:48:48 apk. 586, 10985-10985 / com. XXXX. Res_manager_study I/System. Out: Android. Content. Res. Resources @ 9 bc9c4 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@b66d0adCopy the code


You can see that after adding the webView initialization code, a Resources instance is added to mActiveResources that points to the webView component installation path.

The WebView gets the Resources it needs from this resource. This is why Resources that replace activities and applications in versions 7.0 and below do not crash Web components, because at this level the Web component Resources are separated from the main APK Resources.

OK analysis of 6.0 to see 9.0.

9.0 ResourceManager is relatively complex. We also use reflection to print ResourceManager data in two cases.

Write print code.

9.0 Web resource injection analysis

System.out.println("Print the ResourceImpl cached in mResourceImpls"); ResourcesManager resourcesManager = ResourcesManager.getInstance(); 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("Print mActivityResourceReferences cache 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();
}Copy the code


Output print or type in the code Output the data in the mResourceImpls and mActivityResourceReferences we don’t understand the two cache role can go to the previous article.

I/ system. out: Prints the ResourceImpl I/ system. out: prints the ResourceImpl cached in mResourceImpls. 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-keyse xposed-nokeys-navhidden-nonav-v28 mCompatInfo={480dpi always-compat}} I/System.out: Print mActivityResourceReferences cache Activity Resources I/System. Out: android. Content. res. 1 b73b0 ResourcesImpl @Copy the code


According to the corresponding ResourcesImpl mActivityResourceReferences AcitvityResource we find and learn about the contents of the ResourcesImpl according to ResourceKey.

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-keyse xposed-nokeys-navhidden-nonav-v28 mCompatInfo={480dpi always-compat}}Copy the code


So let’s just print it out and add the source to initialize the webView before we print the code webView webView = new webView (context);

I/ system. out: Prints the ResourceImpl I/ system. out: prints the ResourceImpl cached in mResourceImpls. 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-keyse xposed-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-keyse xposed-nokeys-navhidden-nonav-v28 mCompatInfo={480dpi always-compat}} I/System.out: Print mActivityResourceReferences cache Activity Resources I/System. Out: android. Content. res. 70 dd909 ResourcesImpl @Copy the code


According to the same mActivityResourceReferences AcitvityResource we find the corresponding ResourcesImpl and according to learn about the contents of the ResourcesImpl ResourceKey.

The code before contrast does not add the webview instantiation We found that the added mLibDirs/data/app/com. Android. Chrome – dO2jAeCdfgkLjVHzK2yx0Q = = / base. Apk

Conclusion: In 9.0, Android adds Web component resources to Assert as libDir for Resource lookup without Resource separation.

With this in mind we look further where libDir adds web component resources.

The webView calls the addWebViewAssetPath method of the WebViewDelegate during initialization.

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 LoadedApkin ContextImpl, whichdoes not change during the life of the // application. appInfo.sharedLibraryFiles = newLibAssets; // Update existing Resources with the WebView library. ResourcesManager.getInstance().appendLibAssetForMainAssetPath( appInfo.getBaseResourcePath(), newAssetPath); }}Copy the code


Finally the method called is ResourcesManager getInstance (). AppendLibAssetForMainAssetPath (appInfo. GetBaseResourcePath (), newAssetPath);

Pass in two parameters: the first is the current application’s respath and the second is the webView’s respath.

public void appendLibAssetForMainAssetPath(String assetPath, String libAsset) {
    synchronized (this) {
        // Record whichResourcesImpl need updating // (and what ResourcesKey they should update to). final ArrayMap<ResourcesImpl, ResourcesKey> updatedResourceKeys = new ArrayMap<>(); final int implCount = mResourceImpls.size(); // Iterate over all the ResourcesImpl ResourcesImpl is the core of the Rescource and the relationship between them is Resource contains ResourcesImpl contains AssertManagerfor(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; // If a ResourcesImpl has an assetPath, it will not be processed if the mResDir of a ResourcesImpl is not currently appliedif(impl ! = null && objects.equals (key.mresdir, assetPath)) {// Determine if the new resource path already existsif(! 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 ! System.arraycopy(key.mlibdirs, 0, newLibAssets, 0) {// Add the new path to the libDir of ResourcesImpl of ResourcesKey. 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); }}Copy the code


/ / this method is to update the current Resource holding ResourcesImpl private void redirectResourcesToNewImplLocked (@ NonNull final ArrayMap < ResourcesImpl,  ResourcesKey> updatedResourceKeys) { // Bail earlyif 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 ! Final ResourcesKey key = updatedResourceKeys.get(r.getimpl ()));if(key ! = null) {/ / and then according to the new ResourcesKey generates new ResourcesImpl final ResourcesImpl impl = findOrCreateResourcesImplForKeyLocked (key);if (impl == null) {
                     throw new Resources.NotFoundException("failed to redirect ResourcesImpl"); } // Finally replace Resources with ResourcesImpl r.setimpl (impl); } } } // Update any references to ResourcesImpl that require reloadingforEach Activity. // This is the same thing as above, except here we are dealing with all recorded Activity resourcesfor (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);
                 }
             }
         }
     }
 }Copy the code


When appendLibAssetForMainAssetPath method is invoked, logical order as well, don’t like to see the source code, or is there a process to draw picture.

In this way, the WebView adds the WebView Resource to the Activity’s Resource.

Final solution

In fact, we have analyzed the reason why long-pressing WebView crashes due to lack of resources on 7.0 + machines.

In the Resource substitution scheme, we replaced the Context’s Resource with our ProxyResources, which are not managed by ResourcesManager. That is, our ProxyResources were not updated when the webView resource was injected.

The solution is clear once you understand all the principles.

See the following code:

// Step 4 merge proxy Resources into ResourcesManager for unified management. Since our ProxyResourcess ResPath is the path of the application, webView resource injection will be synchronized to this Resif(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { synchronized (ResourcesManager.getInstance()) { // Don't worry about exploding the list by adding more and more references to it, because weak references are added, and if the page is closed, Automatically recycling ArrayList < WeakReference < Resources > > list = Reflector. With (ResourcesManager. GetInstance ()). The field ("mResourceReferences").get(); list.add(new WeakReference<Resources>(textRepairProxyResourcess)); }}Copy the code


At this point, webView crash problem solved.

6. Review of problems

Problem a:

Why reflection replace Resource in attachBaseContext?

Answer:

Regardless of whether the mResources of the Application or the Activity are replaced, Hook the baseContext within attachBaseContext, Hook an Activity or Application directly will not work because the Activity or Application itself is not the Context, it is just a ContextWapper. The actual Context in the ContextWapper is actually assigned at attachBaseContext.

Problem two:

Since we have replaced the Activity and Application Resource, why should we use factory to initialize the layout?

Answer:

We have to replace the mResources Activity or Application, but if you don’t realize process of 5 ActivtyLifecycleCallbacks, written in XML text can’t replace, MResourcesImpl (mResourcesImpl) is used to assign values to a View using TypedArray. The getText method of mResources is also the method that calls mResourcesImpl in mResources.

Question 3:

How to do String online updates for apps that already use skin mode, such as browsers?

Answer:

Just modify the SkinProxyResource used by the original skin mode, and proxy getText, getString and other methods to the updated online TextProxyResources.

For more content, please pay attention to vivo Internet technology wechat public account

Note: To reprint the article, please contact our wechat account: Labs2020.