CLASS_ISPREVERIFIED
前边提到过,QQ空间超级补丁的实现方式是将发生错误的class打包到一个单独的dex中,然后将修复后的dex插入到系统的dexElements数组中,从而实现修复bug的效果。
我们知道,Android是支持多dex的,我们编写代码创建了很多的类,最终打包之后可能会只有一个dex或者多个dex,而每个类与类之间可能是存在引用关系的,如A引用了B,那么存在这样两种情况,第一是A和B最终打包后都在一个dex中,第二种是,A和B不在一个dex中。我们假设A和B在同一个dex中,并且A引用到的类都在它所在的这个dex,则加载A这个类时,A类会被打上一个标记: CLASS_ISPREVERIFIED。
QQ空间开发团队-安卓App热补丁动态修复技术介绍
那么问题就来了,假设B类出现了异常,那么我们将这个类修复后,会单独打到一个dex中,那么很明显就和A不在同一个dex了,将这个dex插入到系统dexElements中后,在加载到这个类的时候, 在5.0以下的版本上会抛出异常

使用补丁包中的B类取代出现bug的B,则会导致A与其引用的B不在同一个Dex, 但A已经被打上标记,此时出现冲突。导致校验失败!
解决办法-防止类被打上CLASS_ISPREVERIFIED标记
我们有必要让所有的类都不会被打上这个标志,那么怎么做呢?从前边的描述我们大概知道了,只有当类引用到的类都在同一个dex时,才被打上这个标记,那么我们是不是强制的要求每个类都去引用到另一个dex中的类就能避免这种情况了呢?是的。
我门创建一个类,随便命名一下,这里就叫做AntilazyLoad(qq命名的),把这个类生成一个dex,那么这个类就是我们项目中所有的类都要去引用的一个类,(比如在构造方法中,每个类都有构造方法)所有类都引用它所以所有的类都不会被打上CLASS_ISPREVERIFIED标记
public MainActivity() {
Class var10000 = AntilazyLoad.class;
}
但是要怎么做呢,我们是不可能直接通过Java代码去引用另一个dex中的类的。所以就要用到字节码插桩技术,最终实现的效果就是上边那段代码
//gradle执行会解析build.gradle文件,afterEvaluate表示在解析完成之后再执行我们的代码
afterEvaluate({
android.getApplicationVariants().all {
variant ->
//获得: debug/release
String variantName = variant.name
//首字母大写 Debug/Release
String capitalizeName = variantName.capitalize()
//这就是打包时,把jar和class打包成dex的任务
Task dexTask =
project.getTasks().findByName("transformClassesWithDexBuilderFor" + capitalizeName);
//在他打包之前执行插桩
dexTask.doFirst {
//任务的输入,dex打包任务要输入什么? 自然是所有的class与jar包了!
FileCollection files = dexTask.getInputs().getFiles()
for (File file : files) {
//.jar ->解压-》插桩->压缩回去替换掉插桩前的class
// .class -> 插桩
String filePath = file.getAbsolutePath();
//依赖的库会以jar包形式传过来,对依赖库也执行插桩
if (filePath.endsWith(".jar")) {
processJar(file);
} else if (filePath.endsWith(".class")) {
//主要是我们自己写的app模块中的代码
processClass(variant.getDirName(), file);
}
}
}
}
})
static boolean isAndroidClass(String filePath) {
return filePath.startsWith("android") ||
filePath.startsWith("androidx");
}
static byte[] referHackWhenInit(InputStream inputStream) throws IOException {
// class的解析器
ClassReader cr = new ClassReader(inputStream)
// class的输出器
ClassWriter cw = new ClassWriter(cr, 0)
// class访问者,相当于回调,解析器解析的结果,回调给访问者
ClassVisitor cv = new ClassVisitor(Opcodes.ASM5, cw) {
//要在构造方法里插桩 init
@Override
public MethodVisitor visitMethod(int access, final String name, String desc,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
mv = new MethodVisitor(Opcodes.ASM5, mv) {
@Override
void visitInsn(int opcode) {
//在构造方法中插入AntilazyLoad引用
if ("<init>".equals(name) && opcode == Opcodes.RETURN) {
//引用类型
//基本数据类型 : I J Z
super.visitLdcInsn(Type.getType("Lcom/enjoy/patch/hack/AntilazyLoad;"));
}
super.visitInsn(opcode);
}
};
return mv;
}
};
//启动分析
cr.accept(cv, 0);
return cw.toByteArray();
}
/**
* linux/mac: /xxxxx/app/build/intermediates/classes/debug/com/enjoy/qzonefix/MainActivity.class
* windows: \xxxxx\app\build\intermediates\classes\debug\com\enjoy\qzonefix\MainActivity.class
* @param file
* @param hexs
*/
static void processClass(String dirName, File file) {
String filePath = file.getAbsolutePath();
//注意这里的filePath包含了目录+包名+类名,所以去掉目录
String className = filePath.split(dirName)[1].substring(1);
//application或者android support我们不管
if (className.startsWith("com/enjoy/hotfix/MyApplication") || isAndroidClass(className)) {
// if (className.startsWith("com\\enjoy\\hotfix\\MyApplication") || isAndroidClass(className)) { //这种写法在mac上不行
return
}
try {
// byte[]->class 修改byte[]
FileInputStream is = new FileInputStream(filePath);
//执行插桩 byteCode:插桩之后的class数据,把他替换掉插桩前的class文件
byte[] byteCode = referHackWhenInit(is);
is.close();
FileOutputStream os = new FileOutputStream(filePath)
os.write(byteCode)
os.close()
} catch (Exception e) {
e.printStackTrace();
}
}
static void processJar(File file) {
try {
// 无论是windows还是linux jar包都是 /
File bakJar = new File(file.getParent(), file.getName() + ".bak");
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(bakJar));
JarFile jarFile = new JarFile(file);
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry jarEntry = entries.nextElement();
// 读jar包中的一个文件 :class
jarOutputStream.putNextEntry(new JarEntry(jarEntry.getName()));
InputStream is = jarFile.getInputStream(jarEntry);
String className = jarEntry.getName();
if (className.endsWith(".class") && !className.startsWith
("com/enjoy/hotfix/MyApplication")
&& !isAndroidClass(className) && !className.startsWith("com/enjoy" +
"/patch")) {
byte[] byteCode = referHackWhenInit(is);
jarOutputStream.write(byteCode);
} else {
//输出到临时文件
jarOutputStream.write(IOUtils.toByteArray(is));
}
jarOutputStream.closeEntry();
}
jarOutputStream.close();
jarFile.close();
file.delete();
bakJar.renameTo(file);
} catch (IOException e) {
e.printStackTrace();
}
}
附
字节码插桩
build.gradle
testImplementation 'org.ow2.asm:asm:7.1'
testImplementation 'org.ow2.asm:asm-commons:7.1'
package com.rzm.myapplication;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
import org.objectweb.asm.commons.Method;
import java.io.FileInputStream;
import java.io.FileOutputStream;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*
* 字节码插桩技术对字节码指令要求相当熟悉才行:可以先把要插入的代码写成java代码,然后转成字节码指令,然后按照指令写
*
* 插桩前:
*
* public class AntilazyLoad {
* public AntilazyLoad() {
*
* }
* }
*
* 插桩后:
*
* public class AntilazyLoad {
* public AntilazyLoad() {
* long var1 = System.currentTimeMillis();
* long var3 = System.currentTimeMillis();
* System.out.println("execute:" + (var3 - var1));
* }
* }
*/
public class TestZiJieMaChaZhuang {
@org.junit.Test
public void test() {
try {
FileInputStream fis = new FileInputStream("/Users/renzhenming/Desktop/AntilazyLoad.class");
ClassReader reader = new ClassReader(fis);
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
ClassVisitor visitor = new ClassVisitor(Opcodes.ASM7, writer) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
return new MyMethodVisitor(Opcodes.ASM7, methodVisitor, access, name, descriptor);
}
};
reader.accept(visitor, 0);
byte[] bytes = writer.toByteArray();
FileOutputStream fos = new FileOutputStream("/Users/renzhenming/Desktop/AntilazyLoad2.class");
fos.write(bytes);
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
static class MyMethodVisitor extends AdviceAdapter {
private int start;
/**
* Constructs a new {@link AdviceAdapter}.
*
* @param api the ASM API version implemented by this visitor. Must be one of {@link
* Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}.
* @param methodVisitor the method visitor to which this adapter delegates calls.
* @param access the method's access flags (see {@link Opcodes}).
* @param name the method's name.
* @param descriptor the method's descriptor (see {@link Type Type}).
*/
protected MyMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor);
}
@Override
protected void onMethodEnter() {
super.onMethodEnter();
/**********插入 long l = System.currentTimeMillis() **************/
invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));
start = newLocal(Type.LONG_TYPE);
storeLocal(start);
}
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
/**********插入 long e = System.currentTimeMillis() **************/
invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));
int end = newLocal(Type.LONG_TYPE);
storeLocal(end);
/**********插入System.out.println("execute:"+( e - l))**************/
//获取System中的静态out,out的类型是PrintStream
getStatic(Type.getType("Ljava/lang/System;"), "out", Type.getType("Ljava/io/PrintStream;"));
//创建一个StringBuilder
newInstance(Type.getType("Ljava/lang/StringBuilder;"));
dup();
//执行StringBuilder的构造方法
invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"), new Method("<init>", "()V"));
//把字符串execute:压栈
visitLdcInsn("execute:");
//执行StringBuilder的append方法
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"), new Method("append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;"));
//加载变量end
loadLocal(end);
//加载变量start
loadLocal(start);
//二者相减
math(SUB, Type.LONG_TYPE);
//把相减的结果append
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"), new Method("append", "(J)Ljava/lang/StringBuilder;"));
//执行StringBuilder的toString方法
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"), new Method("toString", "()Ljava/lang/String;"));
//执行PrintStream的println方法
invokeVirtual(Type.getType("Ljava/io/PrintStream;"), new Method("println", "(Ljava/lang/String;)V"));
}
}
}
网友评论