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();
}
}