Android implements hot repair based on DexClassLoader

Keywords: Android jvm less

This paper briefly introduces a kind of implementation of thermal repair, plug-in principle can also be referred to.

Class Loader in Android System

  • Android shields ClassLoader's findClass loading method, so how does it implement its own class loading?

  • There are two classloaders in Android system: PathClassLoader and DexclassLoader.

  • PathClassLoader and DexClassLoader are both inheritance and BaseDexClassLoader, BaseDexClassLoader inheritance and ClassLoader.

Why does Android derive two different subclasses from its class loader? What are their respective uses?

Comparison of Path Class Loader and DexClass Loader

PathClassLoader

public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    public PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {
        super(dexPath, null, libraryPath, parent);
    }
}

DexClassLoader

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

Constructor of BaseDexClassLoader

    /**
     * Constructs an instance.
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android
     * @param optimizedDirectory directory where optimized dex files
     * should be written; may be {@code null}
     * @param libraryPath the list of directories containing native
     * libraries, delimited by {@code File.pathSeparator}; may be
     * {@code null}
     * @param parent the parent class loader
     */
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

Optimized Directory: Formulate odex files optimized for output dex, which can be null
libraryPath: Dynamic library path (to be added to the app dynamic library search path list)
Parent: Develop a parent loader to ensure parent delegation so that each class is loaded only once.

It can be seen that the difference between PathClassLoader and DexClassLoader lies in the optimization directory and libraryPath parameters in the constructor. In Path Class Loader, optimized Directory and libraryPath are null, in DexClass Loader, new File (optimized Directory) and libraryPath.

optimizedDirectory
The constructor of BaseDexClassLoader creates a DexPathList using the OptidDirectory. Look at the OptidDirectory in the DexPathList:

public DexPathList(ClassLoader definingContext, String dexPath,
        String libraryPath, File optimizedDirectory) {
    /******Partial Code Elimination******/
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                       suppressedExceptions);
    /******Partial Code Elimination******/
}
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
                                         ArrayList<IOException> suppressedExceptions) {
   /******Partial Code Elimination******/
    for (File file : files) {
        /******Partial Code Elimination******/
        if (file.isDirectory()) {
           /******Partial Code Elimination******/
        } else if (file.isFile()){
            if (name.endsWith(DEX_SUFFIX)) {
                // Raw dex file (not inside a zip/jar).
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException ex) {
                    System.logE("Unable to load dex file: " + file, ex);
                }
            } else {
                zip = file;
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException suppressed) {
                    suppressedExceptions.add(suppressed);
                }
            }
        } else {
            System.logW("ClassLoader referenced unknown path: " + file);
        }
        if ((zip != null) || (dex != null)) {
            elements.add(new Element(file, false, zip, dex));
        }
    }
    return elements.toArray(new Element[elements.size()]);
}

private static DexFile loadDexFile(File file, File optimizedDirectory)
        throws IOException {
    if (optimizedDirectory == null) {
        return new DexFile(file);
    } else {
        String optimizedPath = optimizedPathFor(file, optimizedDirectory);
        return DexFile.loadDex(file.getPath(), optimizedPath, 0);
    }
}

From the above, we can draw the following conclusions.

DexClassLoader: Ability to load custom jar/apk/dex
PathClassLoader: Only apk s installed in the system can be loaded
So the default class loader for Android system is PathClassLoader, and DexClassLoader can provide dynamic loading just like ClassLoader for JVM.

Parent Delegation Mechanism for Class Loading in Android System

ClassLoader's loadClass Method in Android 5.1 Source Code

protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(className);

        if (clazz == null) {
            ClassNotFoundException suppressed = null;
            try {
                //Let the parent loader load first
                clazz = parent.loadClass(className, false);
            } catch (ClassNotFoundException e) {
                suppressed = e;
            }
            //When the class loader of all parent nodes fails to find the class, the current loader calls the findClass method to load.
            if (clazz == null) {
                try {
                    clazz = findClass(className);
                } catch (ClassNotFoundException e) {
                    e.addSuppressed(suppressed);
                    throw e;
                }
            }
        }

If you want to load classes dynamically, can you use the method of custom ClassLoader and custom ClassLoader in parent delegation mechanism to load the class file you defined? Look at the findClass method of ClassLoader in Android source code:
protected Class<?> findClass(String className) throws ClassNotFoundException {
throw new ClassNotFoundException(className);
}
This method throws a "ClassNotFoundException" exception directly, so it's not possible to load classes in Android in this way.

summary

ClassLoader's loadClass method guarantees a parent delegator.
BaseDexClassLoader provides two derived classes that allow us to load custom classes.

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Repair process



The first of the two pictures is the repair process. The second one is written in detail in the picture. It is no longer verbose. The following program achieves the above functions.

The first step is to copy the bug fix package classes2.dex to the private directory of the program.

 /**
     * Repair Principle
     * Using DexClassLoader: Ability to load an uninstalled jar/apk/dex
     * DexClassLoader ->DexPathlist->dexElements[]
     * Merge the bug-fixing dex package with dexElement [] into a new dex and put fix.dex in front of the array
     * Replace the original dexElements [] with the merged dexElements [] by reflection, so that the old class file with bug s is not loaded.
     * @param view
     */
    public void fixBug(View view) {
        //Repair bug
        //1. Repair package classes2.dex
        String dexName = "classes2.dex";
        File sourceFile = new File(Environment.getExternalStorageDirectory(),dexName);

        //Application directory specified by DexClassLoader
        File targetFile = new File(getDir("odex", Context.MODE_PRIVATE).getAbsolutePath()+File.separator+dexName);
        if(targetFile.exists()){
            targetFile.delete();
        }
        //Copy to Private Path
        FileUtil.copyFile(sourceFile,targetFile);

        FixDexUtil.loadFixedDex(this);

    }

The second part is to merge the dex array, merge our repaired package classes2.dex with the dexElement of the current app, and generate a new DexElement.

The following is mainly based on reflection to replace the bug class.

Get the DexElement in the DexPathList and replace the merged NewDexElement with the original one. This achieves the purpose of repair, which is also a principle of plug-in implementation.

     //Directories where dex files need to be written
        String optimizeDir = filesDir.getAbsolutePath()+File.separator+"opt_dex";
        File fileOpt = new File(optimizeDir);
        if(!fileOpt.exists()){
            fileOpt.mkdirs();
        }
        //1. Get Path Class Loader to load application dex
        PathClassLoader systemClassLoader = (PathClassLoader) context.getClassLoader();
        for (File dexFile : loadedDex) {
            //DexClassLoader construction parameters
            //String dexPath,dex path
            // String optimized Directory, which makes odex files optimized for output dex, can be null
            // String library SearchPath, so path dynamic library path (to be added to the app dynamic library search path list)
            // ClassLoader parent develops a parent loader to ensure a parent delegation mechanism so that each class is loaded only once.

            DexClassLoader dexClassLoader = new DexClassLoader(
                    dexFile.getAbsolutePath(),
                    optimizeDir,
                    null,
                    systemClassLoader
                    );

            //3. merge dex
            try {
                Object dexObj = ReflectUtil.getPathList(dexClassLoader);
                Object pathObj = ReflectUtil.getPathList(systemClassLoader);
                Object fixDexElements = ReflectUtil.getDexElements(dexObj);
                Object pathDexElements = ReflectUtil.getDexElements(pathObj);
                //Merge two arrays
                Object newDexElements = ReflectUtil.combineArray(fixDexElements,pathDexElements);
                LogUtil.log("combineArray>>>");
                //Reassign to the exElements array in PathClassLoader

                Object pathList = ReflectUtil.getPathList(systemClassLoader);

                ReflectUtil.setField(pathList,pathList.getClass(),"dexElements",newDexElements);
                LogUtil.log("Repair completed>>>");
                LogUtil.log("Repair completed>>>"+pathList+"   ");
            } catch (Exception e) {
                e.printStackTrace();
                LogUtil.log("doDexInject Exception>>>"+e.toString());
            }

        }

The complete code will not be pasted. Here's what I want to see.

Welcome learning partners to make progress together: 230274309. Share and progress together! Less paddling and more drying!! Welcome everyone!!! (Do not add divers into the group)

Posted by phorcon3 on Thu, 26 Sep 2019 06:47:51 -0700