美文网首页
34. 热修复-QQ空间超级补丁方案-CLASS_ISPREVE

34. 热修复-QQ空间超级补丁方案-CLASS_ISPREVE

作者: 任振铭 | 来源:发表于2021-02-10 10:39 被阅读0次

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热补丁动态修复技术介绍

https://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=400118620&idx=1&sn=b4fdd5055731290eef12ad0d17f39d4a

那么问题就来了,假设B类出现了异常,那么我们将这个类修复后,会单独打到一个dex中,那么很明显就和A不在同一个dex了,将这个dex插入到系统dexElements中后,在加载到这个类的时候, 在5.0以下的版本上会抛出异常


屏幕快照 2021-02-10 上午10.28.11.png

使用补丁包中的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"));
        }
    }
}

相关文章

网友评论

      本文标题:34. 热修复-QQ空间超级补丁方案-CLASS_ISPREVE

      本文链接:https://www.haomeiwen.com/subject/ssghxltx.html