QFix解決熱修復pre-verified問題

QFix方案實現

原理可參考這篇文章QFix探索之路—手Q熱補丁輕量級方案java

前序文章參考 熱修復之冷啓動類加載原理與實現android

方案

回到這張圖,從dvmResolveClass方法入手,提早解析patch類。 image.png 一開始想到的方案是提早使用"const-class" 或者 "instance-of"指令建立類,fromUnverifiedConstant = true,繞過dex檢測。實際也成功了。但有兩個問題:git

  • 怎麼提早知道哪些補丁類?
  • 或者乾脆引用全部類?性能問題?如何實現?
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);
}    
複製代碼

QFix放棄了此直接load patch class的方案。通過分析,github

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

獲得以下方案:markdown

  • 構建apk時,dex預先埋入空白類,同時獲得每一個dex與空白類的關聯文件。
  • 構建補丁包,映射關聯即bug dex的空白類與補丁類在原dex中的 classIdx。
  • -----------運行app,加載補丁包-------------
  • 使用java方法,調用classLoader.loadClass(空白類name)
  • 使用jni方法,調用 dvmFindLoadedClass(空白類descriptor)
  • 使用jni方法,調用dvmResolveClass(referrer:空白類,classIdx,fromUnverifiedConstant:true)

至於怎麼找到這個方法的,固然就是源碼裏面遊蕩了。app

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

實操

所有實現代碼都在github中ide

空白類注入到Dex

自定義gradle插件,使用smali操做dexfile,注入class。oop

1.buildSrc/build.gradle加入依賴post

//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;
}
複製代碼

到此,代碼就所有實現了。

相關文章
相關標籤/搜索