寫這篇文章的目的呢,也是理一下本身的思路吧,同時把最近看到的一些熱修復知識獻給讀者們。不知道同窗們最近是否是聽到了不少關於熱修復的事情,各大廠商,各界大佬們都有屬於本身的熱修復框架,最近阿里不也推出了個爆炸消息,堪稱最牛逼的修復框架Sophix,同時還推出了對應的一本pdf(叫什麼深刻理解Android熱修復技術原理),不知道多少同窗看過,深刻看應該是能夠看到個原理,可是我感受看了我也寫不出這樣的代碼,畢竟大廠大佬。這篇文章呢,就簡單的教你們如何寫一個屬於本身公司或者本身的熱修復框架java
1.這篇文章的重點在於.class文件的打樁,可能會偏重於groovy語言,與java相通沒事,相信我你絕對能看懂。android
2.若是沒有看過我以前的那篇文章可能會有些懵哦,以前的那篇文章講的是原理,經過DexClassLoader如何熱修復。git
3.由於熱修復關鍵點仍是在於打樁生成差別文件的dex,而不是在於把這個dex文件插入到已安裝的app中(兩個相輔相成(打樁和插入dex)),由於google的multidex裏面已經把這個操做作的很好了,咱們只須要修修改改就能夠完成這個插入操做,還有就是以前那篇文章還留了一個坑。github
4.若是沒有接觸過熱修復的同窗可能會對下面的一些詞彙比較悶(打樁
修改字節碼文件(.class)打樁目的爲了解決CLASS_ISPREVERIFIED預約義,不明白的可參考上一篇文章)api
5.附文章傳送門及這篇文章的項目源碼app
AutoFix歡迎star,fork,issueide
由於google的gradle升級了嘛,主要是他引入了transform API(官網解釋The API existed in 1.4.0-beta2 but it's been completely revamped in 1.5.0-beta1),致使咱們的plugin找不到以前咱們寫好的task任務名。post
下面我給你們講一下經過plugin進行打樁操做,接下來我會給你們看一下nuwa
熱修復項目中的部分代碼。gradle
def preDexTask = project.tasks.findByName("preDex${variant.name.capitalize()}")
def dexTask = project.tasks.findByName("dex${variant.name.capitalize()}")
def proguardTask = project.tasks.findByName("proguard${variant.name.capitalize()}")複製代碼
這是nuwa熱修復的源碼他事先定義好了這些任務,這些任務就是把字節碼文件打包成dex文件的任務,上面的代碼意思就是獲取這些任務的名字。(就是apply plugin: 'com.android.application'裏面的任務)。從上面的代碼能夠看到,咱們定義的任務名稱分別是(preDex${variant.name.capitalize()})(dex${variant.name.capitalize()})(proguard${variant.name.capitalize()})($這個符號就是拼接字符串的意思和kotlin同樣,variant.name.capitalize()這個就是獲取的字符串是debug 仍是release)。而後上面咱們也說了,gradle1.4以後google更名字了,咱們固然找不到這些任務名了,固然報錯了哦。如今呢咱們只須要作一些簡單的if判斷操做不就能夠了嗎?根據不一樣的gradle版本號修改一下名字不就得了,下面貼出RocooFix的代碼塊如何解決的
static String getProGuardTaskName(Project project, BaseVariant variant) {
if (isUseTransformAPI(project)) {
return "transformClassesAndResourcesWithProguardFor${variant.name.capitalize()}"
} else {
return "proguard${variant.name.capitalize()}"
}
}
static String getPreDexTaskName(Project project, BaseVariant variant) {
if (isUseTransformAPI(project)) {
return ""
} else {
return "preDex${variant.name.capitalize()}"
}
}
static String getDexTaskName(Project project, BaseVariant variant) {
if (isUseTransformAPI(project)) {
return "transformClassesWithDexFor${variant.name.capitalize()}"
} else {
return "dex${variant.name.capitalize()}"
}
}複製代碼
看到了嗎?就是判斷一下當前的項目gradle版本號,而後修改一下名稱返回給你。簡單吧。幾行代碼解決了兼容性問題。
以前那篇文章也說了,apk編譯的生命週期。因此這邊顧名思義固然是在被打成dex文件以前對class文件的時候操做啊。
接下來問題來了,如何在被打成dex文件前操做呢,剛剛上面說的獲取那些task名稱還記得嗎?這就是關鍵。由於在groovy中有這麼一個語法,任務之間能夠經過dependsOn來添加依賴。
那麼好如今舉個例子。
task A{}
task B{}
A dependsOn B複製代碼
很明顯嗎?就是執行A前必須B執行完了才行。知道了這個嗎?咱們下面繼續看項目源碼如何設置在咱們打樁完成以後再執行dex操做
def autoJarBeforeDexTask = project.tasks[autoJarBeforeDex]
autoJarBeforeDexTask.dependsOn dexTask.taskDependencies.getDependencies(dexTask)
autoJarBeforeDexTask.doFirst(prepareClosure)
autoPatchTask.dependsOn autoJarBeforeDexTask
dexTask.dependsOn autoPatchTask複製代碼
一下來這麼多代碼 並且這些代碼還不認識可能會有點蒙哦,不要着急,一行一句的講解給你聽他們的依賴關係(簡單講一下上面代碼的意思
autoJarBeforeDexTask這個任務就是進行打樁的任務,dexTask這個任務就是打包成dex的任務,prepareClosure初始化操做的,autoPatchTask打包成補丁文件的任務
)
第一行呢 獲取項目中的task 名字是autoJarBeforeDex
第二行呢 先說一下這句話的意思(dexTask.taskDependencies.getDependencies(dexTask) 這句話就是拿到dexTask的依賴任務),而後咱們的autoJarBeforeDexTask這個任務對dexTask的依賴任務 進行依賴
第三行呢 autoJarBeforeDexTask 點doFirst(prepareClosure)意思就是說在執行autoJarBeforeDexTask任務前先執行這個prepareClosure
第四行和第五行不用說了吧
最後邏輯如prepareClosure -> autoJarBeforeDexTask -> autoPatchTask -> dexTask 依次執行
這樣操做就解決了在dex文件生成前進行字節碼文件的插入
首先呢,打樁你的先獲取字節碼文件吧。就是這些file。如何獲取這些文件呢,由於咱們編譯項目的時候會在項目中生成一個build目錄,裏面有項目相關的全部文件,咱們能夠經過下面的方式獲取這些文件,咱們打樁只須要獲取jar文件和intermediates/class下面對於的class文件,代碼以下
static SetgetDexTaskInputFiles(Project project, BaseVariant variant, Task dexTask) { if (dexTask == null) { dexTask = project.tasks.findByName(getDexTaskName(project, variant)); } if (isUseTransformAPI(project)) { def extensions = [SdkConstants.EXT_JAR] as String[] Set 複製代碼files = Sets.newHashSet(); dexTask.inputs.files.files.each { if (it.exists()) { if (it.isDirectory()) { Collection jars = FileUtils.listFiles(it, extensions, true); files.addAll(jars) //intermediates/class下面對應的class文件 if (it.absolutePath.toLowerCase().endsWith("intermediates${File.separator}classes${File.separator}${variant.dirName}".toLowerCase())) { files.add(it) } //jar包 } else if (it.name.endsWith(SdkConstants.DOT_JAR)) { files.add(it) } } } return files } else { return dexTask.inputs.files.files; } }
文件這時候咱們已經拿到了。而後咱們要遍歷這些文件依次給他們打樁,同時要過濾掉jar包中不須要打樁的文件否則會耗時
//打樁等一些工做 def autoJarBeforeDex = "autoJarBeforeDex${variant.name.capitalize()}" project.task(autoJarBeforeDex) << { //獲取build/intermediates/下的文件 SetinputFiles = AutoUtils.getDexTaskInputFiles(project, variant, dexTask) inputFiles.each { inputFile -> def path = inputFile.absolutePath if (path.endsWith(SdkConstants.DOT_JAR)) { //對jar包進行打樁 NuwaProcessor.processJar(hashFile,hashMap,inputFile, patchDir, includePackage, excludeClass) } else if (inputFile.isDirectory()) { //intermediates/classes/debug 目錄下面須要打樁的class def extensions = [SdkConstants.EXT_CLASS] as String[] //過濾不須要打樁的文件class def inputClasses = FileUtils.listFiles(inputFile, extensions, true); inputClasses.each { inputClassFile -> def classPath = inputClassFile.absolutePath //過濾R文件和config文件 if (classPath.endsWith(".class") && !classPath.contains("/R\$") && !classPath.endsWith("/R.class") && !classPath.endsWith("/BuildConfig.class")) { //引用nuwa而來的 if (NuwaSetUtils.isIncluded(classPath, includePackage)) { if (!NuwaSetUtils.isExcluded(classPath, excludeClass)) { def bytes = NuwaProcessor.processClass(inputClassFile) if ("\\".equals(File.separator)) { classPath = classPath.split("${dirName}\\\\")[1] } else { classPath = classPath.split("${dirName}/")[1] } def hash = DigestUtils.shaHex(bytes) hashFile.append(AutoUtils.format(classPath, hash)) //根據hash值來判斷當前文件是否爲差別文件須要作成patch嗎? if (AutoUtils.notSame(hashMap,classPath, hash)) { def file = new File("${patchDir}${File.separator}${classPath}") file.getParentFile().mkdirs() if (!file.exists()) { file.createNewFile() } FileUtils.writeByteArrayToFile(file, bytes) } } } } } } } } 複製代碼
好吧代碼有點長,可是每個關鍵點都有相應的註釋,
上面的代碼的意思簡單的說就是 先判斷文件是jar包仍是路徑 若是是jar包,進行jar包的打樁方式,若是是路徑的話 找到class文件判斷這個class是否要打樁(如R文件就不須要)。而後根據文件的hash值來來判斷這個類是否修改過,若是修改過吧吧這些類放在一個文件夾中,最後統一打包成補丁。
打樁代碼有兩處
//對jar包進行打樁
NuwaProcessor.processJar(hashFile,hashMap,inputFile, patchDir, includePackage, excludeClass)複製代碼
//對文件進行打樁
def bytes = NuwaProcessor.processClass(inputClassFile)複製代碼
此次介紹的打樁用的是asm這個庫,具體代碼都在項目中能夠去看看,這裏就不詳細說了。
這個關鍵點在於項目要在gradle中配置一些信息 有興趣的同窗能夠看一下項目裏面有集成過程
AutoFix
auto_fix {
lastVersion = '1'//須要打補丁的狀況下打開此處
}複製代碼
若是細心的同窗會發現咱們的項目中建立了hashFile這個文件。這個文件是用來記錄每一個版本的打樁了的字節碼文件的hash值。
上面的代碼也能夠看出須要配置上一次的版本號,你出現bug了確定要修改你的versionCode 而後把以前的填寫到lastVersion上,他會根據上次的hashFile來和此次生成的hashFile進行對比,若是不相同說明這個類被修改過,而後吧這個文件copy已發到patch目錄下,打樁完成以後咱們,咱們到對於的patch目錄下會找到這些文件而後把它們打包成patch.jar生成相應的補丁文件。有這麼一段代碼
//根據hash值來判斷當前文件是否爲差別文件須要作成patch嗎?
if (AutoUtils.notSame(hashMap,classPath, hash)) {
def file = new File("${patchDir}${File.separator}${classPath}")
file.getParentFile().mkdirs()
if (!file.exists()) {
file.createNewFile()
}
FileUtils.writeByteArrayToFile(file, bytes)
}複製代碼
這就是上面說的意思根據hashMap 和當前的hash值來作判斷。最終生成差別文件。打包成補丁文件(問題又來了,如何生成補丁文件呢。還記得以前說的執行任務的流程嗎?prepareClosure -> autoJarBeforeDexTask -> autoPatchTask -> dexTask 依次執行)autoPatchTask這個任務就是打補丁任務能夠看一下源碼
//製做patch補丁包
def autoPatchTaskName = "applyAuto${variant.name.capitalize()}Patch"
project.task(autoPatchTaskName) << {
if (patchDir) {
AutoUtils.makeDex(project, patchDir)
}
}
def autoPatchTask = project.tasks[autoPatchTaskName]複製代碼
沒錯就是他,仍是上一篇文章講到的打包操做只不過代碼話了 具體代碼能夠去項目中看,這裏就不詳解了。
說了這麼多,發現我咋還不會寫呢,怎麼辦,這篇文章說的都是tmd什麼打樁,對的你沒聽錯,由於熱修復就是插入dex
而後就是打樁
就這兩件事以前的文章講的是插入dex
這篇文章講的是打樁
,若是在不會能夠去看看項目源碼。
有什麼疑問和不解能夠留言哦,但願此次分享帶給你們帶來的不是時間的浪費,而是能力的提高。謝謝
源碼奉上:AutoFix歡迎star,fork,issue