QFix解决热修复pre-verified问题

900 阅读3分钟

QFix方案实现

原理可参考这篇文章QFix探索之路—手Q热补丁轻量级方案

前序文章参考 热修复之冷启动类加载原理与实现

方案

回到这张图,从dvmResolveClass方法入手,提前解析patch类。 image.png

一开始想到的方案是使用"const-class" 或者 "instance-of"指令创建类,
来提前调用dvmResloveClass()方法,此时参数fromUnverifiedConstant 为 true,绕过dex检测。 dvmResloveClass()最后会将此class与referrer所在的Dex建立缓存映射。

实际也成功了。

public class ApplicationApp extends Application {
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);

        DexInstaller.installDex(base, this.getExternalCacheDir().getAbsolutePath() + "/patch.dex");
		//会执行 const-class 指令
        Log.d("alvin", "bug class:" + com.a.fix.M.class);
}    

但有个问题:怎么能提前知道哪些dex需要打补丁类?

QFix放弃了此直接load patch class的方案。

经过分析,

  • 补丁包中的class数量是有限的。
  • apk中dex文件的数量也是有限的。

得到如下方案:

  • 构建apk时,每个dex预先埋入空白类HackXX,存储映射每个dexXX--空白类HackXX。
  • 构建补丁包,存储映射空白类HackXX--补丁类--补丁类在原dex中的classIdx。方便后续通过classIdx反查className
  • -----------运行app,加载补丁包-------------
  • 使用java方法,调用classLoader.loadClass(空白类name)
  • 使用jni方法,调用 dvmFindLoadedClass(空白类descriptor)
  • 使用jni方法,调用dvmResolveClass(referrer:空白类,classIdx,fromUnverifiedConstant:true)

这样我们就在空白类HackXX所在dex与补丁class建立了关联,后续都会通过dvmDexGetResloved(pDvmDex,classIdx)直接获取

截屏2020-12-02 下午9.46.32.png

实操

全部实现代码都在github中

空白类注入到Dex

自定义gradle插件,使用smali操作dexfile,注入class。

1.buildSrc/build.gradle加入依赖

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

2.plugin代码

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)
                            }
                        }
                    }
                }
            }
        }
    }
}

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() {
                    return new HashSet<>(classDefs);
                }

                @Override
                public Opcodes getOpcodes() {
                    return dexFile.getOpcodes();
                }
            });
            System.out.println("Outputs injectHackClass: " + file.getName() + ":" + className);
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

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

执行指令 dexdump

#dexdump -h classes2.dex > classes2.dump

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

我们可以得到mapping.txt

classes2.dex:com.a.Hack2:com.a.fix.M:2277

截屏2020-12-04 上午10.46.12.png

导入patch.dex和mapping.text

load patch.dex

patch.dex的生成和加载不变,参看本文上方。

resolve 补丁M.class

同样在ApplicationApp.attachBaseContext()中执行,在load patch之后执行。 代码文件 ApplicationApp.java

  • 解析Mapping.txt,得到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());
        }
    }

nativeResolveClass 就是正常的jni方法,代码实际也是简单的。

#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__)

//方法指针
void *(*dvmFindLoadedClass)(const char *);

//方法指针
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 即为 com.a.Hack2
    void *referrerClassObj = dvmFindLoadedClass(descriptorChars);
    dvmResolveClass(referrerClassObj, classIdx, true);
    return true;
}

到此,代码就全部实现了。