Android hotfix practices
Preface
A few months ago, the Android App hotfix went viral. It was due to an article by the QQ space team about the Android App hotfix dynamic hotfix technology, and then the open-source projects of various major manufacturers came out, the practice in this article is based on HotFix, that is, the Technology Applied in the article by the QQ space Technical Team. The author will detail the details and ideas of the entire process in this article, the purpose of this study is to fix the bug of the app in an emergency. You do not need to repeat the package, and you do not need to download the app again to solve the problem. I personally think this is quite valuable, although the boss does not know .....
Project Structure
Here, I create a new project "HotFixDemo" to show you how to implement the Hotfix framework step by step. This project includes the following modules:
-App: Our Android Application Module.
-Buildsrc: A Groovy-implemented project provides a class for modifying class files.
-Hackdex: provides a class, which will be packaged as hack. dex later. It is also referenced by a piece of code inserted in the constructors of all classes in buildsrc.
-Hotfixlib: This module will eventually be associated with the app, which provides the core method for implementing hot Patching.
The code in this Demo is basically the same as that in the HotFix framework. It mainly tells you the implementation process. If you just look at the code, you cannot apply it to your own app without practice, because you have a lot of in-depth knowledge to understand.
Principles first
I have already mentioned the implementation principle in the QQ space article. I will repeat it here:
-Android uses PathClassLoader as its class loader.
-A ClassLoader can contain multiple dex files. Each dex file is an Element and multiple dex files are arranged in an ordered manner.DexElementsArray
-When looking for a class, the system will traverse the dexElements array and find the class from the dex file. If it finds the class, it will return. Otherwise, the system will continue searching for the next dex file.
-The hot Patching solution is to package the problematic class into a dex file (such as patch. dex) separately, and then insert the dex to the beginning of the dexElements array.
OK. This is how HotFix performs hot Patching on the app,In fact, it is to use the ClassLoader loading mechanism to overwrite problematic methods. Then our so-called patch is to pack problematic classes into a package..
Let's talk about the problem.
Of course, it is not easy to implement dynamic hotfix. The first problem we solve is:
When the verify option is enabled when the VM is started, if the static method, private method, constructor, and so on are directly referenced (the first layer relationship) the classes are all in the same dex file, so the class will be marked with CLASS_ISPREERIFIED
As shown in:
If a class is marked with the CLASS_ISPREERIFIED flag, if another class referenced by this class is in another dex file, an error is reported. Simply put, before patching, the class you fixed has been marked. When you fix a bug through patching, you cannot complete the verification and report an error.
Solve the problem
To solve the problem mentioned in the previous section, you must prevent the related classes from marking the CLASS_ISPREERIFIED mark before the apk is packaged. The solution is as follows:
Insert a piece of code into the constructor of all classes, such:
public class BugClass { public BugClass() { System.out.println(AntilazyLoad.class); } public String bug() { return "bug class"; }}
The referenced AntilazyLoad class will be separately packaged into hack. dex, so that when the apk is installed, classes. classes in dex reference an AntilazyLoad class in different dex, which solves the CLASS_ISPREERIFIED Mark problem.
Implementation Details
I have explained the principles in the above sections, thrown out a problem, and then proposed a solution. I believe you have some knowledge about the hot Patching framework. At least we know what it is. The implementation details are as follows:
Create two classes
package com.devilwwj.hotfixdemo;/** * com.devilwwj.hotfixdemo * Created by devilwwj on 16/3/8. */public class BugClass { public String bug() { return "bug class"; }}
package com.devilwwj.hotfixdemo;/** * com.devilwwj.hotfixdemo * Created by devilwwj on 16/3/8. */public class LoadBugClass { public String getBugString() { BugClass bugClass = new BugClass(); return bugClass.bug(); }}
What we need to do is insert a piece of code in the constructor of the class files of these two classes:
System.out.println(AntilazyLoad.class);
Create a hackdex module and create an AntilazyLoad class
Just look at the figure:
Pack AntilazyLoad into a hack_dex.jar package separately.
Run the following command:
jar cvf hack.jar com.devilwwj.hackdex/*
This command will package the AntilazyLoad class into the hack. jar file.
dx --dex --output hack_dex.jar hack.jar
This command uses the dx tool to convert hack. jar to generate the hack_dex.jar file.
The dx tool is available in our sdk/build-tools.
Finally, we put the hack_dex.jar file under the assets Directory of the project:
Use javassist for dynamic code injection
Create the buildSrc module. This project is developed using Groovy and can be compiled successfully only by configuring the Groovy SDK.
Download the Groovy SDK: http://groovy-lang.org/download.html. After downloading the SDK, configure the project user Library.
It provides a method to inject code to the constructor of a specified class:
/*** Implant code ** @ param buildDir is the build class directory of the project, which is the location of the class to be injected * @ param lib This is the hackdex directory, is the location of the class file of the AntilazyLoad class */public static void process (String buildDir, String lib) {println (lib); ClassPool classes = ClassPool. getDefault () classes. appendClassPath (buildDir) classes. appendClassPath (lib) // Insert the reference code CtClass c = classes into the constructor of the class to be associated. getCtClass ("com. devilwwj. hotfixdemo. bugClass ") if (c. isFrozen () {c. defrost ()} println ("=== add constructor = c. getConstructors () [0]; constructor. insertBefore ("System. out. println (com. devilwwj. hackdex. antilazyLoad. class); ") c. writeFile (buildDir) CtClass c1 = classes. getCtClass ("com. devilwwj. hotfixdemo. loadBugClass ") if (c1.isFrozen () {c1.defrost ()} println (" === add constructor === ") def constructor1 = c1.getConstructors () [0]; constructor1.insertBefore ("System. out. println (com. devilwwj. hackdex. antilazyLoad. class); ") c1.writeFile (buildDir )}
Configure build. gradle of the app Project
The module created in the previous section provides corresponding methods for code injection to the project class. We need to configure it in build. gradle to make it do this automatically:
Apply plugin: 'com. android. application 'Task ('processslist') <{String classPath = file ('build/intermediates/classes/debug') // com. devilwwj. patch. patchClass. process (classPath, project (': hackdex '). buildDir. absolutePath + "/intermediates/classes/debug") // The second parameter is the directory where the hackdex class is located.} android {compileSdkVersion 23 buildToolsVersion "23.0.1" defaultConfig {applicationId "com. devilwwj. hotfixdemo "minSdkVersion 14 targetSdkVersion 23 versionCode 1 versionName" 1.0 "} buildTypes {debug {minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt '), 'proguard-rules. pro'} release {minifyEnabled false proguardFiles getdefadefaproguardfile('proguard-android.txt '), 'proguard-rules. pro'} applicationVariants. all {variant-> variant. dex. dependsOn <processwithpolicsist // enter the code into the class before executing the dx command} dependencies {compile fileTree (dir: 'libs', include :['*. jar ']) testCompile 'junit: junit: 4.12 'compile 'com. android. support: appcompat-v7: 23.1.1 'compile 'com. android. support: design: 23.1.1 'compile project (': hotfixlib ')}
In this case, run the project and decompile the app-debug.apk file under build/output/apk. then you can see that the code has been successfully implanted.
Decompilation tool for mac:
Https://sourceforge.net/projects/jadx? Source = typ_redirect
The Decompilation result is as follows:
In fact, you can also look at it directly in the project:
Create a hotfixlib module and associate it with the project.
This is almost the last step. It is also the core step. It provides the method to dynamically insert heck_dex.jar to dexElements.
Core code:
Package com. devilwwj. 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;/*** com. devilwwj. hotfixlib * Created by devilwwj on 16/3/9. */public final class HotFix {publi C static void patch (Context context, String patchDexFile, String patchClassName) {if (patchDexFile! = Null & new File (patchDexFile ). exists () {try {if (hasLexClassLoader () {injectAliyunOs (context, patchDexFile, patchClassName);} else if (hasDexClassLoader () {combine (context, patchDexFile, patchClassName);} else {injectBelowApiLevel14 (context, patchDexFile, patchClassName);} catch (Throwable th) {}} private static boolean hasLexClassLoader () {try {Class. forName ("dalvik. system. lexClassLoader "); return true;} catch (ClassNotFoundException e) {e. printStackTrace (); return false ;}} private static boolean hasDexClassLoader () {try {Class. forName ("dalvik. system. baseDexClassLoader "); return true;} catch (ClassNotFoundException e) {e. printStackTrace (); return false ;}} private static void injectAliyunOs (Context context, String patchDexFile, String patchClassName) throws ClassNotFoundException, NoSuchMethodException, expiration, InvocationTargetException, InstantiationException, noSuchFieldException {PathClassLoader obj = (PathClassLoader) context. getClassLoader (); String replaceAll = new File (patchDexFile ). getName (). replaceAll ("\\. [a-zA-Z0-9] + ",". lex "); Class cls = Class. forName ("dalvik. system. lexClassLoader "); Object newInstance = cls. getConstructor (new Class [] {String. class, String. class, String. class, ClassLoader. class }). newInstance (new Object [] {context. getDir ("dex", 0 ). getAbsolutePath () + File. separator + replaceAll, context. getDir ("dex", 0 ). getAbsolutePath (), patchDexFile, obj}); cls. getMethod ("loadClass", new Class [] {String. class }). invoke (newInstance, new Object [] {patchClassName}); setField (obj, PathClassLoader. class, "mPaths", appendArray (getField (obj, PathClassLoader. class, "mPaths"), getField (newInstance, cls, "mRawDexPath"); setField (obj, PathClassLoader. class, "mFiles", combineArray (getField (obj, PathClassLoader. class, "mFiles"), getField (newInstance, cls, "mFiles"); setField (obj, PathClassLoader. class, "mZips", combineArray (getField (obj, PathClassLoader. class, "mZips"), getField (newInstance, cls, "mZips"); setField (obj, PathClassLoader. class, "mLexs", combineArray (getField (obj, PathClassLoader. class, "mLexs"), getField (newInstance, cls, "mDexs");} @ TargetApi (14) private static void injectBelowApiLevel14 (Context context, String str, String str2) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {PathClassLoader obj = (PathClassLoader) context. getClassLoader (); DexClassLoader dexClassLoader = new DexClassLoader (str, context. getDir ("dex", 0 ). getAbsolutePath (), str, context. getClassLoader (); dexClassLoader. loadClass (str2); setField (obj, PathClassLoader. class, "mPaths", appendArray (getField (obj, PathClassLoader. class, "mPaths"), getField (dexClassLoader, DexClassLoader. class, "mRawDexPath"); setField (obj, PathClassLoader. class, "mFiles", combineArray (getField (obj, PathClassLoader. class, "mFiles"), getField (dexClassLoader, DexClassLoader. class, "mFiles"); setField (obj, PathClassLoader. class, "mZips", combineArray (getField (obj, PathClassLoader. class, "mZips"), getField (dexClassLoader, DexClassLoader. class, "mZips"); setField (obj, PathClassLoader. class, "mDexs", combineArray (getField (obj, PathClassLoader. class, "mDexs"), getField (dexClassLoader, DexClassLoader. class, "mDexs"); obj. loadClass (str2 );} /*** inject dex into the dexElements array * @ param context * @ param str * @ param str2 * @ throws ClassNotFoundException * @ throws NoSuchFieldException * @ throws IllegalAccessException */private static void limit (Context context, string str, String str2) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {PathClassLoader pathClassLoader = (PathClassLoader) context. getClassLoader (); Object a = combineArray (getDexElements (getPathList (pathClassLoader), getDexElements (getPathList (new DexClassLoader (str, context. getDir ("dex", 0 ). getAbsolutePath (), str, context. getClassLoader (); Object a2 = getPathList (pathClassLoader); setField (a2, a2.getClass (), "dexElements", a); pathClassLoader. loadClass (str2 );} /*** get pathList through PathClassLoader * @ param obj * @ return * @ throws ClassNotFoundException * @ throws handler */private static Object getPathList (Object obj) throws ClassNotFoundException, noSuchFieldException, IllegalAccessException {return getField (obj, Class. forName ("dalvik. system. baseDexClassLoader ")," pathList ");} /*** get the dexElements Object through pathList * @ param obj * @ return * @ throws NoSuchFieldException * @ throws IllegalAccessException */private static Object getDexElements (Object obj) throws NoSuchFieldException, illegalAccessException {return getField (obj, obj. getClass (), "dexElements ");} /*** get the specified Object through reflection * @ param obj * @ param cls * @ param str * @ return * @ throws NoSuchFieldException * @ throws IllegalAccessException */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 );} /*** set attributes through reflection * @ param obj * @ param cls * @ param str * @ param obj2 * @ throws NoSuchFieldException * @ throws IllegalAccessException */private static void setField (Object obj, class cls, String str, Object obj2) throws NoSuchFieldException, IllegalAccessException {Field declaredField = cls. getDeclaredField (str); declaredField. setAccessible (true); declaredField. set (obj, obj2);}/*** merge array * @ param obj * @ param obj2 * @ return */private static Object combineArray (Object obj, Object obj2) {Class componentType = obj2.getClass (). getComponentType (); int length = Array. getLength (obj2); int lengh2 = Array. getLength (obj) + length; Object newInstance = Array. newInstance (componentType, leng22.); for (int I = 0; I <leng2; I ++) {if (I <length) {Array. set (newInstance, I, Array. get (obj2, I);} else {Array. set (newInstance, I, Array. get (obj, I-length) ;}return newInstance ;} /*** add to array * @ param obj * @ param obj2 * @ return */private static Object appendArray (Object obj, Object obj2) {Class componentType = obj. getClass (). getComponentType (); int length = Array. getLength (obj); Object newInstance = Array. newInstance (componentType, length + 1); Array. set (newInstance, 0, obj2); for (int I = 0; I <length + 1; I ++) {Array. set (newInstance, I, Array. get (obj, I-1);} return newInstance ;}}
Prepare patches and test results
Patch is the bug fixing package for our program. If a bug occurs in our online package and you need to fix it urgently, you can find the class with the bug and fix it, then, package the repaired class file into a jar package so that the server can place the patch package in the specified location. Then, your program can download the patch package to sdcard, then the program automatically patches you to fix the problem.
For example, the BugClass we mentioned above:
Before repair:
public class BugClass { public String bug() { return "bug class"; }}
After repair:
Public class BugClass {public String bug () {return "Xiao Wu fixed the bug !!! ";}}
What you need to do is replace this class. How can this problem be solved?
First package:
Remember: It must be converted by the dx tool, and then the path must be <喎?http: www.bkjia.com kf ware vc " target="_blank" class="keylink"> VcD4NCjxwPnBhdGNoX2RleC5qYXK + fill + sNHL/LfFtb3P7sS/fill = "patch pack" src = "http://www.bkjia.com/uploads/allimg/160316/042002D31-8.png" title = "\"/>
Then the test result is displayed. See the dynamic diagram:
Well, it's done here. Our bug has been fixed.
Summary
This practice is based on the HotFix framework. Here, I would like to thank the author of open source, because it is not suitable for directly using the author's things and I don't know why, so I ran the whole process all over again. The so-called practice was actually known, and it would be less difficult to practice things that were originally difficult. During this period, the practice would naturally not be so smooth, the author encountered a pitfall, such as Groovy compilation and the class in the hack_dex package could not be found, but finally solved them one by one. After studying this hot update framework, the same is true for other hot update frameworks. Because the principles are the same, I don't have to worry about which one. Later, I will use this technology in projects, you don't have to worry about sending a package every time. Thank you for your patience. If you have any questions, leave a message.