簡(jiǎn)介
Nuwa熱補(bǔ)丁是基于下文中手Q熱補(bǔ)丁輕量級(jí)方案的具體實(shí)現(xiàn),大致是通過(guò)“插樁”方式提前加載需要“打補(bǔ)丁”的類,以避免bug 類被加載。想要了解具體原理的可以參考本文參考。這里有兩部分,Nuwa實(shí)現(xiàn)了補(bǔ)丁類的加載動(dòng)作,NuwaGradle 則實(shí)現(xiàn)了補(bǔ)丁類的生成。這里我們重點(diǎn)關(guān)注如何生成補(bǔ)丁。
預(yù)備知識(shí)
在制作補(bǔ)丁的時(shí)候,首先要對(duì)一個(gè)apk的生成過(guò)程有一個(gè)大致的了解。了解了如何生成apk,才能知道我們應(yīng)該在哪一步去獲取制作補(bǔ)丁的“原材料”。
大致步驟如下:
- 使用aapt生成R.java類文件
- 使用android SDK提供的aidl.exe把.aidl轉(zhuǎn)成.java文件
- javac編譯.java類文件生成class文件
- 使用android SDK提供的dx.bat命令行腳本生成classes.dex文件
- 使用Android SDK提供的aapt.exe生成資源包文件
- apkbuilder 生成未簽名的apk安裝文件
- 使用jdk的jarsigner對(duì)未簽名的包進(jìn)行apk簽名
- zipAlign 對(duì)齊
思路
我們制作補(bǔ)丁時(shí),必須防止類被打上ISPREVERIFIED這個(gè)標(biāo)記。
原理一個(gè)類直接引用到的類不在同一個(gè)dex中即可。這樣,就能防止類被打上ISPREVERIFIED標(biāo)記并能進(jìn)行熱更新。
簡(jiǎn)單來(lái)說(shuō),就是將所有類的構(gòu)造函數(shù)中,引用另一個(gè)hack.dex中的類,這個(gè)類叫Hack.class,然后在加載補(bǔ)丁patch.dex前動(dòng)態(tài)加載這個(gè)hack.dex,但是有一個(gè)類的構(gòu)造函數(shù)中不能引用Hack.class,這個(gè)類就是Application類的子類,一旦這個(gè)類的構(gòu)造函數(shù)中加入Hack.class這個(gè)類,那么程序運(yùn)行時(shí)就會(huì)找不到Hack.class這個(gè)類,因?yàn)檫€沒(méi)有被加載。
生成補(bǔ)丁有幾個(gè)需要注意的地方:
- 尋找插入task的點(diǎn)
在gradle1.5以下的版本中,我們可以找到dex task ,而gradle1.5以上,dex task 無(wú)法找到。
這里便用 transformClassesWithDexForXXX 這個(gè)task。在其之前執(zhí)行生成改造類的工作。
沒(méi)有開(kāi)啟Multidex的情況,存在一個(gè)preDex的Task。preDex會(huì)在dex任務(wù)之前把所有的庫(kù)工程和第三方j(luò)ar包提前打成dex,下次運(yùn)行只需重新dex被修改的庫(kù),以此節(jié)省時(shí)間。dex任務(wù)會(huì)把preDex生成的dex文件和主工程中的class文件一起生成class.dex,這樣就需要針對(duì)有無(wú)preDex,做不同的修改字節(jié)碼策略即可。 - 改造字節(jié)類,在類的構(gòu)造函數(shù)中插入另一個(gè)dex中的類
對(duì)于java的.class的改造,我們一般會(huì)用到asm或者javasist 這兩類工具,而NuwaGradle則采用了asm,和同事溝通,發(fā)現(xiàn)使用javasist時(shí),有些類的無(wú)參構(gòu)造函數(shù)可能無(wú)法找到。這里就貼下asm的具體實(shí)現(xiàn):
class NuwaProcessor {
/**
* 處理jar
* @param hashFile
* @param jarFile
* @param patchDir
* @param map
* @param includePackage
* @param excludeClass
* @return
*/
public static processJar(File hashFile, File jarFile, File patchDir, Map map, HashSet includePackage, HashSet excludeClass) {
if (jarFile) {
/**
* classes.jar dex后的文件
*/
def optJar = new File(jarFile.getParent(), jarFile.name + ".opt")
def file = new JarFile(jarFile);
Enumeration enumeration = file.entries();
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(optJar));
/**
* 枚舉jar文件中的所有文件
*/
while (enumeration.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) enumeration.nextElement();
String entryName = jarEntry.getName();
ZipEntry zipEntry = new ZipEntry(entryName);
InputStream inputStream = file.getInputStream(jarEntry);
jarOutputStream.putNextEntry(zipEntry);
/**
* 以class結(jié)尾的文件并且在include中不在exclude中,并且不是cn/jiajixin/nuwa/包中的文件
*/
if (shouldProcessClassInJar(entryName, includePackage, excludeClass)) {
/**
* 構(gòu)造函數(shù)中注入字節(jié)碼
*/
def bytes = referHackWhenInit(inputStream);
/**
* 寫(xiě)入子杰
*/
jarOutputStream.write(bytes);
/**
* hash校驗(yàn)
*/
def hash = DigestUtils.shaHex(bytes)
/**
* 加入hash值
*/
hashFile.append(NuwaMapUtils.format(entryName, hash))
/**
* hash值與上一release版本不一樣則拷到對(duì)應(yīng)的目錄,作為patch的類
*/
if (NuwaMapUtils.notSame(map, entryName, hash)) {
NuwaFileUtils.copyBytesToFile(bytes, NuwaFileUtils.touchFile(patchDir, entryName))
}
} else {
/**
* 否則直接輸出文件不處理
*/
jarOutputStream.write(IOUtils.toByteArray(inputStream));
}
jarOutputStream.closeEntry();
}
jarOutputStream.close();
file.close();
/**
* 刪除jar文件
*/
if (jarFile.exists()) {
jarFile.delete()
}
/**
* dex后的文件重命名為jar文件
*/
optJar.renameTo(jarFile)
}
}
//refer hack class when object init
private static byte[] referHackWhenInit(InputStream inputStream) {
ClassReader cr = new ClassReader(inputStream);
ClassWriter cw = new ClassWriter(cr, 0);
ClassVisitor cv = new ClassVisitor(Opcodes.ASM4, cw) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
mv = new MethodVisitor(Opcodes.ASM4, mv) {
@Override
void visitInsn(int opcode) {
/**
* 如果是構(gòu)造函數(shù)
*/
if ("".equals(name) & opcode == Opcodes.RETURN) {
/**
* 注入代碼
*/
super.visitLdcInsn(Type.getType("Lcn/jiajixin/nuwa/Hack;"));
}
super.visitInsn(opcode);
}
}
return mv;
}
};
cr.accept(cv, 0);
return cw.toByteArray();
}
/**
* 是否需要在preDex前處理
* @param path
* @return
*/
public static boolean shouldProcessPreDexJar(String path) {
return path.endsWith("classes.jar") & !path.contains("com.android.support") && !path.contains("/android/m2repository");
}
/**
* jar中的文件是否需要處理
* @param entryName
* @param includePackage
* @param excludeClass
* @return
*/
private static boolean shouldProcessClassInJar(String entryName, HashSet includePackage, HashSet excludeClass) {
return entryName.endsWith(".class") & !entryName.startsWith("cn/jiajixin/nuwa/") && NuwaSetUtils.isIncluded(entryName, includePackage) && !NuwaSetUtils.isExcluded(entryName, excludeClass) && !entryName.contains("android/support/")
}
/**
* 處理class
* @param file
* @return
*/
public static byte[] processClass(File file) {
def optClass = new File(file.getParent(), file.name + ".opt")
FileInputStream inputStream = new FileInputStream(file);
FileOutputStream outputStream = new FileOutputStream(optClass)
/**
* 對(duì)class注入字節(jié)碼
*/
def bytes = referHackWhenInit(inputStream);
outputStream.write(bytes)
inputStream.close()
outputStream.close()
if (file.exists()) {
file.delete()
}
optClass.renameTo(file)
return bytes
}
}
- 對(duì)于混淆時(shí),應(yīng)有mapping文件
我們一般發(fā)布的apk,都是混淆過(guò)的,所以可能需要對(duì)某些混淆的類來(lái)“打補(bǔ)丁”,這里就需要在執(zhí)行progurad這個(gè)task的時(shí)候指定mapping文件,具體如下:
def proguardTask = project.tasks.findByName("proguard${variant.name.capitalize()}")
if (oldNuwaDir) {
def mappingFile = NuwaFileUtils.getVariantFile(oldNuwaDir, variant, MAPPING_TXT)
NuwaAndroidUtils.applymapping(proguardTask, mappingFile)
}
NuwaAndroidUtils中的具體實(shí)現(xiàn):
//使用mapping文件做proguard
public static applymapping(DefaultTask proguardTask, File mappingFile) {
if (proguardTask) {
if (mappingFile.exists()) {
proguardTask.applymapping(mappingFile)
} else {
println "$mappingFile does not exist"
}
}
}
- 生成dex
/**
* 對(duì)jar進(jìn)行dex操作
* @param project
* @param classDir
* @return
*/
public static dex(Project project, File classDir) {
if (classDir.listFiles().size()) {
def sdkDir
/**
* 獲得sdk目錄
*/
Properties properties = new Properties()
File localProps = project.rootProject.file("local.properties")
if (localProps.exists()) {
properties.load(localProps.newDataInputStream())
sdkDir = properties.getProperty("sdk.dir")
} else {
sdkDir = System.getenv("ANDROID_HOME")
}
if (sdkDir) {
/**
* 如果是windows系統(tǒng),加入后綴.bat
*/
def cmdExt = Os.isFamily(Os.FAMILY_WINDOWS) ? '.bat' : ''
def stdout = new ByteArrayOutputStream()
/**
* 拼接命令
* dx --dex --output=patch.jar classDir
* classDir是注入字節(jié)碼后的補(bǔ)丁目錄
*/
project.exec {
commandLine "${sdkDir}/build-tools/${project.android.buildToolsVersion}/dx${cmdExt}",
'--dex',
"--output=${new File(classDir.getParent(), PATCH_NAME).absolutePath}",
"${classDir.absolutePath}"
standardOutput = stdout
}
def error = stdout.toString().trim()
if (error) {
println "dex error:" + error
}
} else {
throw new InvalidUserDataException('$ANDROID_HOME is not defined')
}
}
}
實(shí)現(xiàn)
具體實(shí)現(xiàn)可以看Android 熱修復(fù)Nuwa的原理及Gradle插件源碼解析,寫(xiě)的很不錯(cuò),我這里就不再做無(wú)用功了。