This article is from netease Cloud community

Author: Wang Chenyan


preface

Hotfix and plug-in are two hot technologies in the Android field, and they are also necessary skills for Android developers.

At present, the popular thermal repair programs include Tinker by wechat, Sophix by Handtao, Robust by Meituan, and qspace thermal repair program.

QQ space hot repair program using Java implementation, relatively easy to use.

If you do not understand the principle of Qzone solution, please learn the introduction of Hot patch dynamic repair technology of Android App first

Today, we will study the principle of hot repair in depth based on QQ space scheme, and complete a hot repair frame hand in hand.

Thanks for reference to Nuwa for this article.

This article is based on Gradle 2.3.3 and supports Gradle 1.5.0-3.0.1.

In actual combat

After understanding the thermal repair principle, we started to build a thermal repair framework

  • Disable DEX Check

According to the first question mentioned in the article, in Android 5.0 and later, classes that do not reference other dex are marked with CLASS_ISPREVERIFIED to improve the loading speed of dex.

If the class marked CLASS_ISPREVERIFIED is marked, the class loader will not look for the class in other dex, and we cannot substitute the class by staking.

The solution is to make all classes depend on other dex. How do you do that?

Create a Hack class, make all classes depend on the class, package the class into a dex, and insert the dex to the front of the array when the application starts.

Okay, once we know what we’re thinking, we’ll get started.

  • Find the compiled class

As simple as it sounds, how do you make all your classes depend on Hack classes? You can’t change them one by one.

Gradle Hooks and ASM are used next.

If you are not familiar with the Gradle build process, do so

To modify the compiled class file, we need to Hook the packaging process first. We implant our code between the time Gradle compiles the class file and the time it is packaged into APK.

Gradle Hooks are used to find compiled class files, and ASM is used to modify class files.

First, we need to find the compiled class file

Create a new Project CFixExample and execute assembleDebug

Watch the Gradle Console output

:app:preBuild UP-TO-DATE :app:preDebugBuild UP-TO-DATE :app:checkDebugManifest :app:preReleaseBuild UP-TO-DATE :app:prepareComAndroidSupportAnimatedVectorDrawable2540Library// Omit part of the Task: : app prepareComAndroidSupportSupportVectorDrawable2540Library: app: prepareDebugDependencies :app:compileDebugAidl UP-TO-DATE :app:compileDebugRenderscript UP-TO-DATE :app:generateDebugBuildConfig UP-TO-DATE :app:generateDebugResValues UP-TO-DATE :app:generateDebugResources UP-TO-DATE :app:mergeDebugResources UP-TO-DATE :app:processDebugManifest UP-TO-DATE :app:processDebugResources UP-TO-DATE :app:generateDebugSources UP-TO-DATE :app:incrementalDebugJavaCompilationSafeguard :app:javaPreCompileDebug :app:compileDebugJavaWithJavac :app:compileDebugNdk NO-SOURCE :app:compileDebugSources :app:mergeDebugShaders :app:compileDebugShaders :app:generateDebugAssets :app:mergeDebugAssets :app:transformClassesWithDexForDebug :app:mergeDebugJniLibFolders :app:transformNativeLibsWithMergeJniLibsForDebug :app:processDebugJavaRes NO-SOURCE :app:transformResourcesWithMergeJavaResForDebug :app:validateSigningDebug :app:packageDebug :app:assembleDebug BUILD SUCCESSFULin 10sCopy the code

These are all the tasks that Gradle performs when it is packaged. Gradle versions are different. Here we are based on Gradle 2.3.3.

Please note processDebugManifest and transformClassesWithDexForDebug these two Task, according to the names we can guess first

The purpose of the first Task should be to process the Manifest, which we will use later

The second Task will convert the class to dex, which is the Hook point we are looking for.

Yes, in order to validate our speculation, we print the transformClassesWithDexForDebug input file

Add the following code to your app’s build.gradle

project.afterEvaluate {
    project.android.applicationVariants.each { variant ->
        Task transformClassesWithDexTask = project.tasks.findByName("transformClassesWithDexFor${variant.name.capitalize()}")
        println("transformClassesWithDexTask inputs")
        transformClassesWithDexTask.inputs.files.each { file ->
            println(file.absolutePath)
        }
    }
}Copy the code

Package again and observe the output

transformClassesWithDexTask inputs
C:\Users\hzwangchenyan\.android\build-cache\97c23f4056f5ee778ec4eb674107b6b52d506af5\output\jars\classes.jar
C:\Users\hzwangchenyan\.android\build-cache\6afe39630b2c3d3c77f8edc9b1e09a2c7198cd6d\output\jars\classes.jar C:\Users\hzwangchenyan\.android\build-cache\c30268348acf4c4c07940f031070b72c4efa6bba\output\jars\classes.jar C:\Users\hzwangchenyan\.android\build-cache\5b09d9d421b0a6929ae76b50c69f95b4a4a44566\output\jars\classes.jar C:\Users\hzwangchenyan\.android\build-cache\e302262273df85f0776e06e63fde3eb1bdc3e57f\output\jars\classes.jar C: \ Users \ hzwangchenyan \. Gradle \ caches \ \ modules - 2 files - 2.1 \ com. Android. Support, support - annotations, 25.4.0 \ f6a2fc748 ae3769633dea050563e1613e93c135e \ support - annotations - 25.4.0. Jar C:\Users\hzwangchenyan\.android\build-cache\36b7224f035cc886381f4287c806a33369f1cb1a\output\jars\classes.jar C:\Users\hzwangchenyan\.android\build-cache\5d757d92536f0399625abbab92c2127191e0d073\output\jars\classes.jar C:\Users\hzwangchenyan\.android\build-cache\011eb26fd0abe9f08833171835fae10cfda5e045\output\jars\classes.jar D: \ Android \ \ extras \ m2repository \ com \ Android SDK, support, the constraint, the constraint - layout - solver \ 1.0.2 \ constraint, layout, and solve R - 1.0.2. Jar C: \ Users \ hzwangchenyan \. 36 b443908e839f37d7bd7eff1ea793f138f8d0dd android \ build - cache \ \ output \ jars \ classes. The jar  C:\Users\hzwangchenyan\.android\build-cache\40634d621fa35fcca70280efe0ae897a9d82ef8f\output\jars\classes.jar D:\Android\AndroidStudioProjects\CFixExample\app\build\intermediates\classes\debugCopy the code

Build-cache is a support package

It looks like these are libraries that the app depends on, but our own code

Take a look at the app\ Build \intermediates\classes\debug directory on the last line

Yes, it’s our own code, so it looks like we were right.

  • Insert a class into a reference to a Hack

Having found the compiled class file, use ASM to modify the class file

ClassReader cr = new ClassReader(inputStream)
ClassWriter cw = new ClassWriter(cr, 0)
ClassVisitor cv = new ClassVisitor(Opcodes.ASM4, cw) {    @Override
    MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions)
        mv = new MethodVisitor(Opcodes.ASM4, mv) {            @Override
            void visitInsn(int opcode) {                if ("<init>".equals(name) && opcode == Opcodes.RETURN) {                    super.visitLdcInsn(Type.getType("Lme/wcy/cfix/Hack;"))
                }                super.visitInsn(opcode)
            }
        }        return mv
    }
}
cr.accept(cv, 0)Copy the code

We get all the methods of the class by overwriting the visitMethod method of the ClassVisitor, and insert a reference to the Hack class in the constructor.

As you can see, the source file to be packaged as dex has both jar and class. The class file can be modified directly. For the JAR file, we need to unzip it first, modify the uncompressed class file, and then compress it.

File optDirFile = new File(jarFile.absolutePath.substring(0, jarFile.absolutePath.length() - 4))
File metaInfoDir = new File(optDirFile, "META-INF")
File optJar = new File(jarFile.parent, jarFile.name + ".opt")

CFixFileUtils.unZipJar(jarFile, optDirFile)if (metaInfoDir.exists()) {
    metaInfoDir.deleteDir()
}

optDirFile.eachFileRecurse { file ->
    if (file.isFile()) {
        processClass(file, hashFile, hashMap, patchDir, extension)
    }
}

CFixFileUtils.zipJar(optDirFile, optJar)
jarFile.delete()
optJar.renameTo(jarFile)
optDirFile.deleteDir()Copy the code
  • Save the Hash value of the file

Our purpose today is to build a hotfix framework, because we need to keep a record of the classes that we introduced the Hack to let us know which classes have changed when we patch the code. We just need to package the modified classes as patches.

We know that at compile time, the bytecode of the same Java file is the same after it is compiled to class, so we can just calculate the Hash value of the file and save it.

Compare the Hash values of the class files during patch making. If they are different, package them into the patch.

  • Insert the Hack dex

New Hack. Java

public class Hack {
}Copy the code

Insert the dex containing the Hack class into the top of the dex array, otherwise there will be a Hack ClassNotFoundException. Located in the/SDK/build – the tools/version/dx

dx --dex --output=patch.jar classDirCopy the code

Package as dex and compress as JAR

How do I insert it at the top of the array, just like a regular patch file, just before the regular patch file

public static void injectDexAtFirst(String dexPath, String defaultDexOptPath) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
    DexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, dexPath, getPathClassLoader());    Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));    Object newDexElements = getDexElements(getPathList(dexClassLoader));    Object allDexElements = combineArray(newDexElements, baseDexElements);    Object pathList = getPathList(getPathClassLoader());
    ReflectionUtils.setField(pathList, pathList.getClass(), "dexElements", allDexElements);
}Copy the code

In this section, the dexElements of BaseDexClassLoader are modified by reflection.

Where does the dex file come from? We can put the dex inside assets and copy it to the application directory before inserting it.

This operation is performed in the attachBaseContext of the Application.

Netease Cloud Free experience pavilion, 0 cost experience 20+ cloud products!

For more information about NETEASE’s r&d, product and operation experience, please visit netease Cloud Community.



“Cross-view granularity computing” –1. Understand the granularity of data