博客原地址:http://blog.csdn.net/allan_bst/article/details/72904721html
1、什麼是熱修復java
熱修復說白了就是」打補丁」,好比大家公司上線一個app,用戶反應有重大bug,須要緊急修復。若是按照通
常作法,那就是程序猿加班搞定bug,而後測試,從新打包併發布。這樣帶來的問題就是成本高,效率低。因而,熱
修復就應運而生.通常經過事先設定的接口從網上下載無Bug的代碼來替換有Bug的代碼。這樣就省事多了,用
戶體驗也好。(以下圖所示:Android 插件化技術的三個技術點以及它們的應用場景)android
2、熱修復原理shell
在 Java 中,要加載一個類須要用到 ClassLoader 。數組
Android 中有三個 ClassLoader, 分別爲 URLClassLoader 、 PathClassLoader 、 DexClassLoader 。其中安全
1.dexPath,指目標類所在的APK或jar文件的路徑.類裝載器將從該路徑中尋找指定的目標類, 該類必須是APK或jar的全路徑.若是要包含多個路徑,路徑之間必須使用特定的分割符分隔, 特定的分割符可使用System.getProperty(「path.separtor」)得到. 2.dexOutputDir,因爲dex文件被包含在APK或者Jar文件中,所以在裝載目標類以前須要先從APK或Jar文件 中解壓出dex文件,該參數就是制定解壓出的dex 文件存放的路徑.在Android系統中, 一個應用程序通常對應一個Linux用戶id,應用程序僅對屬於本身的數據目錄路徑有寫的權限, 所以,該參數可使用該程序的數據路徑. 3.libPath,指目標類中所使用的C/C++庫存放的路徑 4.classload,是指該裝載器的父裝載器,通常爲當前執行類的裝載器
從 framework源碼 中的 dalvik.system 包下,找到 DexClassLoader 源碼,實際內容是在它的父類 BaseDexClassLoader 中,順帶一提,這個類最低在API14開始有用。包含了兩個變量:服務器
private final String originalPath; private final DexPathList pathList; //pathList就是多dex的結構列表,查看 其源碼: /** class definition context */ private final ClassLoader definingContext; /** list of dex/resource (class path) elements */ private final Element[] dexElements; /** list of native library directory elements */ private final File[] nativeLibraryDirectories;
dexElements 就是一個dex列表,那麼咱們就能夠把每一個 Element 當成是一個 dex。
看下PathClassLoader代碼網絡
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}
DexClassLoader代碼併發
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
兩個ClassLoader就兩三行代碼,只是調用了父類的構造函數.app
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
在BaseDexClassLoader 構造函數中建立一個DexPathList類的實例,這個DexPathList的構造函數會建立一個dexElements 數組
public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {
...
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
//建立一個數組
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);
...
}
而後BaseDexClassLoader 重寫了findClass方法,調用了pathList.findClass,跳到DexPathList類中.
/* package */final class DexPathList {
...
public Class findClass(String name, List<Throwable> suppressed) {
//遍歷該數組
for (Element element : dexElements) {
//初始化DexFile
DexFile dex = element.dexFile;
if (dex != null) {
//調用DexFile類的loadClassBinaryName方法返回Class實例
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
return null;
}
...
}
會遍歷這個數組,而後初始化DexFile,若是DexFile不爲空那麼調用DexFile類的loadClassBinaryName方法返回Class實例.
此時咱們整理一下思路,DexClassLoader 包含有一個dex數組 Element[] dexElements ,其中每一個dex文件是一個Element,當須要加載類的時候會遍歷 dexElements,若是找到類則加載,若是找不到從下一個 dex 文件繼續查找。
那麼咱們的實現就是把這個插件 dex 插入到 Elements 的最前面,這麼作的好處是不只能夠動態的加載一個類,而且因爲 DexClassLoader 會優先加載靠前的類,因此咱們同時實現了宿主 apk 的熱修復功能。
3、熱修復之HotFix2.0體驗
先來看一下他的通俗易懂的原理圖
接下來講一下如何實現hotfix,首先打開阿里百川網址http://baichuan.taobao.com,下載最新官方的SDK,其中包括打包工具,調試工具
1.首先在阿里百川的個人應用中添加一個我的應用,記下來應用的APP ID,APPKey,和RSA密鑰
2.建立好應用後,打開AndroidStudio新建一個Project,而後集成hotfix
gradle遠程倉庫依賴, 打開項目找到app的build.gradle文件,添加以下配置:
添加maven倉庫地址:
repositories { maven { url "http://repo.baichuan-android.taobao.com/content/groups/BaichuanRepositories" } }
添加gradle座標版本依賴: dependencies { compile 'com.taobao.android:alisdk-hotfix:2.0.9' }
compile ('com.taobao.android:alisdk-hotfix:2.0.9') { exclude(module:'utdid4all') }
<! -- 網絡權限 --> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <! -- 外部存儲讀權限 --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
配置AndroidManifest文件,這時用到咱們前面所說的那三個id,key,密鑰了
在AndroidManifest.xml
中間的application
節點下添加以下配置,吧value的數值改爲那三個便可:
<meta-data
android:name="com.taobao.android.hotfix.IDSECRET"
android:value="App ID" />
<meta-data
android:name="com.taobao.android.hotfix.APPSECRET"
android:value="App Secret" />
<meta-data
android:name="com.taobao.android.hotfix.RSASECRET"
android:value="RSA密鑰" />
3.接下來進行初始化工作,官方文檔建議在App全局的Application的onCreate()方法進行初始化SophixManager.getInstance().setContext(this)
.setAppVersion(appVersion)
.setAesKey(null)
.setEnableDebug(true)
.setPatchLoadStatusStub(new PatchLoadStatusListener() {
@Override
public void onLoad(final int mode, final int code, final String info, final int handlePatchVersion) {
// 補丁加載回調通知
if (code == PatchStatus.CODE_LOAD_SUCCESS) {
// 代表補丁加載成功
} else if (code == PatchStatus.CODE_LOAD_RELAUNCH) {
// 代表新補丁生效須要重啓. 開發者可提示用戶或者強制重啓;
// 建議: 用戶能夠監聽進入後臺事件, 而後應用自殺
} else if (code == PatchStatus.CODE_LOAD_FAIL) {
// 內部引擎異常, 推薦此時清空本地補丁, 防止失敗補丁重複加載
// SophixManager.getInstance().cleanPatches();
} else {
// 其它錯誤信息, 查看PatchStatus類說明
}
}
}).initialize();
SophixManager.getInstance().queryAndLoadNewPatch();
1.3.2 接口說明
1.3.2.1 initialize方法
initialize(): <必選>
該方法主要作些必要的初始化工做以及若是本地有補丁的話會加載補丁, 但不會自動請求補丁。所以須要自行調用queryAndLoadNewPatch方法拉取補丁。這個方法調用須要儘量的早, 推薦在Application的onCreate方法中調用, initialize()方法調用以前你須要先調用以下幾個方法, 方法調用說明以下:
setContext(this): <必選> Application上下文context
setAppVersion(appVersion): <必選> 應用的版本號
setAesKey(aesKey): <可選> 用戶自定義aes祕鑰, 會對補丁包採用對稱加密。這個參數值必須是16位數字或字母的組合,是和補丁工具設置裏面AES Key保持徹底一致, 補丁才能正確被解密進而加載。此時平臺無感知這個祕鑰, 因此不用擔憂百川平臺會利用大家的補丁作一些非法的事情。
setEnableDebug(true/false): <可選> 默認爲false, 是否調試模式, 調試模式下會輸出日誌以及不進行補丁簽名校驗. 線下調試此參數能夠設置爲true, 查看日誌過濾TAG:Sophix, 同時強制不對補丁進行簽名校驗, 全部就算補丁未簽名或者簽名失敗也發現能夠加載成功. 可是正式發佈該參數必須爲false, false會對補丁作簽名校驗, 不然就可能存在安全漏洞風險
setPatchLoadStatusStub(new PatchLoadStatusListener()): <可選> 設置patch加載狀態監聽器, 該方法參數須要實現PatchLoadStatusListener接口, 接口說明見1.3.2.2說明
setUnsupportedModel(modelName, sdkVersionInt):<可選> 把不支持的設備加入黑名單,加入後不會進行熱修復。modelName爲該機型上Build.MODEL的值,這個值也能夠經過adb shell getprop | grep ro.product.model取得。sdkVersionInt就是該機型的Android版本,也就是Build.VERSION.SDK_INT,若設爲0,則對應該機型全部安卓版本。
1.3.2.2 queryAndLoadNewPatch方法
該方法主要用於查詢服務器是否有新的可用補丁. SDK內部限制連續兩次queryAndLoadNewPatch()方法調用不能短於3s, 不然的話就會報code:19的錯誤碼. 若是查詢到可用的話, 首先下載補丁到本地, 而後
應用本來沒有補丁, 那麼若是當前應用的補丁是熱補丁, 那麼會馬上加載(不論是冷補丁仍是熱補丁). 若是當前應用的補丁是冷補丁, 那麼須要重啓生效.
應用已經存在一個補丁, 首先會把以前的補丁文件刪除, 而後不馬上加載, 而是等待下次應用重啓再加載該補丁
補丁在後臺發佈以後, 並不會主動下行推送到客戶端, 須要手動調用queryAndLoadNewPatch方法查詢後臺補丁是否可用.
只會下載補丁版本號比當前應用存在的補丁版本號高的補丁, 好比當前應用已經下載了補丁版本號爲5的補丁, 那麼只有後臺發佈的補丁版本號>5纔會從新下載.
同時1.4.0以上版本服務後臺上線了「一鍵清除」補丁的功能, 因此若是後臺點擊了「一鍵清除」那麼這個方法將會返回code:18的狀態碼. 此時本地補丁將會被強制清除, 同時不清除本地補丁版本號
1.3.2.3 cleanPatches()方法
清空本地補丁
1.3.2.4 PatchLoadStatusListener接口
該接口須要自行實現並傳入initialize方法中, 補丁加載狀態會回調給該接口, 參數說明以下:
mode: 補丁模式, 0:正常請求模式 1:掃碼模式 2:本地補丁模式
code: 補丁加載狀態碼, 詳情查看PatchStatusCode類說明
info: 補丁加載詳細說明, 詳情查看PatchStatusCode類說明
handlePatchVersion: 當前處理的補丁版本號, 0:無 -1:本地補丁 其它:後臺補丁
這裏列舉幾個常見的code碼說明, 詳情查看SDK中PatchStatus類的代碼,其中有具體說明
code: 1 補丁加載成功
code: 6 服務端沒有最新可用的補丁
code: 11 RSASECRET錯誤,官網中的密鑰是否正確請檢查
code: 12 當前應用已經存在一箇舊補丁, 應用重啓嘗試加載新補丁
code: 13 補丁加載失敗, 致使的緣由不少種, 好比UnsatisfiedLinkError等異常, 此時應該嚴格檢查logcat異常日誌
code: 16 APPSECRET錯誤,官網中的密鑰是否正確請檢查
code: 18 一鍵清除補丁
code: 19 連續兩次queryAndLoadNewPatch()方法調用不能短於3s
SophixPatchTool
, 如還未下載打包工具,請前往文檔SDK下載&版本更新記錄下載Android打包工具。/**
* 監聽返回鍵
*/
// @Override
// public void onBackPressed() {
// if(drawerLayout.isDrawerOpen(nv)){
// drawerLayout.closeDrawers();
// return;
// }
// if (System.currentTimeMillis() - newTime > 2000) {
// newTime = System.currentTimeMillis();
// Snackbar snackbar = Snackbar.make(pager, "再按一次返回鍵退出程序", Snackbar.LENGTH_SHORT);
// snackbar.getView().setBackgroundColor(getResources().getColor(R.color.colorPrimary));
// snackbar.show();
// } else {
// finish();
// }
// }
SophixPatchTool
將這兩個app對比,生成一個補丁,在不推送升級app的狀況下修復bapp的不能退出程序的bug】SophixPatchTool
,按照提示選擇bapp和app,點擊Go,生成一個baichuan-hotfix-patch.jar,而後將這個jar文件