Introduction to the

Hotfix refers to changing the behavior of an application by replacing some files without reinstalling APK to modify the code. In China, most of them are realized by means of reflection to ClassLoader.

Google has officially provided a new solution: Android App Bundle, but due to the Domestic Internet environment, it has not been effectively promoted. Instead, various hot update plug-ins on the market, such as Tinker of Tencent wechat, and the complete PaaS solution: Bugly App upgrade, have been replaced. It’s important to note that Google Play prohibits apps with hot updates.

Generally speaking, the domestic hot repair program is nothing more than downloading the patch file through IO operation, replacing it in some ways, and the repair will be completed by the next startup.

Knowledge base: Reflection, parent delegate pattern, class loading mechanism, Java IO operations

Extension: Android incremental update

MultiDex

Official documentation: Enable MultiDex for applications with more than 64K methods

Simply put, MultiDex is a compilation of class files into multiple dex, intended to bypass the dex method for large applications, load other dex files at run time. In this case, there are multiple dex in an APK. Generally, only the first dex is loaded at startup, and the other dex will be loaded by ClassLoader in onCreate of Application. Then the key technology of hot repair lies in the ClassLoader, through the ClassLoader to load the replacement dex file.

Principle of class substitution

Class replacement, the core is to use the ClassLoader to load the replacement class, ClassLoader will be compiled to load the virtual machine.

Classloaders are divided into Java and Android classloaders, because in Android virtual machines (ART and DVM), dex files are loaded instead of JAR and class files. This article introduces ClassLoader in Android. ClassLoader in Android includes BootClassLoader, PathClassLoader and DexClassLoader.

For details about ClassLoader in Java, see:

Android解析ClassLoader(一)Java中的ClassLoader

Android解析ClassLoader(二)Android中的ClassLoader

BootClassLoader

The Android system uses BootClassLoader to preload common classes during startup. BootClassLoader is an internal class modified by ClassLoader with default, and applications cannot be called directly.

PathClassLoader

The Android system uses PathClassLoader to load dex-related files in the local file system. It is generally used to load system classes and application classes. Only dex files in apK/JAR can be loaded.

/**
 * Provides a simple {@link ClassLoader} implementation that operates on a list
 * of files and directories in the local file system, but does not attempt to
 * load classes from the network. Android uses this class for its system class
 * loader and for its application class loader(s).
 */
public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null.null, parent);
    }

    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }

    / * * *@hide* /
    @libcore.api.CorePlatformApi
    public PathClassLoader( String dexPath, String librarySearchPath, ClassLoader parent, ClassLoader[] sharedLibraryLoaders) {
        super(dexPath, librarySearchPath, parent, sharedLibraryLoaders); }}Copy the code

PathClassLoader inherits from BaseDexClassLoader, and the methods visible from the code are in the parent class, following the parent delegate pattern.

The PathClassLoader takes up to three arguments:

  • DexPath: a set of paths for apK files or JAR files containing dex. Multiple paths are separated by file delimiters (:) by default.
  • LibrarySearchPath: a collection of paths containing C/C++ Native libraries. Multiple paths are separated by file delimiters, which can be null.
  • Parent: indicates the parent of the ClassLoader.

DexClassLoader

DexClassLoader is used to load the dex file from the APK/JAR file. It can also load the dex from a JAR package or an uninstalled APK and can be customized by users. Therefore, DexClassLoader is a key class for hot repair. After API 26, the dex files need to be stored in the private folder of the application. You can obtain the folder path by using context.getCodecachedir (). You can load the dex files that need to be hot repaired by using DexClassLoader.

/**
 * A class loader that loads classes from {@code .jar} and {@code .apk} files
 * containing a {@code classes.dex} entry. This can be used to execute code not
 * installed as part of an application.
 *
 * <p>Prior to API level 26, this class loader requires an
 * application-private, writable directory to cache optimized classes.
 * Use {@code Context.getCodeCacheDir()} to create such a directory:
 * <pre>   {@code
 *   File dexOutputDir = context.getCodeCacheDir();
 * }</pre>
 *
 * <p><strong>Do not cache optimized classes on external storage.</strong>
 * External storage does not provide access controls necessary to protect your
 * application from code injection attacks.
 */
public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent); }}Copy the code

The constructor argument has an extra optimizedDirectory, but it’s deprecated in API 26, so you don’t need to worry too much. The other parameters are the same as in PathClassLoader.

The key way to

In DexClassLoader, the parent BaseDexClassLoader is mainly used for class loading, starting from the code of BaseDexClassLoader:

/**
 * Base class for common functionality between various dex-based
 * {@link ClassLoader} implementations.
 */
public class BaseDexClassLoader extends ClassLoader {

    // Ignore some code
    @UnsupportedAppUsage
    private final DexPathList pathList;
    
    / /...
    @Override
    protectedClass<? > findClass(String name)throws ClassNotFoundException {
        // First, check whether the class is present in our shared libraries.
        if(sharedLibraryLoaders ! =null) {
            for (ClassLoader loader : sharedLibraryLoaders) {
                try {
                    return loader.loadClass(name);
                } catch (ClassNotFoundException ignored) {
                }
            }
        }
        // Check whether the class in question is present in the dexPath that
        // this classloader operates on.
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException(
                    "Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }
    
    / /...
}
    
Copy the code

It can be seen that DexClassLoader maintains a DexPathList. In DexPathList, findClass method is used to find the corresponding class. Then let’s look at the code of DexPathList:

/**
 * A pair of lists of entries, associated with a {@code ClassLoader}.
 * One of the lists is a dex/resource path &mdash; typically referred
 * to as a "class path" &mdash; list, and the other names directories
 * containing native code libraries. Class path entries may be any of:
 * a {@code .jar} or {@code .zip} file containing an optional
 * top-level {@code classes.dex} file as well as arbitrary resources,
 * or a plain {@code .dex} file (with no possibility of associated
 * resources).
 *
 * <p>This class also contains methods to use these lists to look up
 * classes and resources.</p>
 *
 * @hide* /
public final class DexPathList {
    private static final String DEX_SUFFIX = ".dex";
    private static final String zipSeparator = ! "" /";

    /** class definition context */
    @UnsupportedAppUsage
    private final ClassLoader definingContext;

    /** * List of dex/resource (class path) elements. * Should be called pathElements, but the Facebook app uses reflection * to modify 'dexElements' (http://b/7726934). */
    @UnsupportedAppUsage
    private Element[] dexElements;

    /** List of native library path elements. */
    // Some applications rely on this field being an array or we'd use a final list here
    @UnsupportedAppUsage
    /* package visible for testing */ NativeLibraryElement[] nativeLibraryPathElements;

    /** List of application native library directories. */
    @UnsupportedAppUsage
    private final List<File> nativeLibraryDirectories;

    /** List of system native library directories. */
    @UnsupportedAppUsage
    private final List<File> systemNativeLibraryDirectories;
    
    / /...
    
    /**
     * Finds the named class in one of the dex files pointed at by
     * this instance. This will find the one in the earliest listed
     * path element. If the class is found but has not yet been
     * defined, then this method will define it in the defining
     * context that this instance was constructed with.
     *
     * @param name of class to find
     * @param suppressed exceptions encountered whilst finding the class
     * @return the named class or {@code null} if the class is not
     * found in any of the dex files
     */
    publicClass<? > findClass(String name, List<Throwable> suppressed) {for(Element element : dexElements) { Class<? > clazz = element.findClass(name, definingContext, suppressed);if(clazz ! =null) {
                returnclazz; }}if(dexElementsSuppressedExceptions ! =null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }
    
    / /...
}
Copy the code

You can see that a dexElements member variable is maintained in DexPathList, iterated sequenced in the findClass method. That said, this is the key to hotfix. Developers only need to modify dexElements to hotfix operations.

Code practice

Generate dex

The Android SDK provides the tool dx for generating dex files. The tool is located in the build-tools directory of the SDK directory. This tool can be found in any folder of the SDK version. To generate the dex file, run the following command:

dx --dex --no-strict --output out.dex test.class

Dex is a dex file generated by test.class, which can be named by itself. In addition, the test.class parameter can be used as a directory, that is, all the classes in the directory can be generated by dex files.

Into the patch

Note file read and write permissions: Android 10 partition storage introduction and Baidu APP adaptation practice

From the above class replacement principle, we can know that we need to obtain dexElements of DexPathList for modifying the array, and keep the original array at the same time. Therefore, we need to obtain the array of PathClassLoader, and then customize the DexClassLoader. The DexClassLoader array is defined and the two arrays are combined. Since the corresponding classes are read in sequence, the array of the custom patch should come first. The general idea is that the code looks like this, with details in the comments:

public static void loadDex(Context context) {
    if (context == null) {
        return;
    }
    File filesDir = context.getCodeCacheDir();
    File[] listFiles = filesDir.listFiles();
    
    // Filter non-dex files
    for (File file : listFiles) {
        if (file.getName().startsWith("classes") || file.getName().endsWith(".dex")) {
            Log.d(TAG, "dexName:"+ file.getName()); mLoadedDex.add(file); }}// Walk through the file to join
    for (File dex : mLoadedDex) {
        try {
            // Get the system class loaded by the PathClassLoader, etc
            PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
            Class baseDexClassLoader = Class.forName("dalvik.system.BaseDexClassLoader");
            Field pathListFiled = baseDexClassLoader.getDeclaredField("pathList");
            pathListFiled.setAccessible(true);
            Object pathListObject = pathListFiled.get(pathClassLoader);

            Class systemDexPathListClass = pathListObject.getClass();
            Field systemElementsField = systemDexPathListClass.getDeclaredField("dexElements");
            systemElementsField.setAccessible(true);
            Object systemElements = systemElementsField.get(pathListObject);

            // Custom DexClassLoader Defines the patch dex to be loaded. You can separate multiple dex with colons (:) to avoid traversal
            DexClassLoader dexClassLoader = new DexClassLoader(dex.getAbsolutePath(), null.null, context.getClassLoader());
            Class customDexClassLoader = Class.forName("dalvik.system.BaseDexClassLoader");
            Field customPathListFiled = customDexClassLoader.getDeclaredField("pathList");
            customPathListFiled.setAccessible(true);
            Object customDexPathListObject = customPathListFiled.get(dexClassLoader);

            Class customPathClass = customDexPathListObject.getClass();
            Field customElementsField = customPathClass.getDeclaredField("dexElements");
            customElementsField.setAccessible(true);
            Object customElements = customElementsField.get(customDexPathListObject);

            // Merge arraysClass<? > elementClass = systemElements.getClass().getComponentType();int systemLength = Array.getLength(systemElements);
            int customLength = Array.getLength(customElements);
            int newSystemLength = systemLength + customLength;

            // Generate a new array of type Element
            Object newElementsArray = Array.newInstance(elementClass, newSystemLength);
            for (int i = 0; i < newSystemLength; i++) {
                if (i < customLength) {
                    Array.set(newElementsArray, i, Array.get(customElements, i));
                } else{ Array.set(newElementsArray, i, Array.get(systemElements, i - customLength)); }}// Overwrite the new array
            Field elementsField = pathListObject.getClass().getDeclaredField("dexElements");
            elementsField.setAccessible(true);
            elementsField.set(pathListObject, newElementsArray);
        } catch(Exception e) { e.printStackTrace(); }}}Copy the code

Note that the patch dex file should be placed in CodeCacheDir so that the DexClassLoader can access it (API 26)

The resources

Android App hot patch dynamic repair technology introduction – Qzone technical team

Hotfix getting started: ClassLoader in Android

Brief introduction to the Android Dex file