QFix方案实现
原理可参考这篇文章QFix探索之路—手Q热补丁轻量级方案
前序文章参考 热修复之冷启动类加载原理与实现
方案
回到这张图,从dvmResolveClass方法入手,提前解析patch类。
一开始想到的方案是使用"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)直接获取
实操
全部实现代码都在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
导入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;
}
到此,代码就全部实现了。