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

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以下的版本上会抛出异常


使用补丁包中的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();
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章