An overview,
Github has recently opened a number of hotpatch dynamic repair frameworks, including:
- Github.com/dodola/HotF…
- Github.com/jasonross/N…
- Github.com/bunnyblue/D…
According to the description of the above three frameworks, the principle comes from: Introduction of hot patch dynamic repair technology of Android App and subcontracting scheme of Android DEX, so these two articles must be read. I won’t compare the three frameworks too much here, because the principles are the same and the code implemented may not be that different.
Interested in the principle of direct reading this article, plus the above framework of the source code can be read. Of course, this post will also do an analysis of the source code of the above framework, as well as the analysis of the technology used in the whole implementation process.
Two, the principle of thermal repair
For the principle of thermal repair, if you read the above two articles, I believe you have a general understanding. The main thing to know is that the Android ClassLoader system, Android loading classes generally use PathClassLoader and DexClassLoader, first of all, let’s look at the difference between these two classes:
-
For the PathClassLoader, from the comments in the documentation:
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).As you can see, Android uses this class as a loader for its system and application classes. And for this class, you can only load apK files already installed in the Android system.
-
For DexClassLoader, look again at the comment:
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.As you can see, this class can be used to load classes.dex files from inside.jar and.apk files. Can be used to execute non-installed program code.
Ok, if you are familiar with the plugin, you must be familiar with this class, plug-in is generally provided with an APK (plug-in) file, and then load the APK in the program, so how to load the APK class? In fact, through this DexClassLoader, the code will be described later.
All you need to know is that Android uses PathClassLoader as its classloader, and DexClassLoader can load classes.dex files from inside.jar and.apk files.
Android uses PathClassLoader as its classloader, so how does hotfix work?
Ok, to load a class, just give it a classname, and then go to findClass. Both PathClassLoader and DexClassLoader inherit from BaseDexClassLoader. BaseDexClassLoader has the following source code:
#BaseDexClassLoader @Override protected Class<? > findClass(String name) throws ClassNotFoundException { Class clazz = pathList.findClass(name); if (clazz == null) { throw new ClassNotFoundException(name); } return clazz; } #DexPathList public Class findClass(String name) { for (Element element : dexElements) { DexFile dex = element.dexFile; if (dex ! = null) { Class clazz = dex.loadClassBinaryName(name, definingContext); if (clazz ! = null) { return clazz; } } } return null; } #DexFile public Class loadClassBinaryName(String name, ClassLoader loader) { return defineClass(name, loader, mCookie); } private native static Class defineClass(String name, ClassLoader loader, int cookie);Copy the code
As you can see, there is a pathList object in The BaseDexClassLoader, and the pathList contains a set of dexfiles, dexElements, and for class loading, you just walk through that set and look for it through the DexFile.
Ok, colloquially speaking:
A ClassLoader can contain multiple dex files, each of which is an Element. The dex files are arranged into an ordered array dexElements. When searching for a class, the dex files will be traversed in sequence, and then the class will be searched from the current traversed dex file. If you cannot find it, proceed to the next dex file. (From: Android App hot patch dynamic repair technology introduction)
In this case, we can do something in the dexElements, such as place our patch.jar at the first element of the array with the fixed classes in it, so that when we traverse findClass, the fixed classes will be found, replacing the buggy classes.
At this point, you’re probably smiling because it’s so simple. However, there is still a problem called CLASS_ISPREVERIFIED. For this problem, please refer to “Introduction to hot Patch Dynamic Repair technology for Android App” for detailed explanation.
Ok, for CLASS_ISPREVERIFIED, please verify this for us:
If static methods, private methods, constructors, etc. are all in the same dex file when verify is turned on at virtual machine startup, Then the class is marked with CLASS_ISPREVERIFIED.
Then, what we need to do is to prevent the class from being marked with the CLASS_ISPREVERIFIED flag.
Notice that it’s a reference-blocking class, that is, suppose you have a class in your app called LoadBugClass that references BugClass internally. If you want to publish a new BugClass, you must prevent the LoadBugClass from being marked with the CLASS_ISPREVERIFIED flag.
That is, you need to prevent the related classes from being marked with CLASS_ISPREVERIFIED before you generate the APK. To prevent this, let LoadBugClass refer to another dex file in the constructor, such as hack.dex.
Ok, to sum up:
Dynamically changing the dexElements indirectly referenced by the BaseDexClassLoader object; 2. During app packaging, prevent relevant classes from marking CLASS_ISPREVERIFIED.
If you don’t see it clearly, don’t worry, read it a few times, and I’ll explain it in code.
Three, prevent the relevant categories hitCLASS_ISPREVERIFIED
mark
Ok, the following code will basically go through github.com/dodola/HotF… Provided code to explain.
So, here’s a concrete class:
Process is roughly: before the dx tool execution, will LoadBugClass. Class files, modify, and add System. Its structure out. The println (dodola. Hackdex. AntilazyLoad. Class), and then continue to packaging process. Note: The AntilazyLoad. Class class is independent of hack.dex.
Ok, here you may have two questions:
- How to modify a class file
- How do I do question 1 before DX
(1) How to modify a class file
Here we use Javassist, which is simple:
Ok, let’s start with a few new classes:
package dodola.hackdex; public class AntilazyLoad { } package dodola.hotfix; public class BugClass { public String bug() { return "bug class"; } } package dodola.hotfix; public class LoadBugClass { public String getBugString() { BugClass bugClass = new BugClass(); return bugClass.bug(); }}Copy the code
Notice that the package here, what we want to do is generate a class file after the above class is compiled normally. For example, loadBugclass. class, we add a line to the loadBugclass. class constructor:
System.out.println(dodola.hackdex.AntilazyLoad.class)
Copy the code
Let’s look at the action class:
package test; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor; public class InjectHack { public static void main(String[] args) { try { String path = "/Users/zhy/develop_work/eclipse_android/imooc/JavassistTest/"; ClassPool classes = ClassPool.getDefault(); classes.appendClassPath(path + "bin"); / / bin directory project can CtClass c = classes. Get (" dodola. Hotfix. LoadBugClass "); CtConstructor ctConstructor = c.getConstructors()[0]; ctConstructor .insertAfter("System.out.println(dodola.hackdex.AntilazyLoad.class);" ); c.writeFile(path + "/output"); } catch (Exception e) { e.printStackTrace(); }}}Copy the code
Ok, click Run and note the package that imports javassist-*.jar in the project.
First get the ClassPool object and then add the classpath, which can be called multiple times if you have multiple classpath. Then find the LoadBugClass from the classpath, take its constructor, and insert a line of code at the end of it. Ok, the code is easy to understand.
Ok, let’s decompile the class file we generated:
Ok, there are a few articles for javassist if you’re interested:
- www.ibm.com/developerwo…
- Zhxing.iteye.com/blog/170330…
(2) How to perform the operation of (1) before dx
Ok, this is combined with github.com/dodola/HotF… Source code for the.
After importing the source code, open app/build.gradle
apply plugin: 'com.android.application' task('processWithJavassist') << { String classPath = File (' build/intermediates/classes/debug ') / / project to compile the class directory dodola. Patch. PatchClass. The process (the classPath, Project (' : hackdex '). The buildDir. AbsolutePath + '/ intermediates/classes/debug) / / the second parameter is the class hackdex android directory} { Variants -> varie.dex. DependsOn << processWithJavassist // to type the code into the class before running the dx command}}Copy the code
You will notice that the processWithJavassist task will be executed before executing dx. This task does exactly what we did above. And the source code is also given, you have a look.
Ok, so here you go, you can hit Run. Ok, have interest, you can go to see dodola decompiling. Hotfix. LoadBugClass have been added in the constructor of a class to change professions code.
The use of decompiled, tools, etc., the reference: blog.csdn.net/lmj62356579…
Ok, so far we have been able to install and run APK normally. But there is no relevant code for patching.
Dynamically change the dexElements indirectly referenced by the BaseDexClassLoader object
Ok, here it’s a little easier, dynamically changing a reference to an object that we can reflect.
But the important thing to note here, remember we said before, is that looking for class is iterating through dexElements; Our AntilazyLoad. Class is not actually included in classes.dex, and for the purposes described above, we need to make AntilazyLoad. Class a separate hack_dex.jar. It must be converted by the DX tool.
Specific practices:
jar cvf hack.jar dodola/hackdex/*
dx --dex --output hack_dex.jar hack.jar
Copy the code
- 1
- 2
- 1
- 2
If you can’t jar that class file, go to Baidu…
Ok, now that we have hack_dex.jar, what does this do?
Remember that our app middle class references AntilazyLoad.class, so we must insert hack_dex.jar into dexElements when the app starts, otherwise we will definitely crash.
So, the onCreate method of the Application is a good place to do this. We put hack_dex.jar in assets.
Hotfix:
/* * Copyright (C) 2015 Baidu, Inc. All Rights Reserved. */ package dodola.hotfix; import android.app.Application; import android.content.Context; import java.io.File; import dodola.hotfixlib.HotFix; /** * Created by sunpengfei on 15/11/4. */ public class HotfixApplication extends Application { @Override public void onCreate() { super.onCreate(); File dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "hackdex_dex.jar"); Utils.prepareDex(this.getApplicationContext(), dexPath, "hackdex_dex.jar"); HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hackdex.AntilazyLoad"); try { this.getClassLoader().loadClass("dodola.hackdex.AntilazyLoad"); } catch (ClassNotFoundException e) { e.printStackTrace(); }}}Copy the code
Create a file in the app’s private directory and write hackdex_dex.jar from Assets to this file by calling utils. prepareDex. HotFix. Patch then reflects and modifies dexElements. Let’s take a closer look at the source code:
/*
* Copyright (C) 2015 Baidu, Inc. All Rights Reserved.
*/
package dodola.hotfix;
/**
* Created by sunpengfei on 15/11/4.
*/
public class Utils {
private static final int BUF_SIZE = 2048;
public static boolean prepareDex(Context context, File dexInternalStoragePath, String dex_file) {
BufferedInputStream bis = null;
OutputStream dexWriter = null;
bis = new BufferedInputStream(context.getAssets().open(dex_file));
dexWriter = new BufferedOutputStream(new FileOutputStream(dexInternalStoragePath));
byte[] buf = new byte[BUF_SIZE];
int len;
while ((len = bis.read(buf, 0, BUF_SIZE)) > 0) {
dexWriter.write(buf, 0, len);
}
dexWriter.close();
bis.close();
return true;
}
Copy the code
Ok, it’s actually a read and write to a file, writing a file from assets to a file in the app’s private directory.
The patch method is mainly looked at below
/* * Copyright (C) 2015 Baidu, Inc. All Rights Reserved. */ package dodola.hotfixlib; import android.annotation.TargetApi; import android.content.Context; import java.io.File; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import dalvik.system.DexClassLoader; import dalvik.system.PathClassLoader; /* compiled from: ProGuard */ public final class HotFix { public static void patch(Context context, String patchDexFile, String patchClassName) { if (patchDexFile ! = null && new File(patchDexFile).exists()) { try { if (hasLexClassLoader()) { injectInAliyunOs(context, patchDexFile, patchClassName); } else if (hasDexClassLoader()) { injectAboveEqualApiLevel14(context, patchDexFile, patchClassName); } else { injectBelowApiLevel14(context, patchDexFile, patchClassName); } } catch (Throwable th) { } } } }Copy the code
First find classes dalvik. System. BaseDexClassLoader, if found, enter the if.
In injectAboveEqualApiLevel14, according to the context to get the PathClassLoader, then through getPathList (PathClassLoader), get a PathClassLoader pathList of object, Get the dexElements object through pathList after calling getDexElements.
Ok, so how do we convert our hack_dex.jar to a dexElements object?
First, initialize a DexClassLoader object. The parent class of DexClassLoader is BaseDexClassLoader. So we can get dexElements in the same way as the PathClassLoader.
Ok, so here we have the indirect reference to dexElements from the PathClassLoader object in the system, and dexElements from our hack_dex.jar, and now it’s time to merge the two arrays.
You can see that the code above uses the combineArray method.
After the merge is complete, the new array is set to pathList by reflection.
Let’s look at the reflection in detail:
private static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList"); } private static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException { return getField(obj, obj.getClass(), "dexElements"); } private static Object getField(Object obj, Class cls, String str) throws NoSuchFieldException, IllegalAccessException { Field declaredField = cls.getDeclaredField(str); declaredField.setAccessible(true); return declaredField.get(obj); } It is easy to understand the process of fetching a member variableCopy the code
private static Object combineArray(Object obj, Object obj2) { Class componentType = obj2.getClass().getComponentType(); int length = Array.getLength(obj2); int length2 = Array.getLength(obj) + length; Object newInstance = Array.newInstance(componentType, length2); for (int i = 0; i < length2; i++) { if (i < length) { Array.set(newInstance, i, Array.get(obj2, i)); } else { Array.set(newInstance, i, Array.get(obj, i – length)); } } return newInstance; }
Ok, here the two arrays are merged, just one thing to notice is that the dexElements in hack_dex.jar are placed in front of the new array.
At this point, we have dynamically injected the DexFile contained in hack_dex.jar into the dexElements of the ClassLoader at application startup time. So you don’t miss AntilazyLoad.
Ok, so we still don’t see how we patched it. In fact, as already mentioned, the patching process is the same as when we injected hack_dex.jar.
You are now running the HotFix app project and clicking on tests in menu:
It pops up: Call test method: Bug Class
Now let’s see how to do the thermal repair.
Five, complete the thermal repair
Ok, so we assume that the BugClass class has an error that needs to be fixed:
package dodola.hotfix; public class BugClass { public String bug() { return "fixed class"; }} You can see the string change: bug class -> fixed class. Then, compile, place the class->jar->dex of this class. The steps are the same as above.Copy the code
jar cvf path.jar dodola/hotfix/BugClass.class dx –dex –output path_dex.jar path.jar
Get the path_dex.jar file. Normally, this thing would be downloadable, but of course we'll show you how it works, and you can just put it on your SDcard. Add the following code to the onCreate of the Application:Copy the code
public class HotfixApplication extends Application {
@Override
public void onCreate()
{
super.onCreate();
File dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "hackdex_dex.jar");
Utils.prepareDex(this.getApplicationContext(), dexPath, "hack_dex.jar");
HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hackdex.AntilazyLoad");
try
{
this.getClassLoader().loadClass("dodola.hackdex.AntilazyLoad");
} catch (ClassNotFoundException e)
{
e.printStackTrace();
}
dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "path_dex.jar");
Utils.prepareDex(this.getApplicationContext(), dexPath, "path_dex.jar");
HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hotfix.BugClass");
}
Copy the code
}
The first line is still copied to the private directory. If you are on SDcard, the operation is basically the same. Don’t ask: what if you are on SDcard or network
Ok, so let’s run our app again.
Ok, finally, say that there is a button for patching in the project, under Menu, so you can also not add our last 3 lines in the Application.
After you run the app, click patch first, and then click Test to find the successful repair.
If you click Test and then patch, the test won’t change, because once the class is loaded, it won’t reload again.
Ok, so far, the principle of our hot repair has been solved. I believe it has been introduced in detail. If you have enough patience, it can be realized. Patch production and other operations in the middle, our operation is more troublesome, automation, you can refer to github.com/jasonross/N… .
Finally, thanks to qzone team and open source authors ~~
Welcome to my micro blog: weibo.com/u/316501872…