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