
一、什么是熱修復
在應用上線后出現Bug需要及時修復時,不需要再發(fā)新的安裝包,只要發(fā)布補丁包,在用戶無感知的情況下修復Bug。
二、怎么進行熱修復
從以下幾個角度入手來描述熱修復需要解決的問題:服務端、用戶端、開發(fā)端。
1、服務端:補丁包管理:
2、用戶端:進行熱修復:什么時候執(zhí)行熱修復?怎么執(zhí)行熱修復(使用補丁包)?Android版本兼容問題;
3、開發(fā)端:生成補丁包:補丁包是什么?如何生成補丁包?開啟混淆后怎么處理?對比改動自動生成補丁包(Gradle)?
三、常用熱修復框架:
熱修復方案很多,其中比較出名的有騰訊的Tinker、阿里的AndFix、美團的Robust以及QZone的超級補丁方案:

下面就從 類替換 及 即時生效 兩個角度來對以上幾種方案做個簡單說明:
1、Tinker和QZone都采用了類Class的替換:根據類加載機制,所以就不能實現即時生效,而這兩者的區(qū)別在于補丁包上,Tinker采用了差分對dex包做了diff,而Qzone則是全量的類替換;
2、AndFix和Robust可以理解為是對方法Method的替換:AndFix是在Native動態(tài)替換java層的方法,通過nativce層hook JAVA層代碼,通過注解來找到替換的Method;而Robust是對每個函數在編譯打包階段自動的插入一段代碼,類似于代理,將方法執(zhí)行的代碼重定向到其他方法中;
四、熱修復所用到的知識點
類加載:BootClassLoader、PathClassLoader、DexClassLoader、雙親委派機制以及類查找流程(注:此知識點會有專門章節(jié)解析),基于此可以簡單總結一下熱修復的流程:
——獲取當前應用的PathClassLoader
——反射獲取到DexPathList屬性對象pathList
——發(fā)射修改pathList的dexElements:獲取path.dex補丁包的dexElements、獲取pathList的dexElement、將兩個dexElements按照補丁包dexElement在前原有dexElemtns災后的順序合并后反射復制給pathList的dexElement;
五、熱修復兼容問題:
1、Elements數組獲取問題:補丁包path.dex最后要獲取其dexElements,針對不同版本獲取接口makePathElement方法名稱或者參數獲取會有不同,需要兼容;
2、AndroidN混合編譯問題:在使用混合模式運行時,應用在安裝時不做編譯,而是運行時解釋字節(jié)碼,同時在JIT編譯了一些代碼將這些代碼信息記錄至Profile文件,等到設備空閑的時候使用AOT編譯生成app_image的base.art(類對象映射)文件,這個art文件會在apk啟動是自動加載(相當于緩存),根據類加載原理,類被加載了無法被替代,即無法修復;
——解決方案:運行時替換PathClassLoader:app_image中的的class是插入到PathClassLoader的ClassTable中的,假設我們完全廢棄掉PathClassLoader,而采用一個新建的ClassLoader來加載后續(xù)的類,即可達到cache無用化的作用:Thread.currentThread().setContextClassLoader(classLoade r);
3、CLASS_ISPREVERIFIED標志:在Dalvik虛擬機中,如果一個類所調用的Class全部在一個Dex文件中,則就會被打上CLASS_ISPREVERIFIED標志;比如MainActivity類中只引用了Utils類,當打包時,MainActivity和Utils都在classes.dex中,則加載時MainActivity類就會被標記為CLASS_ISPREVERIFIFD;此時在做熱修復的時候,如果用補丁包的dex中的Utils類去替換有Bug的Utils類,則就會導致MainActivity引用的Utils類不在同一個dex中,所以會和之前的標志產生沖突,出現校驗失?。?br>
——解決方案:利用字節(jié)碼插樁技術:可在每個類中添加一個特殊類的調用,這個類單獨編譯生成一個dex文件,此時在加載的時候就不會被打上CLASS_ISPREVERIFIED標志,進而就能校驗通過,進行替換修復;
(注:字節(jié)碼插樁和ASM會有專門章節(jié)解析)
六、Gradle插件相關:
1、插件開發(fā)的幾種方式:
——Build Script腳本: 把插件寫在buidler.gradle文件中,一般用于簡單的邏輯,并且只對改builder.gradle可見;
——buildSrc目錄:將插件源文件放在buildSrc/src/main/groovy中,僅對此項目可見;
——獨立項目:一個獨立的JAVA項目或者模塊,可將文件包發(fā)布到倉庫(jcenter),方便其他項目引入;
2、插件實現:
public class TestPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
...
}
}
繼承 implements Plugin<Project>,實現 apply(Project project)方法;
3、插件擴展:
public class TestPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.getExtensions().create("patch", PatchExtension.class);
project.afterEvaluate(new Action<Project>() {
@Override
public void execute(final Project project) {
...
});
}
});
}
}
通過create方法創(chuàng)建擴展,patch對應擴展名,即在builder.gradle中
對應patch,其中debugOn、applicationName則對應PatchExtension類中定義;
apply plugin: 'com.example.patch'
patch {
debugOn true
applicationName 'com.example.hotfix.MyApplication'
}
public class PatchExtension {
boolean debugOn;
String applicationName;
}
注一:插件的引入:
apply plugin: 'com.example.patch'
apply plugin: com.example.patch.plugin.TestPlugin
以上兩種引入插件的方式都可以,唯一區(qū)別為一個有引號,一個沒有,有引號引入的相當于插件的一個別名,如果要采用用引號引入的方式,則必須在buildSrc\build\resources\main\META-INF\gradle-plugins/com.example.patch.properties這樣一個文件,別名對應于此文件的文件名,在編譯時會自動填充此文件的內容:
implementation-class=com.example.patch.plugin.PatchPlugin
注二:afterEvaluate的使用:即builder.gradle也是一行一行執(zhí)行的,所以一般是在腳本掃描完后的回調函數afterEvaluate中才能正確的獲取patch中的配置值;
七、利用Gradle插件自動生成補丁包
1、Task的理解:即任務,Android工程引入的插件中,創(chuàng)建多個自定義任務,比如:
—編譯JAVA任務:compileDebugJavaWithJavac
—混淆任務:transformClassesAndResourcesWithProguardFor
—打包dex任務:transformClassesWithDexBuilderFor
2、自動生成補丁包:
思路:在編譯生成class文件時,可以將生成的class文件的名字和對應的md5值保存,當第二次再次編譯的時候可以對比生成class的文件的md5值是否有變化,如有變化,則將次class文件打入到補丁包中;
實現:基于上述Task的理解,可以獲取transformClassesWithDexBuilderFor任務的輸入作為切入點,進行class文件及對應md5值的生成及校驗;
3、防止混淆開啟引起補丁包異常:因為開啟混淆后,可能每次編譯后對同一個類混淆的名字有改變,如果有改變,會影響補丁包生成的可靠性:
——解決方案:可以利用開啟混淆后生成的mapping文件,此文件記載了混淆前后類及方法的變化,同時可以在proguard-rules.pro中配置-applymapping選項;即在每次編譯混淆后都會參照前一次混淆的名字,這樣就保證了生成補丁包校驗時的可靠性;
總結一下:
以上是基于熱修復的學習的基本內容,只是簡單列舉了一下流程、知識點及注意點,針對每個知識點的詳解后續(xù)會有文章做支持,暫時先列舉出來:
——類加載相關...
——Gradle相關...
——熱修復的Demo...