Exploration of Android Frontier Technology: Application of ClassLoader in Thermal Repair
Writing bugs again?This is a joke, but it is also because we are not gods, but we can't think about everything perfectly. A bug is inevitable.So what if we have a bug for android?
When we encounter bugs early on, we usually release a version in an emergency.However, this Bug may be a simple line of code, for which iterating through a version with a full or incremental update is a bit overkill.And the popularity of the new version will take time, and what if there is a minor problem with the new version this time?
So to solve this problem, hot fix appears.
Hot Fix, now everyone should be familiar with it.Starting in 16 years, hot fix technology has been hot in the Android technology community for a while, and this technology that fixes online bug s without releasing a new version looks very black.
The purpose of this article is not to hot fix itself, but to familiarize yourself with the core of the hot fix case: the class loading mechanism.(Hot fixes will be explained in more detail later)
ART and Dalvik
DVM is also a virtual machine that implements the JVM specification, using the CMS garbage collector by default, but unlike JVM running Class byte codes, DVM performs Dex(Dalvik Executable Format), a compression format specifically designed for Dalvik.Dex files are many.class files that handle compressed products and can ultimately 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.Both ART and Dalvik are compatible runtimes that run Dex byte codes, so applications developed for Dalvik can also run in the ART environment.
https://source.android.google...
dexopt and dexaot
- dexopt
When a virtual machine in Dalvik loads a DEX file, it validates and optimizes the DEX file. The result of optimizing the DEX file becomes an odex(Optimized dex) file, which is similar to the DEX file but uses some optimizing opcodes.
- dex2oat
ART precompilation mechanism, which compiles ODEX into an OAT (actually an ELF file) executable (machine code) after dexopt optimization of the DEX file is performed at installation time, followed by AOT precompilation of the odex.(Unoptimized DEX takes longer to convert to OAT than ODEX optimization)
Introduction to ClassLoader
Any Java program consists of one or more class files, which need to be loaded into the JVM to be used when the program is running. The class loading mechanism of Java is responsible for loading these class files.The simple function of ClassLoader is to load the class file for use at runtime.Each Class object has a classLoader field inside it that identifies which ClassLoader it was loaded by.
class Class<T> { ... private transient ClassLoader classLoader; ... }
ClassLoader is an abstract class whose implementation classes are:
- BootClassLoader
Used to load Android Framework layer class files.
- PathClassLoader
For Android application class loader.You can load the specified dex, as well as classes.dex from jar, zip, apk
- DexClassLoader
Used to load the specified dex, as well as classes.dex in jar, zip, apk
Many blogs have said that PathClassLoader can only load the dex of the installed apk, which should actually be on the dalvik virtual machine.
But now you don't normally care about dalvik.
Log.e(TAG, "Activity.class By:" + Activity.class.getClassLoader() +" Load"); Log.e(TAG, "MainActivity.class By:" + getClassLoader() +" Load"); //Output: Activity.class By: java.lang.BootClassLoader@d3052a9 Load 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]]] Load
The relationships between them are as follows:
PathClassLoader
andDexClassLoader
The common parent of isBaseDexClassLoader
.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); } }
You can see that the only difference between the two is that creating DexClassLoader passes an optimizedDirectory parameter and passes it as a File object to super, while PathClassLoader passes it directly to null.So both can load the specified dex, as well as classes.dex from jar, zip, apk
PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/xx.dex", getClassLoader()); File dexOutputDir = context.getCodeCacheDir(); DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/xx.dex",dexOutputDir.getAbsolutePath(), null,getClassLoader());
In fact, the optimizedDirectory parameter is the output directory (odex) of dexopt.So when PathClassLoader was created, this directory was null, which means no dexopt?No, the default path when optimizedDirectory is null is: /data/dalvik-cache.
In API 26 source code, the optimizedDirectory of DexClassLoader is marked as deprecated and the implementation becomes:
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) { super(dexPath, null, librarySearchPath, parent); }
... exactly like Path Class Loader!
Parental Delegation Mechanism
You can see that creating a ClassLoader requires receiving a ClassLoader parent parameter.The purpose of this parent is to implement a parental delegation for class loading.That is:
When a class loader receives a request to load a class, it first delegates the load task to the parent loader, which in turn recursively returns if the parent loader can complete the class load task; it only loads itself if the parent loader cannot complete the load task.
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 != null) { //If parent is not null, the loadClass of parent is called to load c = parent.loadClass(name, false); } else { //parent is null, then BootClassLoader is called to load c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } if (c == null) { // Find it yourself if you can't find it long t1 = System.nanoTime(); c = findClass(name); } } return c; }
So we created our own lassLoader: new PathClassLoader ('/sdcard/xx.dex', getClassLoader ()); it doesn't just load class es from xx.dex.
It is worth noting that c = findBootstrapClassOrNull(name);
By the method name, it is assumed that BootClassLoader loaded classes can also be loaded when parent is null.
Can the new PathClassLoader ('/sdcard/xx.dex', null) load Activity.class?
But in fact, the implementation in Android is: (Java is different)
private Class findBootstrapClassOrNull(String name) { return null; }
findClass
You can see that when all parent ClassLoaders cannot load a Class, their findClass method is called.FindClass is defined in ClassLoader as:
protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
In fact, any ClassLoader subclass can override loadClass and findClass.Normally, if you do not want to use parental delegation, override the loadClass to modify its implementation.Rewriting findClass means defining how to find a Class if neither parent ClassLoader can find it.Our PathClassLoader is responsible for loading classes written by itself in programs such as Main Activity and using parent-delegated ClassLoader to load activities in the Framework.Note that PathClassLoader does not override loadClass, so we can see how findClass in PathClassLoader is implemented.
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 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; }
The implementation is straightforward, finding the class from the pathList.Continue viewing DexPathList
public DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory) { //......... // The splitDexPath implementation returns List<File>.add(dexPath) // makeDexElements will go to List <File>.add(dexPath) and use DexFile to load the dex file to return the Element array this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext); //......... } public Class findClass(String name, List<Throwable> suppressed) { //Get Dex File from element ation for (Element element : dexElements) { DexFile dex = element.dexFile; if (dex != null) { //Find class Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed); if (clazz != null) { return clazz; } } } if (dexElementsSuppressedExceptions != null) { suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions)); } return null; }
hot fix
There is an Element array in PathClassLoader, and there is a dexFile member in the Element class representing the dex file, that is, if there are X dex in the APK, then there are X elements in the Element array.
The Element array in PathClassLoader is: [patch.dex, classes.dex, classes2.dex].If a Key.class exists in both patch.dex and classes2.dex, when a class lookup occurs, the loop obtains the DexFile in dexElements, and if a Key.class is found, it returns immediately, regardless of whether the DexFile in subsequent elementations can be loaded into the Key.class.
Therefore, a hot fix implementation can actually make a separate fix.dex file (patch package) for the class in which the Bug occurs, then when the program starts, download fix.dex from the server, save it to a path, use it to create an Element object, and then insert the Element object into the pathList of our program's class loader PathClassLoader The header of the xElements array.This resolves the Bug by loading the fix class in fix.dex first when the class with the Bug appears.
There is more than one way to do this, and there may be other issues to consider if you want to fully implement it (e.g., reflection compatibility).