Android hot repair
One, a brief introduction
Bugs are usually problems with one or more classes. In an ideal world, we just need to update the fixed classes to the app on the user’s phone to fix the bugs. How do I update these classes dynamically? In fact, no matter what kind of heat repair solution, there are definitely several steps:
- Deliver a patch (containing the repaired class) to the user’s phone, i.e. let the app download from the server (network transfer)
- App somehow makes classes in patches (apK, dex, JAR, etc.) called by app (local update)
One way to do this, for this article, is to use the Android class loader, which loads the fixed classes and overwrites the classes that should be faulty, theoretically fixing the bug.
Class loading mechanism
1. Parental delegation model
When a bytecode file is loaded, the current classLoader is asked if the bytecode file has already been loaded. If yes, the system returns and does not reload again. If not, its Parent is asked if the bytecode file has been loaded. Similarly, if it has been loaded, the Parent is returned with the loaded bytecode file. If no classLoader on the entire inheritance line has been loaded, the child classLoader (i.e., The current subclassloader performs the loading of the class.
1) Features:
If a class is loaded by any of the classLoader inheritance lines, the class will not be loaded in the entire system life cycle, greatly improving the efficiency of class loading.
2) Functions:
- Sharing functionality for class loading
Some classes at the Framework level, once loaded by the top-level classLoader, are cached in memory and never reloaded for use anywhere else.
- Isolation for class loading
The classes loaded by the classLoader on the common inheritance thread must not be the same class. This prevents some developers from writing code that impersonates the core library and accesses visible member variables in the core library. For example, java.lang.String is loaded before the application starts. If you can simply replace the String class with a custom String class in an application, there are serious security issues.
Verify that multiple classes are the same class:
- The same className
- The same packageName
- Is loaded by the same classLoader
3) loadClass ()
Verify the parent delegate model with the loadClass() method
Find the loadClass() method in the ClassLoader class, which calls another overloaded loadClass() method with two arguments.
publicClass<? > loadClass(String name)throws ClassNotFoundException {
return loadClass(name, false);
}
Copy the code
To find the real loadClass() method, here’s the source:
protectedClass<? > loadClass(String name,boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loadedClass<? > c = findLoadedClass(name);if (c == null) {
try {
if(parent ! =null) {
c = parent.loadClass(name, false);
} else{ c = findBootstrapClassOrNull(name); }}catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.c = findClass(name); }}return c;
}
Copy the code
As you can see, when loading a class, there are three steps:
- Check whether the current classLoader has loaded the Cong class. If so, return to the cong class. If not, proceed to step 2.
- Call the loadClass() method of the parent classLoader to check whether the parent classLoader has loaded this class.
- If none of the parent classLoaders has loaded the class, the current classLoader eventually calls the findClass() method to find and load the class in the dex file.
ClassLoader in Android
Class loader type
Android has a long history with Java. Java applications based on JVM use ClassLoader to load the classes in the application. Android has optimized the JVM, uses dalvik VM, and the class files are packed into a DEX file. Their classloaders will certainly differ.
There are four main classloaders in Android:
- BootClassLoader: Load class bytecode files in the Android Framework layer (similar to Java Bootstrap ClassLoader).
- PathClassLoader: load Apk class bytecode files already installed in the system (similar to Java App ClassLoader)
- DexClassLoader: load the class bytecode file of the specified directory (similar to the Custom ClassLoader in Java)
- BaseDexClassLoader: The parent class of PathClassLoader and DexClassLoader
An app must use BootClassLoader and PathClassLoader, which can be verified by the following code:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ClassLoader classLoader = getClassLoader();
if(classLoader ! =null) {
Log.e(TAG, "classLoader = " + classLoader);
while(classLoader.getParent() ! =null) {
classLoader = classLoader.getParent();
Log.e(TAG, "classLoader = "+ classLoader); }}}Copy the code
The above code can get the current class’s PathClassLoader from the context, and then get the BootClassLoader from getParent(). This is because Android classloaders use the parent delegate model just like Java classloaders.
2. Differences between PathClassLoader and DexClassLoader
The general source code can be found in Android Studio, but the source code of PathClassLoader and DexClassLoader is system level source, so it cannot be directly viewed in Android Studio. You can go to androidxref.com to view it directly, and the source addresses of several classes to be analyzed are listed below.
Here is some of the source code for Android 5.0:
- PathClassLoader.java
- DexClassLoader.java
- BaseDexClassLoader.java
- DexPathList.java
1) Usage scenarios
Let’s start with the differences in usage scenarios between the two classLoaders
- PathClassLoader: loads only apK files (/data/app directory) that have been installed in the Android operating system. It is the default class loader used by Android.
- DexClassLoader: can load dex/jar/ APk /zip files in any directory, more flexible than PathClassLoader, is the focus of hot repair.
2) Code differences
Here is a look at the PathClassLoader and DexClassLoader source differences, are very simple
// PathClassLoader
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
// DexClassLoader
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(dexPath, newFile(optimizedDirectory), librarySearchPath, parent); }}Copy the code
Through comparison, two conclusions can be drawn:
- Both PathClassLoader and DexClassLoader inherit from BaseDexClassLoader.
- Both PathClassLoader and DexClassLoader call the constructor of the parent class in the constructor, but DexClassLoader passes an additional optimizedDirectory.
3, BaseDexClassLoader
By observing the source code of PathClassLoader and DexClassLoader, we can determine that the truly meaningful processing logic must be in BaseDexClassLoader, so the following focuses on the analysis of BaseDexClassLoader source code.
1) Constructor
Let’s start by looking at what the BaseDexClassLoader constructor does:
public class BaseDexClassLoader extends ClassLoader {
private finalDexPathList pathList; .public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent){
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory); }... }Copy the code
- DexPath: the directory where the program file to be loaded (dex file or JAR/APk /zip file) is located.
- OptimizedDirectory: output directory of dex files (this directory is specially used to store decompressed program files in jar/ APk /zip formats).
- LibraryPath: libraryPath used when loading program files.
- Parent: parent loader
From the point of view of a complete App, the application file specifies the classes.dex file in the APK package. But from a hotfix perspective, program files refer to patches.
Because PathClassLoader only loads dex files in installed packages, DexClassLoader can load not only dex files, but also dex files in JAR, APK, and ZIP files. Jar, APk, and zip are actually some compressed formats. To get the dex file in the compressed package, you need to decompress it. Therefore, DexClassLoader will specify a directory to decompress when calling the parent class constructor.
2) the findClass ()
The class loader must provide a method for the outside world to find the class to which it is loaded. That method is findClass(), but neither PathClassLoader nor DexClassLoader source code overrides the findClass() method of the parent class. FindClass () does not override findClass(); findClass() does not override findClass();
private final DexPathList pathList;
@Override
protectedClass<? > findClass(String name)throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
FindClass () is the object of pathList
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
As you can see, the findClass() method of the BaseDexClassLoader actually gets the class from the findClass() method of the DexPathList object (pathList), The DexPathList object happens to have been created in the BaseDexClassLoader constructor. So, let’s look at what’s going on in the DexPathList class.
4, DexPathList
1) Constructor
private final Element[] dexElements;
public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {...this.definingContext = definingContext;
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions); . }Copy the code
In this constructor, the current classloader definingContext is saved and makeDexElements() is called to get the Element collection.
By tracing the source code of splitDexPath(dexPath), we found that the function of this method is actually to convert all program files in the dexPath directory into a File set. And also found that dexPath is a colon (” : “) as a delimiter joined the multiple program files directory string (such as: / data/dexdir1: / data/dexdir2:…). .
The next step is to analyze the makeDexElements() method. Since this section of code is longer, I’ll post the key code and comment it out:
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) {
// 1. Create an Element collection
ArrayList<Element> elements = new ArrayList<Element>();
// 2. Run through all dex files (jar, APk, or zip files)
for (File file : files) {
ZipFile zip = null;
DexFile dex = null; String name = file.getName(); .// If it is a dex file
if (name.endsWith(DEX_SUFFIX)) {
dex = loadDexFile(file, optimizedDirectory);
// If it is an APk, JAR, zip file (this part is handled slightly differently in different Android versions)
} else{ zip = file; dex = loadDexFile(file, optimizedDirectory); }...// 3. Wrap the dex file or zip file as an Element object and add it to the Element collection
if((zip ! =null) || (dex ! =null)) {
elements.add(new Element(file, false, zip, dex)); }}// 4. Convert the Element collection to an Element array and return it
return elements.toArray(new Element[elements.size()]);
}
Copy the code
In this method, you can see a few things. In general, the constructor of DexPathList is to wrap program files (maybe dex, APK, JAR, zip) into Element objects and add them to the Element collection.
In fact, Android class loaders (either PathClassLoader or DexClassLoader) only recognize dex files in the end, and loadDexFile() is the core method of loading dex files. Dex can be extracted from JAR, APK, zip, etc. But I won’t analyze it here, because the first goal is done, so I’ll analyze it later.
2) the findClass ()
Look again at the DexPathList findClass() method:
public Class findClass(String name, List<Throwable> suppressed) {
for(Element Element: dexElements) {// Iterate through a dex file.if(dex ! Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);if(clazz ! = null) {returnclazz; }}}if(dexElementsSuppressedExceptions ! = null) { suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions)); }return null;
}
Copy the code
The DexPathList findClass() method simply iterates through the Element array. If it finds a class with the same class name as name, it returns the class directly. If it does not find a class with the same name, it returns null.
Why call DexFile loadClassBinaryName() to load the class? This is because an Element object corresponds to a dex file, and a dex file contains multiple classes. That is, the Element array contains dex files, not class files. This can be seen from the source code of the Element class and the internal structure of the dex file.
Three, the realization principle of thermal repair
After the analysis of PathClassLoader, DexClassLoader, BaseDexClassLoader and DexPathList, we know that When loading a class on Android, the class loader first obtains (Element[] dexElements) from the Element array in its DexPathList object into the corresponding class, and then loads it. Array traversal is used, but notice that traversal is a dex file. In the for loop, we first iterate through the dex file and then retrieve the class from the dex file, so we simply pack the repaired class into a dex file and place it at the first Element of the Element array to ensure that the newly repaired class is retrieved (of course, A buggy class exists, but it is placed in the last Element of the Element array, so it has no chance of being retrieved.