Public number: [Android old skin] hope to write things can help you 🤣🤣

Android hot repair core principle, ClassLoader class loading

[TOC] Discussion on Android frontier technology: Application of ClassLoader in hot repair

Writing a bug again? Although this sentence is a joke, but also because we are human, not god, but also cannot consider everything perfect, bugs are inevitable. So what do we do with Android when we have bugs?

We usually rush out a release when we get a Bug early on. However, this Bug may be a simple line of code for which iterating a version with full or incremental updates is overkill. And it takes time for the new version to spread, and what if this new version has a minor problem?

So in order to solve this problem, hot repair comes in.

Thermal repair, now everyone should be familiar with. Since 2016, hotfixes have been a hot topic in the Android tech community for a while, and the idea of fixing online bugs without releasing a new version seems pretty dark tech.

The purpose of this chapter is not to focus on hot fixes per se, but to familiarize ourselves with the core of hot fixes: the classloading mechanism. (

ART and Dalvik

DVM is also a virtual machine that implements the JVM specification and uses the CMS garbage collector by default, but instead of the JVM running Class bytecode, DVM executes Dex(Dalvik Executable Format) — a compression Format designed for Dalvik. Dex files are the result of many.class files being processed and compressed, and can eventually be executed in the Android runtime environment.

ART (Android Runtime) is a developer option introduced in Android 4.4 and is the default Android Runtime for Android 5.0 and later. ART and Dalvik are compatible runtimes that run Dex bytecode, so applications developed for Dalvik can also run in the ART environment.

Source. The android. Google. Cn/devices/tec…

Dexopt and dexaot

  • dexopt

    In Dalvik, when the VM loads a DEX file, it verifies and optimizes the dex file, and the optimization result of the DEX file becomes Odex (Optimized dex), which is similar to the DEX file except that some optimization opcodes are used.

  • dex2oat

    ART precompile mechanism, after performing dexopt optimization on dex files at installation time, odex is AOT precompile into OAT (actually ELF files) executable files (machine code). (Compared with ODEX optimization, it takes longer to convert unoptimized DEX to OAT)

This introduction

Any Java program is made up of one or more class files that need to be loaded into the JVM when the program is running. Java’s class loading mechanism is responsible for loading these class files. The function of a ClassLoader is simply to load a class file for application runtime. Each Class object has a classLoader field inside it to identify which classLoader loaded it.

class Class<T> { ... private transient ClassLoader classLoader; . }Copy the code

ClassLoader is an abstract class, and its concrete implementation classes are mainly:

  • BootClassLoader

    Used to load the Android Framework layer class file.

  • PathClassLoader

    Class loader for Android applications. You can load the specified dex, as well as classes. Dex in JAR, ZIP, and APK

  • DexClassLoader

    Load the specified dex, as well as classes. Dex in JAR, ZIP, and APK

    Many blogs say that the PathClassLoader can only load the dex of the installed APK, but it should be on the Dalvik VM.

    But dalvik is generally not a concern these days.

    The e (TAG, "Activity. The class by:" + Activity. Class. GetClassLoader () + "load"); Log.e(TAG, "mainActivity.class" from: "+ getClassLoader() +" load "); 1 / / output: Activity class by: Java. Lang. BootClassLoader @ d3052a9 loading MainActivity. Class by: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.enjoy.enjoyfix-1/base.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.enjoyfix-1/lib/x86, / system/lib/vendor/lib]]] loadingCopy the code

    The relationship between them is as follows:

The common parent of PathClassLoader and DexClassLoader is BaseDexClassLoader.

public class DexClassLoader extends BaseDexClassLoader { public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) { super(dexPath, new File(optimizedDirectory), librarySearchPath, parent); } } 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); }}Copy the code

The only difference you can see is that creating DexClassLoader requires passing an optimizedDirectory argument, which is created as a File object and passed to super, whereas PathClassLoader is passed directly to NULL. So both can load the specified dex, as well as classes.dex in JAR, ZIP, and APK

PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/xx.dex", getClassLoader());
​
File dexOutputDir = context.getCodeCacheDir();
DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/xx.dex",dexOutputDir.getAbsolutePath(), null,getClassLoader());
Copy the code

In fact, the optimizedDirectory parameter is the output directory of dexopt (odex). When the PathClassLoader is created, the directory is null, which means no dexopt. No, the default path for null optimizedDirectory is /data/dalvik-cache.

DexClassLoader optimizedDirectory is deprecated in API 26.

public DexClassLoader(String dexPath, String optimizedDirectory,
                    String librarySearchPath, ClassLoader parent) {
    super(dexPath, null, librarySearchPath, parent);
}
Copy the code

. Same as PathClassLoader!

Parental delegation mechanism

You can see that creating a ClassLoader requires receiving a ClassLoader parent parameter. The purpose of this parent is to implement parent delegation for class loading. That is:

When a class loader receives a request to load a class, it first delegates the loading task to the parent class loader, recursively. If the parent class loader can complete the class loading task, it returns successfully. Only if the parent class loader is unable to complete the load task, do the load itself.

protected Class<? > loadClass(String name, Boolean resolve) throws ClassNotFoundException{// Check if the class is loaded class c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent ! C = parent.loadClass(name, false); c = parent.loadClass(name, false); C = findBootstrapClassOrNull(name); }} catch (ClassNotFoundException e) {} if (c == null) {long t1 = system.nanotime (); c = findClass(name); } } return c; }Copy the code

New PathClassLoader(“/sdcard/xx.dex”, getClassLoader()); Not only classes in xx.dex can be loaded.

C = findBootstrapClassOrNull(name);

When parent is null, classes loaded by the BootClassLoader can also be loaded.

New PathClassLoader(“/sdcard/xx.dex”, null), can load activity.class?

But actually, the implementation in Android is :(Java is different)

private Class findBootstrapClassOrNull(String name)
{
  return null;
}   
Copy the code

findClass

You can see that when all the parent ClassLoaders fail to load a Class, they call their own findClass method. FindClass is defined in a ClassLoader as:

protected Class<? > findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }Copy the code

LoadClass and findClass can be overridden by any ClassLoader subclass. Generally, if you don’t want to use parent delegates, override loadClass to change its implementation. Overwriting findClass means defining how to find a Class if both parent classloaders can’t find it. Our PathClassLoader is responsible for loading its own classes in programs like MainActivity, using the parent to delegate the parent ClassLoader to load activities in the Framework. PathClassLoader does not override loadClass, so we can see how findClass is implemented in PathClassLoader.

public BaseDexClassLoader(String dexPath, File optimizedDirectory,String librarySearchPath, ClassLoader parent) { super(parent); this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory); } @Override protected Class<? > findClass(String name) throws ClassNotFoundException { List<Throwable> suppressedExceptions = new ArrayList<Throwable>(); // Find the specified 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

The implementation is simple, looking up the class from the pathList. Keep looking at DexPathList

public DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory) { //......... Add (dexPath) // makeDexElements will go to List<File>.add(dexPath) and use DexFile to load the dex File Element array this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext); / /... } public Class findClass(String name, List<Throwable> suppressed) {// Retrieve DexFile from element representing Dex for (Element element: dexElements) { DexFile dex = element.dexFile; if (dex ! = null) {// Find class clazz = dex.loadClassBinaryName(name, definingContext, suppressed); if (clazz ! = null) { return clazz; } } } if (dexElementsSuppressedExceptions ! = null) { suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions)); } return null; }Copy the code

Hot repair

There is an Element array in the PathClassLoader, and a dexFile member in the Element class represents the dex file. That is, if there are X dex in the APK, there are X elements in the Element array.

The Element array in the PathClassLoader is: [patch.dex, class. dex, classes2.dex]. If there is a key. class in patch.dex and classes2.dex, during class search, the loop obtains the DexFile in dexElements, and returns the Key. It does not matter whether dexfiles in subsequent elements can be loaded into key.class.

So, in effect, a hot-fix implementation could create a separate fix. Dex file for the buggy class, and then download fix. Dex from the server when the program starts, save it to a path, and then use the fix. We then insert the Element object into the dexElements array header in the pathList of our program’s classloader, PathClassLoader. In this way, when loading a class with a Bug, the fix class in fix.dex will be loaded first to solve the Bug.

There is more than one way to hotfix, and there may be other issues that need to be addressed (e.g., reflection compatibility) if the hotfix is to be fully implemented.

homework

Implement compatibility problems of ClassLoader hot repair