QFix scheme implementation

The principle can be found in this article: QFix Road to Discovery – Manual Q hot patch lightweight solution

The principle and implementation of cold start class loading for hot repair are referred to in the preceding article

plan

Go back to this figure and start with dvmResolveClass method to resolve the Patch class in advance.The initial solution was to create a class with the “const-class” or “instance-of” directives, fromUnverifiedConstant = true, to bypass the dex detection. And it worked. But there are two problems:

  • How do I know which patch classes in advance?
  • Or simply refer to all classes? Performance issues? How to do that?
public class ApplicationApp extends Application {
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);

        DexInstaller.installDex(base, this.getExternalCacheDir().getAbsolutePath() + "/patch.dex");
		// The const-class directive is executed
        Log.d("alvin"."bug class:" + com.a.fix.M.class);
}    
Copy the code

QFix gives up the scheme of directly loading patch class. After analysis,

  • The number of classes in a patch pack is limited.
  • The number of DEX files in APK is also limited.

The following scheme is obtained:

  • When apK is constructed, dex is pre-embedded with blank classes, and the associated files of each dex and blank classes are obtained.
  • Build the patch package and map the association between the blank class of bug dex and the patch class in the original dex classIdx.
  • ———– Run the app and load the patch package ————-
  • Using the Java method, call classLoader.loadClass(blank class name)
  • Using the JNI method, call dvmFindLoadedClass(blank class Descriptor)
  • Using jni method, called dvmResolveClass (referrer: blank, classIdx, fromUnverifiedConstant: true)

As for how to find this method, of course, is the source inside the wandering.

In field

All the implementation code is in Github

Blank classes are injected into Dex

Custom Gradle plugin, use smali to manipulate dexfile and inject class.

  • Custom Gradle plugin reference
  • Smali technical reference
  • Class is injected into dex

BuildSrc /build.gradle adds dependencies

//buildSrc/build.gradle
dependencies {
 	...
    compile group: 'org.smali'.name: 'dexlib2'.version: '2.2.4'. }Copy the code

2. The plugin code

class QFixPlugin implements Plugin<Project> {

    void apply(Project project1) {
        project1.afterEvaluate { project ->
            project.tasks.mergeDexDebug {
                doLast {
                    println 'QFixPlugin inject Class after mergeDexDebug'
                    project.tasks.mergeDexDebug.getOutputs().getFiles().each { dir ->
                        println "outputs: " + dir
                        if(dir ! =null && dir.exists()) {
                            def files = dir.listFiles()
                            files.each { file ->
                                String dexfilepath = file.getAbsolutePath()
                                println "Outputs Dex file's path: " + dexfilepath
                                  InjectClassHelper.injectHackClass(dexfilepath)
                            }
                        }
                    }
                }
            }
        }
    }
}

Copy the code

InjectClassHelper.java

public class InjectClassHelper {

    public static void injectHackClass(String dexPath) {
        try {
            File file = new File(dexPath);
            String fileName = file.getName();
            String indexStr = fileName.split("\ \.") [0].replace("classes"."");
            System.out.println(" =============indexStr:"+indexStr);
            String className = "com.a.Hack"+ indexStr;
            String classType = "Lcom/a/Hack" + indexStr + ";";
            
            DexBackedDexFile dexFile = DexFileFactory.loadDexFile(dexPath, Opcodes.getDefault());
			ImmutableDexFile immutableDexFile = ImmutableDexFile.of(dexFile);

            Set<ClassDef> classDefs = new HashSet<>();
            for (ImmutableClassDef classDef : immutableDexFile.getClasses()) {
                classDefs.add(classDef);
            }
            ImmutableClassDef immutableClassDef = new ImmutableClassDef(
                    classType,
                    AccessFlags.PUBLIC.getValue(),
                    "Ljava/lang/Object;".null.null.null.null.null);
            classDefs.add(immutableClassDef);

            String resultPath = dexPath;
            File resultFile = new File(resultPath);
            if(resultFile ! =null && resultFile.exists()) resultFile.delete();
            DexFileFactory.writeDexFile(resultPath, new DexFile() {
                
                @Override
                public Set<ClassDef> getClasses(a) {
                    return new HashSet<>(classDefs);
                }

                @Override
                public Opcodes getOpcodes(a) {
                    returndexFile.getOpcodes(); }}); System.out.println("Outputs injectHackClass: " + file.getName() + ":" + className);
            
        } catch(Exception e) { e.printStackTrace(); }}}Copy the code

Mapping

Outputs Dex file's path: /Users/mawenqiang/Documents/demo_project/hotfix_dexload/dexload_QFix/build/intermediates/dex/debug/out/classes2.dex
Outputs injectHackClass: classes2.dex:com.a.Hack2
Outputs Dex file's path: /Users/mawenqiang/Documents/demo_project/hotfix_dexload/dexload_QFix/build/intermediates/dex/debug/out/classes.dex
Outputs injectHackClass: classes.dex:com.a.Hack
Copy the code

Run the dexdump command

#dexdump -h classes2.dex > classes2.dump

Class #1697 header:
class_idx           : 2277 #class_idx
......
Class descriptor  : 'Lcom/a/fix/M; '.Copy the code

We can get mapping.txt

classes2.dex:com.a.Hack2:com.a.fix.M:2277
Copy the code

Import patch.dex and Mapping.text

load patch.dex

The generation and loading of patch.dex remain unchanged, as shown above.

Resolve patch M.c lass

Also in ApplicationApp. AttachBaseContext (), in the load patch after execution. The code file applicationApp.java

  • Analytical Mapping. TXT, get hackClassName patchClassIdx
  • classLoader.loadClass(com.a.Hack2)
  • nativeResolveClass(hackClassDescriptor, patchClassIdx)
public static void resolvePatchClasses(Context context) {
        try {
            BufferedReader br = new BufferedReader(new FileReader(context.getExternalCacheDir().getAbsolutePath() + "/classIdx.txt"));
            String line = "";
            while(! TextUtils.isEmpty(line = br.readLine())) { String[] ss = line.split(":");
                //classes2.dex:com.a.Hack2:com.a.fix.M:2277
                if(ss ! =null && ss.length == 4) {
                    String hackClassName = ss[1];
                    long patchClassIdx = Long.parseLong(ss[3]);
                    Log.d("alvin"."readLine:" + line);
                    String hackClassDescriptor = "L" + hackClassName.replace('. '.'/') + ";";
                    Log.d("alvin"."classNameToDescriptor: " + hackClassName + "-- >" + hackClassDescriptor);
                    ResolveTool.loadClass(context, hackClassName);
                    ResolveTool.nativeResolveClass(hackClassDescriptor, patchClassIdx);
                }
            }
            br.close();

        } catch(Exception e) { e.printStackTrace(); }}/**
     * * "descriptor" should have the form "Ljava/lang/Class;" or
     * * "[Ljava/lang/Class;", i.e. a descriptor and not an internal-form
     * * class name.
     *
     * @param referrerDescriptor
     * @param classIdx
     * @return* /
    public static native boolean nativeResolveClass(String referrerDescriptor, long classIdx);

    public static void loadClass(Context context, String className) {
        try {
            Log.d("alvin", context.getClassLoader().loadClass(className).getSimpleName());
        } catch (Exception e) {
            e.printStackTrace();
            Log.d("alvin", e.getMessage()); }}Copy the code

NativeResolveClass is a normal JNI method, and the code is actually simple.

#include <jni.h>
#include <android/log.h>
#include <dlfcn.h>

#define  LOG_TAG    "alvin"
#define  LOGE(...)  __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)

// Method pointer
void *(*dvmFindLoadedClass)(const char *);

// Method pointer
void *(*dvmResolveClass)(const void *, unsigned int.bool);


extern "C" jboolean Java_com_a_dexload_qfix_ResolveTool_nativeResolveClass(JNIEnv *env, jclass thiz, jstring referrerDescriptor, jlong classIdx) {
    LOGE("enter nativeResolveClass");
    void *handle = 0;
    handle = dlopen("/system/lib/libdvm.so", RTLD_LAZY);
    if(! handle)LOGE("dlopen libdvm.so fail");
    if(! handle)return false;

    const char *loadClassSymbols[3] = {
            "_Z18dvmFindLoadedClassPKc"."_Z18kvmFindLoadedClassPKc"."dvmFindLoadedClass"};
    for (int i = 0; i < 3; i++) {
        dvmFindLoadedClass = reinterpret_cast<void* (*) (const char> (*)dlsym(handle, loadClassSymbols[i]));
        if (dvmFindLoadedClass) {
            LOGE("dlsym dvmFindLoadedClass success %s", loadClassSymbols[i]);
            break; }}const char *resolveClassSymbols[2] = {"dvmResolveClass"."vResolveClass"};
    for (int i = 0; i < 2; i++) {
        dvmResolveClass = reinterpret_cast<void* (*) (const void *, unsigned int.bool) > (dlsym(handle, resolveClassSymbols[i]));
        if (dvmResolveClass) {
            LOGE("dlsym dvmResolveClass success %s", resolveClassSymbols[i]);
            break; }}if(! dvmFindLoadedClass)LOGE("dlsym dvmFindLoadedClass fail");
    if(! dvmResolveClass)LOGE("dlsym dvmResolveClass fail");
    if(! dvmFindLoadedClass || ! dvmResolveClass)return false;

    const char *descriptorChars = (*env).GetStringUTFChars(referrerDescriptor, 0);
    / / referrerClassObj is com. A.H ack2
    void *referrerClassObj = dvmFindLoadedClass(descriptorChars);
    dvmResolveClass(referrerClassObj, classIdx, true);
    return true;
}
Copy the code

At this point, the code is fully implemented.