This article is mainly about QQ Spatial Team Introduction of Android App Hot Patch Dynamic Repair Technology Principle and implementation learning.
Be based on Introduction of Android App Hot Patch Dynamic Repair Technology There are many hot-fix frameworks that are open source on github, roughly:
https://github.com/dodola/HotFix
https://github.com/jasonross/...
https://github.com/bunnyblue/...
principle
- Prevent related classes from being labeled CLASS_ISPREVERIFIED when the app is packaged;
- Put bug-free classes into a patch.jar package;
- Load the class.dex file from the sdcard package through DexClassLoader;
- Through Java's reflection mechanism, the classes.dex file in patch.jar is dynamically added to the dexElements array indirectly referenced by the BaseDexClassLoader object.Make BaseDexClassLoader load classes.dex in patch.jar to complete bug fixing when loading classes.
CLASS_ISPREVERIFIED PROBLEM
Questions about CLASS_ISPREVERIFIED Introduction of Android App Hot Patch Dynamic Repair Technology Detailed description is provided.
According to the above article, when the verify option is turned on at virtual machine startup, the class directly referenced (first-level relationship) to the static method, private method, constructor of the related class is in the same dex file, then the class is marked with CLASS_ISPREVERIFIED.
If hot fixes are used, prevent this class from being labeled CLASS_ISPREVERIFIED.
Note that the class is blocking the referencer, that is, suppose there is a class called LoadBugClass in the app that references BugClass internally.After publishing, you find that there is a write error in the BugClass. If you want to publish a new BugClass class, you need to prevent the LoadBugClass class from being marked as CLASS_ISPREVERIFIED.
That is, before generating an apk, you need to prevent the related classes from being labeled CLASS_ISPREVERIFIED.For how to prevent this, the article above makes it clear that LoadBugClass can reference another DEX file in its construction method, such as a class in hack.dex.
There are actually two things:
- 1. Prevent related classes from labeling CLASS_ISPREVERIFIED when the app is packaged.
- 2. Dynamically changing dexElements indirectly referenced by BaseDexClassLoader objects;
DexClassLoader and PathClassLoader
DexClassLoader
The source comment for DexClassLoader reads as follows:
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.
You can see that DexClassLoader can be used to load classes.dex files from within files of type.jar and.apk.Can be used to execute non-installed program code.
Hot fixes take advantage of the ability of DexClassLoader to load classes.dex files from external files.
DexClassLoader.java
package dalvik.system; public class DexClassLoader extends BaseDexClassLoader { public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) { super(dexPath, new File(optimizedDirectory), libraryPath, parent); } }
PathClassLoader
The source comment for PathClassLoader reads as follows:
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 PathClassLoader as its loader for system and application classes.And this class can only load apk files that have been installed on Android system.
The Android system uses PathClassLoader as its class loader to load classes from the apk.
PathClassLoader.java
package dalvik.system; 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); } }
Here, we learned that classes.dex can be loaded from sdcard's patch.jar through DexClassLoader:
- The Android system uses PathClassLoader as its class loader to load classes from the apk.
- DexClassLoader can load classes.dex from external files of type.jar and.apk.
BaseDexClassLoader
As mentioned earlier, the classes.dex file in patch.jar is dynamically added to the dexElements array indirectly referenced by the BaseDexClassLoader object through Java's reflection mechanism.Make BaseDexClassLoader load classes.dex in patch.jar to complete bug fixing when loading classes.
Here's how to reflect into the dexElements array indirectly referenced by the BaseDexClassLoader class.
BaseDexClassLoader.java Source
package dalvik.system; public class BaseDexClassLoader extends ClassLoader { private final DexPathList pathList; ... @Override protected Class<?> findClass(String name) throws ClassNotFoundException { List<Throwable> suppressedExceptions = new ArrayList<Throwable>(); 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; } ... }
BaseDexClassLoader loads classes by findClass, by pathList attribute, and by calling the findClass method of the DexPathList class. Next, take a look at the findClass method of the DexPathList class.
DexPathList.java
package dalvik.system; final class DexPathList { private final Element[] dexElements; ... public Class findClass(String name, List<Throwable> suppressed) { for (Element element : dexElements) { DexFile dex = element.dexFile; if (dex != null) { Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed); if (clazz != null) { return clazz; } } } if (dexElementsSuppressedExceptions != null) { suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions)); } return null; } ... }
An array of dexElements is found here.
Through the reflection mechanism, the class.dex file in the patch.jar package is added to the front of the dexElements array to complete the bug hot fix.
BaseDexClassLoader loops the dexElements array when loading classes, first loading the dex files that are in the front of the dexElements array, and then completing the bug fix if the corresponding classes are found in the dex files that are in the front of the dexElements array without loading the dex files that are in the back.
Code implementation that prevents related classes from being uploaded to CLASS_ISPREVERIFIED
adopt https://github.com/dodola/Hot....
The general process is:
Before the dx tool executes, modify the LoadBugClass.class file, add System.out.println (dododola.hackdex.AntilazyLoad.class) to its construction, and continue the packaging process.Note: The class AntilazyLoad.class is independent of hack.dex.
There are two questions:
- How to modify a.class file (inject code through javassist)
- How to make.class file modifications occur before dx
How to modify a class file for a class (inject code through javassist)
Create the following new classes:
Referenced class AntilazyLoad.java (class included in hack.jar)
package dodola.hackdex; public class AntilazyLoad {}
Class BugClass.java to fix
package dodola.hotfix; public class BugClass { public String bug() { return "bug class"; } }
Reference class LoadBugClass.java
package dodola.hotfix; public class LoadBugClass { public String getBugString() { BugClass bugClass = new BugClass(); return bugClass.bug(); } }
What you need to do here is add:
System.out.println(dodola.hackdex.AntilazyLoad.class)
You can do this through javassist, about javassist
JAVAssist
Javassist Learning Summary
For injecting code into the LoadBugClass.class construction method, there is a Test.java below
Test.java
package dodola.hotfix; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor; public class Test { public static void main(String[] args) { try { //Find the.class file from the file path // class File Path String path = "D:\\work_space\\ws\\Android_Test\\app\\build\\intermediates\\classes\\debug\\dodola\\hotfix"; ClassPool classes = ClassPool.getDefault(); classes.appendClassPath(path); // Locate LoadBugClass.class CtClass c = classes.get("dodola.hotfix.LoadBugClass"); // Insert code into construction method CtConstructor ctConstructor = c.getConstructors()[0]; ctConstructor.insertBefore("System.out.println(dodola.hackdex.AntilazyLoad.class);"); // output file c.writeFile(path + "/output"); } catch (Exception e) { e.printStackTrace(); } } }
Note: Imported in build.gradle
compile 'org.javassist:javassist:3.18.2-GA'
run runs Test.java, injecting code into LoadBugClass.class.
Below is a comparison of LoadBugClass.class before and after injection
How to make.class file modifications occur before dx
open https://github.com/dodola/Hot...
apply plugin: 'com.android.application' // Construction Method Injection Code task('processWithJavassist') << { // The directory in which the.class resides after the project has been compiled String classPath = file('build/intermediates/classes/debug') // Call the process method of PatchClass.java to inject code into the.class file construction method dodola.patch.PatchClass.process( // The directory in which the.class resides after the project has been compiled classPath, // The directory where hackdex's. class is located project(':hackdex').buildDir.absolutePath + '/intermediates/classes/debug') } android { ... // Inject code into.class before executing dx command applicationVariants.all { variant -> variant.dex.dependsOn << processWithJavassist } } dependencies { ... }
Make AntilazyLoad a hack_dex.jar package
After the code injection of "System.out.println (dododola.hack dex.AntilazyLoad.class);", the AntilazyLoad.class is also packaged into a hack_dex.jar package and loaded into the program through DexClassLoader, otherwise classes injected into the code, such as LoadBugClass, throw a ClassNotFoundException exception at runtime.
hack_dex.jar package generation
Corresponding command behavior:
jar cvf hack.jar dodola/hackdex/* dx --dex --output hack_dex.jar hack.jar
Load the.Dex file from hack_dex.jar
HotfixApplication.java
package dodola.hotfix; import java.io.File; import android.app.Application; import android.content.Context; import dodola.hotfixlib.HotFix; //1. dexClassLoader loads hackdex.jar(dodola.hackdex.AntilazyLoad) //2. DEX file in hack_dex, dynamically added to the dexElements array indirectly referenced by the BaseDexClassLoader object public class HotfixApplication extends Application { @Override public void onCreate() { super.onCreate(); // File dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "hackdex_dex.jar"); // Copy the hackdex_dex.jar file under assets to the path / data/data/packagename/dex/hackdex_dex.jar Utils.prepareDex(this.getApplicationContext(), dexPath, "hackdex_dex.jar"); // Dynamically add hack_dex to the dexElements array indirectly referenced by the BaseDexClassLoader object HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hackdex.AntilazyLoad"); // Load dododola.hackdex.AntilazyLoad try { this.getClassLoader().loadClass("dodola.hackdex.AntilazyLoad"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
patch method in HotFix.java file
//Add patch.dex dynamically to the dexElements array indirectly referenced by the BaseDexClassLoader object public static void patch(Context context, String patchDexFile, String patchClassName) { // dex file exists if (patchDexFile != null && new File(patchDexFile).exists()) { try { // Ali Equipment if (hasLexClassLoader()) { injectInAliyunOs(context, patchDexFile, patchClassName); } // Non-Ali Equipment else if (hasDexClassLoader()) { injectAboveEqualApiLevel14(context, patchDexFile, patchClassName); } else { injectBelowApiLevel14(context, patchDexFile, patchClassName); } } catch (Throwable th) { } } }
injectAboveEqualApiLevel14 method in HotFix.java file
private static void injectAboveEqualApiLevel14(Context context, String str, String str2) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { // PathClassLoader PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader(); // --------------------------------- // Merge Array Object a = combineArray( // Array of dexElements in PathClassLoader getDexElements(getPathList(pathClassLoader)), // After DexClassLoader loads the local dex file, the dexElements array getDexElements(getPathList(new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader()))) ); //----------dexElements array assignment--------- // Get pathList Object a2 = getPathList(pathClassLoader); setField(a2, a2.getClass(), "dexElements", a); //---------------Load str2 class--------- pathClassLoader.loadClass(str2); }
Reference resources:
https://zhuanlan.zhihu.com/p/...
http://blog.csdn.net/lmj62356...