When it comes to Android hotfixes, I’m sure you all know a little bit about them. Hot repair can be said to be after plug-in, another new technology. Current Android hotfix frameworks fall into two main categories:

Native Hook scheme has certain compatibility problems, and its hot repair is method-based. The subcontracting scheme of Java Dex has good compatibility and is accepted by the public. In fact, as early as the end of last year, HotFix and Nuwa have appeared, and their principle is the same, are based on the idea introduced in the article “Android App Hot Patch dynamic repair technology Introduction” released by qzone terminal development team. If you haven’t read this article, I strongly recommend reading it first.

Although the HotFix framework has now been Deprecated by author Dodola, this does not prevent us from parsing its source code. So let’s get down to business.

0x01

Let’s start with the HotFix project structure:

It can be seen that the project is mainly divided into four modules:

  • App: there is a Demo of HotFix usage;
  • BuildSrc: Gradle Task for compiling code injected at packaging time;
  • HackDex: Only one AntilazyLoad class is typed into hack.dex independently to prevent the CLASS_ISPREVERIFIED related problems.
  • Hotfixlib: the hotfix framework’s lib;

Let’s start with the app and take a look at HotfixApplication:

public class HotfixApplication extends Application { @Override public void onCreate() { super.onCreate(); File dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "hackdex_dex.jar"); / / the assets of the hackdex_dex. Jar copied to dexPath Utils. PrepareDex (enclosing 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

In the onCreate() method, there is very little code. PrepareDex: Copy hackdex_dex.jar from Assets into internal storage using utils.preparedex:

/** * Copy hack_dex from Assets to internal storage * @param context * @param dexInternalStoragePath * @param dex_file * @return */ public static boolean prepareDex(Context context, File dexInternalStoragePath, String dex_file) { BufferedInputStream bis = null; OutputStream dexWriter = null; try { 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; } catch (IOException e) { if (dexWriter ! = null) { try { dexWriter.close(); } catch (IOException ioe) { ioe.printStackTrace(); } } if (bis ! = null) { try { bis.close(); } catch (IOException ioe) { ioe.printStackTrace(); } } return false; }}Copy the code

HotFix. Patch:

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

In the patch method, there are three cases:

  1. Ali Cloud System;
  2. Android system API Level >= 14;
  3. Android SYSTEM API Level < 14;

In fact, aliyun hotfix and Android system API < 14 code is similar, that is, change.dex to.lex. Android API >= 14 and Android API < 14 are not analyzed here.

Android API Level >= 14

First to analyze injectAboveEqualApiLevel14 method:

private static void injectAboveEqualApiLevel14(Context context, String str, String str2) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader(); A = combineArray(getDexElements(getPathList(pathClassLoader)), getDexElements(getPathList( new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader())))); // Get the current pathList Object a2 = getPathList(pathClassLoader); // Set the merged DexElements[] array to the DexElements setField(a2, a2.getClass(), "DexElements ", a) in PathList; pathClassLoader.loadClass(str2); }Copy the code

Get the pathClassLoader inside the current context, Then call combineArray(getDexElements(getPathList(pathClassLoader)), getDexElements(getPathList(new DexClassLoader(STR, Context.getdir (“dex”, 0).getabsolutePath (), STR, context.getClassLoader())))). There are many layers of methods nested in this combineArray method, so let’s look at them one by one. The first is the getPathList method:

private static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList"); } 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); }Copy the code

GetPathList is the object that gets the BaseDexClassLoader attribute.

The PathClassLoader class inherits from BaseDexClassLoader:

After we get the pathList, we call getDexElements. As the name implies, you get the dexElements attribute in pathList.

private static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {return getField(obj, obj.getClass(), "dexElements"); }Copy the code

So the parameters passed into the combineArray method are Elements[]. One is dexElements in the current application and the other is dexElements in hackdex_dex.jar.

Here’s the combineArray source code:

private static Object combineArray(Object obj, Object obj2) {// Get the DexElements[] array class Class componentType = obj2.getClass().getComponentType(); Int length = array.getLength (obj2); Length2 = array.length2 (obj) + length2; 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; }Copy the code

The main thing to do is to combine the two dexElements passed in into one dexElements. Note, however, that the dex in the second Obj2 should come before obJ in order to achieve a hot fix.

Finally, we come back to see injectAboveEqualApiLevel14 method in the rest of the code:

// Get the current pathList Object a2 = getPathList(pathClassLoader); // Set the merged DexElements[] array to PathList setField(a2, a2.getClass(), "DexElements ", a); / / load first dodola. Hackdex. AntilazyLoad. Class pathClassLoader. LoadClass (str2);Copy the code

I think you can understand these lines of code. It walked injectAboveEqualApiLevel14 throughout the process. For the rest, let’s look at injectBelowApiLevel14.

Android API Level < 14

InjectBelowApiLevel14 injectBelowApiLevel14

@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());
    // why load class str2
    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);
}Copy the code

We find that in API Level < 14, the process is the same as in API Level >= 14, but there are more attributes to merge. Mainly because the ClassLoader source code has changed, so make compatible by version. Here is not analyzed, after seeing injectAboveEqualApiLevel14 believe injectBelowApiLevel14 must also understand.

0x02

In MainActivity, a hot fix is made.

File dexPath = new File(getDir("dex", context.mode_private), "path_dex.jar"); Utils.prepareDex(this.getApplicationContext(), dexPath, "path_dex.jar"); // DexInjector.inject(dexPath.getAbsolutePath(), defaultDexOptPath, "dodola.hotfix // .BugClass"); HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hotfix.BugClass");Copy the code

Surprisingly, the hotfix code in MainActivity is identical to the hackdex_dex.jar code in HotfixApplication above. Yeah, it’s the same process, so it’s easy to understand the same thing.

0x03

This is the whole logic of HotFix. However, we still have a problem to solve, which is how to introduce AntilazyLoad dynamically into the constructor. HotFix uses JavAssist for dynamic code injection. The code is in buildSrc:

@param buildDir is the project's build class directory, where we need to inject the class. @param lib is hackdex's directory, where AntilazyLoad's class file is located */ public static void process(String buildDir, String lib) { println(lib) ClassPool classes = ClassPool.getDefault() classes.appendClassPath(buildDir) Class.appendclasspath (lib) // Insert the reference CtClass c = class.getctClass (" dodola.hotfix.bugClass ") in the constructor of the class that needs to be associated If (c.isfrozen ()) {c.frost ()} println("==== add constructor ====") def constructor = c.getconstructors ()[0]; constructor.insertBefore("System.out.println(dodola.hackdex.AntilazyLoad.class);" ) c.writeFile(buildDir) CtClass c1 = classes.getCtClass("dodola.hotfix.LoadBugClass") if (c1.isFrozen()) { c1.defrost() } println("==== add constructors ====") def constructor1 = c1.getconstructors ()[0]; constructor1.insertBefore("System.out.println(dodola.hackdex.AntilazyLoad.class);" ) c1.writeFile(buildDir) }Copy the code

0x04

This is the HotFix framework as a whole, and it’s relatively simple. Now the author has rewritten a RocooFix framework to solve the problem that Gradle 1.4 and above cannot be packaged. If interested in children’s shoes can be concerned about.

So that’s all for today, bye bye!

0x05

References

Adhere to original technology sharing, your support will encourage me to continue to create!

WeChat exceptional

Alipay rewards