Research on the dynamic update scheme of Android multi language

This article starts with WeChat official account of vivo Internet technology.
Links: https://mp.weixin.qq.com/s/jG8rAjQ8QAOmViiQ33SuEg
Author: Chen Long

Recent projects need to support dozens of languages. Many small languages seem to be the same as garbled code in the eyes of people who don't know them. Translation is generally translated by a translation company, and then imported into the project after translation, which is prone to some problems.

1, Problem 1: translation is easy to make mistakes

The process of translation is that the client develops and compiles the Chinese version --- translate to English --- outsource translation to translate the small language according to the English string. In this process, some polysemy words and some words related to the context are easy to be translated incorrectly.

2, Problem 2: errors cannot be found in time

As mentioned above, we can't understand the strings provided by the translation company, and we don't know if they are wrong. Almost all of them are online, and we only know after the user feedback.

Therefore, translation bugs in small languages have always been one of the most common bugs in the project, so it is necessary to explore a scheme that can be used to dynamically update translation strings.

3, Design ideas

In Android, multilanguage strings are saved in xml under various folders, and the qualifiers in each folder represent a language, which is generally understood by Android developers.

As shown in the figure below

 

As a kind of Resource, String file is used to call various methods of Resource whether in layout or in java code.

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

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

  1. Use the addAssertPath method to load the apk package in the sd card and build the AsserManager instance.

  2. AsserManager builds plugsource instances.

  3. Write ProxyResource in decorator mode, and obtain plugsource in priority among various methods of obtaining resources, which can not be obtained from the backup AppResource.

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

  5. Inheriting layoutingger.factory, intercepting the layout generation process, and pointing the resource acquisition to ProxyResource to complete the layout initialization.

Now that there is a plan for reference, we can start work directly.

In fact, there are many details in the follow-up development process, but it's difficult to start everything. We can start from the first step.

Four, development

Process 1: extract PlugResources resources from independent 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());

Process 2: build your own TextResResources to implement getText and other methods to proxy getText method to getText of plugsources

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 {
            // Some cases are very special. For example, when webView's 34800147 resource uses mapppresources.getresourceentryname, it will throw
            // notfound exception, but you can get the string of this resource by using 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;
            }
        }
    }
  
    /**
     * Some methods are called in super's constructor and need to be null judged
     *
     * @return
     */
    private boolean checkNull() {
        if (mAppResources != null && mResPackResources != null) {
            return true;
        } else {
            return false;
        }
    }
  
    /**
     * Some methods are called in super's constructor and need to be null judged
     *
     * @return
     */
    private boolean checkTextRepairOn() {
        return TextRepairConfig.getInstance().isTextRepairOnThisSystem();
    }
      
}

 

Process 3: when the Application is started, Hook the Application's mResources object and set the TextResResources object

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

Process 4: when the Activity is started, Hook the mResources object of the Activity and set the TextResResources object

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

Process 5: register activitylifecyclecallbacks to implement its own Factory for the layoutinbater of activity in onActivityCreated, intercept the Attribute attribute of text in the Factory and reset the text

public class TextRepairFactory implements LayoutInflater.Factory2 {
    private static final HashMap<String, Constructor<? extends View>> mConstructorMap = new HashMap<>();
    /**
     * The system calls the constructor of two parameters, which we also call
     */
    private static final Class<?>[] mConstructorSignature = new Class[] { Context.class, AttributeSet.class };
    /**
     * Generally, the View of Android system is stored under these packages
     */
    private final String[] a = new String[] { "android.widget.", "android.view.", "android.webkit." };
    // Property processing class
    TextRepairAttribute mTextRepairAttribute;
      
    public TextRepairFactory() {
        mTextRepairAttribute = new TextRepairAttribute();
    }
      
    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        /*
         * We imitate the source code to create a View
         */
        View view = createViewFormTag(name, context, attrs);
        /*
         * Here, if View returns null, it is a custom control,
         * The custom control doesn't need to be spliced by us. You can get the full class name directly
         */
        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 {
                // Get View instance object by reflection
                Class<? extends View> clazz = context.getClassLoader().loadClass(name).asSubclass(View.class);
                constructor = clazz.getConstructor(mConstructorSignature);
                // Cache the class object of View
                mConstructorMap.put(name, constructor);
            } catch (Throwable e) {
            }
        }
        return constructor;
    }
      
    private View createViewFormTag(String name, Context context, AttributeSet attrs) {
        // Include custom controls
        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);
    }
}

But is it really that simple?

The above code has formed the rudiment of resource replacement and basically completed a basic resource replacement process.

After the subsequent debugging and spot check process, I found that this is just beginning to enter the pit.

Five, explore

Exploration 1: api restricted calls

As soon as the demo runs, it finds that there are many alarm messages printed in the log.

Because it is the reflection method that replaces the Resource, it also triggers Google's Api restricted call mechanism, so I studied the Api restricted call.

Conclusion:

The system signature application is not limited for the time being, because the demo uses debugging signature. After the system signature is replaced, the alarm disappears.

Exploration 2: performance test

Using plugapk package in sd card to generate plugsources is mainly in the process of generating assetmanager, which takes 10-15ms. For page startup, this time is too long, so we try to cache assetmanager and shorten the time.

After the reflection replacement resource is completed, call the getText method of PlugResources. First get the name and type of the original resource from the local Resources according to the Id, and then use the name and type to call getidentifier to get the resId in PlugResources. This process takes a long time. Although it is also nanosecond level, it takes a data level higher than that in the no hook scenario.

However, fortunately, in the page fluency performance test, we did not find that the fluency has declined, and the page startup speed has not declined significantly.

Exploration 3: system version compatibility

The real big hole is coming.

After solving the previous problems, I started to enter the monkey test. During the test, I found that machines above 7.0 will crash as long as I press the content in the webView interface to pop up the copy and paste dialog box. It can be seen from the log that webView resources are not found. If I try to live in this crash, the string displayed in the original resource location will become an id tag similar to @ 1232432 .

google searched for half a day and found that there were few relevant materials. It seems that we need to understand the logic of webView resource loading from the source level.

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

Question: why the 6.0 system can use this scheme without webView problem, but the system above 7.0 will crash, and what is the specific difference between 6.0 and 7.0 resource management.

To get the answer, you need to read the source code of resources above 6.0 and 7.0, starting from the source code of 6.0.

1. 6.0 source code analysis of resource management

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;
       . . . . . . . . . . . 
   }

At the beginning of Context creation, the Resource has been created.

There are two places where Resource creation is involved

  1. resources =packageInfo.getResources(mainThread);

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

First, from packageInfo.getResources(mainThread); speaking of packageInfo is actually LoadedApk

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;
}

See ActivityThread again

getTopLevelResources method of ActivityThread

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());

In fact, mResourcesManager.getTopLevelResources is called

Android M's ResourcesManager is relatively simple to write

There is a Resource cache inside

The getTopLevelResource method assembles a key using the parameters passed in

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

Use this key to search in the cache and use it when you find it.

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

Can't find to create a new assets to generate a Resource instance

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.");
}
}
}
}

Another function of the cache is to find all the currently active resources from the cache when the configuration changes.

And call the public void updateconfiguration (configuration config, displaymetric metrics, CompatibilityInfo compat) {method of these resources, and the configuration of the meassets in the Resource is finally effective

Let's take a look at Resource.java

Its core consists of two parts

1: Encapsulate Assets, which means that all resource calls are ultimately methods that are called to meassets

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: Provide 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);
//Cache the large resources extracted from the massarts to avoid reading time and memory consumption

After reading the source code of 6.0, we will find a 9.0 code to see that 9.0 resource management is basically the same as 7.0, so we directly use the source code of 9.0 for analysis.

Compared with Android 6.0, AssertManager is not maintained in Resources in the 9.0 source code. Instead, AssertManager and other caches are encapsulated 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;
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 takes the responsibility of Resources in the old version, packaging AssertManager and maintaining data cache.

The code of Resources is also simpler, and its method calls are ultimately implemented by ResourcesImpl.

What remains unchanged is whether Resources management should be handed over to Resources manager. Like Android 6.0, Resources manager is a singleton mode.

What's the difference between 9.0's ResourcesManager and 6.0's ResourcesManager?

From the start of the application, it's still a familiar ContextImpl.

2. 9.0 source code analysis of resource management

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;
 }

Whether it is the Resource generating Application or the Resource generating Activity, the final call is the method difference in ResourceManager. It's a call to

ResourcesManager.getInstance().getResources. Another call is resourcesManager.createBaseActivityResources.

OK, let's take a look at the source code of ResourcesManager.

First of all, let's look at the various attributes it provides. Let's pick the important ones.

 /**
     * ResourceImpls And its configuration. These are data that occupy a large amount of memory
     * It should be reused as much as possible. All ResourcesImpl generated by ResourcesManager will be cached in this map
     */
    private final ArrayMap<ResourcesKey, WeakReference<ResourcesImpl>> mResourceImpls = new ArrayMap<>();
   
    /**
     *A list of Resource references that can be reused. Note that this list does not store the Activity Resources cache. According to my understanding, all non Activity Resources will be cached here, such as Application Resources
     */
    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.
        In this cache, the cache of Resources of Activity is stored. ActivityResources is an object, which contains the Configuration of an Activity and all possible Resources, such as an Activity. In some cases, its ResourcesImpl has changed. Then ActivityResources may hold multiple Resource references
     */
    private final WeakHashMap<IBinder, ActivityResources> mActivityResourceReferences = new WeakHashMap<>();
   
    /**
     * The cached ApkAssets can be ignored first
     */
    private final LruCache<ApkKey, ApkAssets> mLoadedApkAssets = new LruCache<>(3);
   
    /**
     * This is also a cache of ApkAssets, which can be ignored first
     */
    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;
    }
   
    /**
     * Resource and basic configuration overrides associated with the Activity.
     */
    private static class ActivityResources {
        public final Configuration overrideConfig = new Configuration();
//According to the general understanding, an Activity has only one Resource, but a list is used to store it. This is to consider that if the Activity changes and a Resource is regenerated, the list will store all the Resources used in the Activity history. Of course, if no one holds these Resources, they will be recycled
        public final ArrayList<WeakReference<Resources>> activityResources = new ArrayList<>();
    }

With these important properties in mind, let's take a look at the many methods provided by the ResourceManager.

ResourceManager provides the following to write public methods for calling.

First, we can see that getResources and createBaseActivityResources use a ResourcesKey to call 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) {
            // Force the creation of ActivityResources object and put it in 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 {
  
    }
}

getOrCreateResources I wrote comments at each line of code. Please pay attention to the comments in the code. Some of the comments are the translation of the citation comments 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);
  
            // Clean up cache that has been recycled
            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);
            }
            //Get a ResourcesImpl according to the corresponding key. It may be new or in the cache
            ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
            if (resourcesImpl != null) {
                //Use ResourcesImpl to generate a resource
                return getOrCreateResourcesForActivityLocked(activityToken, classLoader, resourcesImpl, key.mCompatInfo);
            }
  
            // We will create the ResourcesImpl object outside of holding this lock.
  
        } else {
            // Clean up because the mResourceReferences contain weak references. To determine whether these weak references have been released, remove them from the Array
            ArrayUtils.unstableRemoveIf(mResourceReferences, sEmptyReferencePredicate);
  
            // Do not depend on Activity. Find the shared resource with correct ResourcesImpl. Here is to find it in the cache of mResourceImpls according to the key
            ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
            if (resourcesImpl != null) {
                //If resourcesimpl is found, go to mResourceReferences to see if there are available resources. If the classloader and resourcesimpl are the same, get the existing resources object. Otherwise, a new resources object will be created.
                return getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
            }
  
            // We will create a ResourcesImpl object outside of holding this lock.
        }
  
        // If we get here, we can't find the right ResourcesImpl to use, so create one now.
        ResourcesImpl resourcesImpl = createResourcesImpl(key);
        if (resourcesImpl == null) {
            return null;
        }
  
        // Add this ResourcesImpl to the cache.
        mResourceImpls.put(key, new WeakReference<>(resourcesImpl));
  
        final Resources resources;
        if (activityToken != null) {
            //Find out whether there are appropriate Resources available from the mpactivityresourcereferences. If not, build a Resources soldier to add to the mpactivityresourcereferences
            resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader, resourcesImpl, key.mCompatInfo);
        } else {
            //Use the created ResourcesImpl to match a Resource. Whether to take (if any) from the cached mResourceReferences or create a new one depends on the following methods
            resources = getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
        }
        return resources;
    }
}

Draw a flow chart

After reading the figure, we can basically use the following code cache of resourcesmanager to see what objects are contained in the cache when an App starts and an Activity is opened.

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

The printed results are as follows:

 

After analyzing the source code of Resource management for two different API levels, let's analyze the difference between two different apilevels after loading a webView component.

Let's start with 6.0.

According to the code of 6.0 ResourceManager, let's do a test first:

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

3. 6.0 analysis of web resource injection

ResourcesManager resourcesManager = ResourcesManager.getInstance();
//6 printing
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();
}

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@f911117

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

Modify the code again:

Add webView to initialize WebView webView = new WebView(context) before printing;

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.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

You can see that after adding the webView initialization code, an instance of Resources is added to the activeresources, which points to the webView component installation path.

WebView gets the Resources it needs from this resource. This is also the reason why the Resources replacing Activity and Application in version 7.0 and below will not crash the web components, because in this level system, the web component Resources are separated from the main apk Resources.

OK, look at 9.0 after analyzing 6.0.

9.0's ResourceManager is relatively complex. We also use the reflection method to print out the ResourceManager data in both cases.

Write print code.

4. 9.0 analysis of web resource injection

System.out.println(" Printing mResourceImpls Cached ResourceImpl");
ResourcesManager resourcesManager = ResourcesManager.getInstance();
// 9 source code
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(" Printing mActivityResourceReferences Cached 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();
}

Printout in this print code, we output the data in mResourceImpls and mpactivityresourcereferences. If you don't understand these two caching functions, you can go to the previous article.

I/System.out:  Printing mResourceImpls Cached 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:  Printing mActivityResourceReferences Cached Activity Resources
I/System.out: android.content.res.ResourcesImpl@1b73b0

According to the activityresource in the mpactivityresourcereferences, we find the corresponding ResourcesImpl and know the content of ResourcesImpl according to the 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-keysexposed-nokeys-navhidden-nonav-v28 mCompatInfo={480dpi always-compat}}

Next, we add the source code to initialize webView before printing the code. webView = new WebView(context);

I/System.out:  Printing mResourceImpls Cached 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:  Printing mActivityResourceReferences Cached Activity Resources
I/System.out: android.content.res.ResourcesImpl@70dd909

Similarly, according to the activityresource in the mpactivityresourcereferences, we find the corresponding ResourcesImpl and know the content of ResourcesImpl according to the ResourceKey.

Compared with the code before webview instantiation, we found that / data/app/com.android.chrome-dO2jAeCdfgkLjVHzK2yx0Q==/base.apk was added in mLibDirs

Conclusion: in the 9.0 source code, android added the Web component Resource as libDir to asset for Resource lookup, without the way of Resource separation.

After understanding this reason, we further look for the place where libDir adds web component resources.

webView calls the addWebViewAssetPath method of 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 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);
    }
}

The final method called is ResourcesManager.getInstance().appendLibAssetForMainAssetPath(appInfo.getBaseResourcePath(), newAssetPath);

The first one is the respath of the current application and the second one is the respath of webView. See the following source code annotation for details.

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();
        //Traversing all ResourcesImpl ResourcesImpl is the core of recovery. The relationship between them is that Resource contains ResourcesImpl contains 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;
            //The first step is to determine whether the ResourcesImpl contains the assetPath. That is to say, if the mresir of a ResourcesImpl is not currently applied, it will not be processed
            if (impl != null && Objects.equals(key.mResDir, assetPath)) {
                //It is also necessary to determine whether the new resource path already exists, and if so, do not handle it
                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) {
                        //Here, add the new path to the libDir of the ResourcesKey corresponding to the ResourcesImpl to be added
                        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);
    }
}
//This method is to update the Resource that currently holds ResourcesImpl
 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) {
             //First, find a new ResourcesKey according to the old ResourcesImpl
             final ResourcesKey key = updatedResourceKeys.get(r.getImpl());
             if (key != null) {
                 //Then generate a new ResourcesImpl according to the new ResourcesKey
                 final ResourcesImpl impl = findOrCreateResourcesImplForKeyLocked(key);
                 if (impl == null) {
                     throw new Resources.NotFoundException("failed to redirect ResourcesImpl");
                 }
                 //Finally, replace ResourcesImpl in Resources
                 r.setImpl(impl);
             }
         }
     }
  
     // Update any references to ResourcesImpl that require reloading for each Activity.
     //This is the same as the above, except that all recorded Activity resources are processed here
     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);
                 }
             }
         }
     }
 }

When the appendLibAssetForMainAssetPath method is called, the logical order is as follows. If you don't like the source code, you'd better draw a flowchart.

 

In this way, WebView adds the Resource of WebView to the Resource of Activity.

Final solution

In fact, we have analyzed the reasons why WebView crashes due to the lack of resources in machines above 7.0.

In the Resource replacement scheme, we replaced the Resource of Context with our ProxyResources, but ProxyResources is not managed by the Resource manager, that is to say, our ProxyResources are not updated when webView resources are injected.

After understanding all the principles, the solution is clear at a glance.

See the following code:

// step 4 merges the Resources of the agent into the ResourcesManager for unified control. Because the ResPath of our proxyresources is the path of the application, the webView Resources will be synchronized into the Res when they are injected
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    synchronized (ResourcesManager.getInstance()) {
        //You don't need to worry about the increase of the number caused by the continuous addition in the list, because the weak references are added. If the page is closed, it will be recycled automatically
        ArrayList<WeakReference<Resources>> list = Reflector.with(ResourcesManager.getInstance()).field("mResourceReferences").get();
        list.add(new WeakReference<Resources>(textRepairProxyResourcess));
    }
}

At this point, the webView crash problem is resolved.

6, Problem review

Question 1:

Why do you want to replace Resource with reflection in attachBaseContext?

Answer:

No matter the Application or Activity mResources are replaced, they must hook the baseContext in attachBaseContext. It is not successful to hook the Activity or Application itself directly because the Activity or Application itself is not a Context, it is just a ContextWapper. The real Context in ContextWapper is actually assigned at attachBaseContext.

Question two:

Since the Resource of Activity and Application has been replaced, why use factory to process layout initialization? Isn't layout initialization using the Resource in Activity?

Answer:

We have replaced the mResources of Activity or Application, but if we do not implement the activitylifecyclecallbacks in process 5, the text written in XML cannot be replaced, because View does not directly use mResources when using TypedArray to assign values, It uses mResourcesImpl directly, so it's useless to hook mResources directly. In fact, the getText method of mResources also calls the mResourcesImpl method in mResources.

Question three:

How to update String online for apps (such as browsers) that have already used skin changing mode?

Answer:

You only need to modify SkinProxyResource used in the original skin changing mode, and use getText, getString and other methods to proxy to TextProxyResources updated online.

More content, please pay attention to vivo Internet technology WeChat official account.

Note: please contact Labs2020 for reprint.

Keywords: Mobile Android Apache Google xml

Added by liamloveslearning on Tue, 07 Apr 2020 13:20:15 +0300