對於熱更新的問題就是了解兩個點的問題:
- 如何加載補丁包,也就是如何加載dex 文件的過程(dex是補丁包,更改的文件都在補丁包中)
- 修復後的類如何替換掉舊的類
通過這篇文章給大家介紹下我理解的熱更新的邏輯,需要先了解一些關係到的知識
熱更新方案有三種
- 底層替換方案
- 類加載方案
- Instant Run
本篇文章主要是 類加載 和 Instant Run 兩種方式進行的熱更新
類加載方案
需要先了解Android 類加載,可以看這篇 https://blog.csdn.net/hjiangshujing/article/details/104249956
此處用到的是Android 中的 DexClassLoader 類加載器
以下做簡單的介紹
Android 類加載
- BootClassLoader
- DexClassLoader – optimizedDirect
- PathClassLoader – 沒有 optimizedDirect,默認optimizedDirect 的值爲/data/dalvik-cache ,PathClassLoader 無法定義解壓的dex文件存儲路徑,因此它通常用來加載已經安裝的apk的dex文件(安裝的apk的dex文件會存儲在/data/dalvik-cache中)
類加載方案原理
首先要了解類加載過程
class 加載過程從ClassLoader 的loadClass方法開始
ClassLoader 的加載方法爲loadClass
可以通過 Android 中的類加載 文章中的最後的圖來說
DexPathList.java 中的findClass 方法(核心環節)
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {//1
Class<?> clazz = element.findClass(name, definingContext, suppressed);//2
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
- 遍歷dexElements
- 調用Element 的findClass 方法
- 每個Element 中內部封裝了DexFile ,用於加載dex,如果DexFile 不爲null,就調用DexFile 的loadClassBinaryName 方法
- loadClassBinaryName 方法中調用了defineClass,調用了defineClassNative方法來加載dex相關文件
那麼類加載方案的原理就出來,如下:
- ClassLoader 的加載過程,其中一個環節就是調用DexPathList的findClass 方法
- Element 內部封裝了DexFile ,DexFile用於加載dex文件,因此每個dex文件對應一個Element ,多個Element 組成了有序的Element 數組dexElements。
- 當要查找類時,會遍歷dexElements (相當於遍歷dex 文件數組),並調用DexFile的loadClassBinaryName 方法查找類
- 如果在Element 中(dex 文件)找到了該類就返回
- 如果沒有找到就接着在下一個Elment中進行查找
- 將Bug類Key.Class進行修改,再將Key.class打包成包含dex的補丁包Patch.jar
- 將補丁包放在Elment 數組dexElements 的第一個元素,這樣會首先找到Patch.dex中的Key.class去替換之前存在Bug的Key.class
- 排在數組後面的dex文件中存在Bug的Key.class 根據ClassLoader的雙親委託模式就不會被加載,這就是類加載方案
代碼實現
/**
* 修復方法
*/
private static void dexFix(ClassLoader classLoader, File optimizedDirectory, File... dexFiles) throws NoSuchFieldException, NoSuchMethodException, IllegalAccessError, IllegalAccessException {
StringBuilder sb = new StringBuilder();
for (File file : dexFiles) {
sb.append(file.getAbsolutePath()).append(":");
}
//1.使用DexClassLoader 加載所有外部dex文件
DexClassLoader dexClassLoader = new DexClassLoader(sb.deleteCharAt(sb.length() - 1).toString(),
optimizedDirectory.getAbsolutePath(), null, ClassLoader.getSystemClassLoader());
//2.獲取系統 dexElements
Object pathElements = getClassLoaderElements(classLoader);
//3.獲取外部dex 的dexElements
Object dexElements = getClassLoaderElements(dexClassLoader);
Field pathListField = ReflectUtil.findFiled(classLoader.getClass(), "pathList");
Object pathList = pathListField.get(classLoader);
Field dexElementsFiled = ReflectUtil.findFiled(pathList.getClass(), "dexElements");
//4.將系統與外部dexElements合併
Object arrayAppend = arrayAppend(dexElements, pathElements);
//5.修改系統 dexElements
dexElementsFiled.set(pathList, arrayAppend);
}
/**
* 將所有Array類型的數據按順序合併成一個Array數據
*/
public static Object arrayAppend(Object... elements) {
int length = 0;
for (Object element : elements) {
length += Array.getLength(element);
}
Object array = Array.newInstance(elements[0].getClass().getComponentType(), length);
for (int i = 0, j = 0, k = 0, elementLength = Array.getLength(elements[k]); i < length; i++) {
Array.set(array, i, Array.get(elements[k], i - j));
if (i - j == elementLength - 1) {
j += elementLength;
k++;
if (k < elements.length) {
elementLength = Array.getLength(elements[k]);
}
}
}
return array;
}
/**
* 獲取ClassLoader 中 dexElements 成員變量
*/
private static Object getClassLoaderElements(ClassLoader classLoader) throws NoSuchMethodException, IllegalAccessException {
Field pathListField = ReflectUtil.findFiled(classLoader.getClass(), "pathList");
Object pathList = pathListField.get(classLoader);
Field dexElementsFiled = ReflectUtil.findFiled(pathList.getClass(), "dexElements");
return dexElementsFiled.get(pathList);
}
缺點:
- 類加載方案需要重啓App 後ClassLoader重新加載新類
- 因爲類無法被卸載,想要重新加載新的類就需要重啓App
- 因此採用類加載方案的熱修復框架不能即時生效
- 如:QQ空間的超級補丁 ,Nuwa ,微信的Tinker ,餓了麼Amigo 都是使用通過操作Element 數組實現的
Instant Run (立即更新)
什麼是Instant Run
Instant Run 是Android Studio 2.0 以後新增的一個運行機制,能夠顯著減少開發人員第二次以及以後的構建和部署時間
使用Instant Run 前後編譯部署應用程序流程的區別
- 之前:代碼更改 -> 構建和部署app -> app銷燬->app 重啓 -> activity重啓->更改的代碼運行
- 之後:代碼更改->構建改變部分->部署更改部分->(Cold Swap<App 重啓>, Hot Swap ,Warm Swap<Activity 重啓>)->更改的代碼運行
- 傳統的編譯部署需要重新安裝App和重啓App,會比較耗時
Instant Run 的構建和部署都是基於更改的部分
Instant Run 原理
Instant Run原理就是:Instant Run 在第一次構建 APK 時,使用 ASM 在每一個方法中注入了判斷代碼
ASM 是一個 Java 字節碼操控框架,它能夠動態生成類或者增強現有類的功能。 ASM 可以直接產生 clsss文件,也可以在類被加載到虛擬機之前動態改變類的行爲。
怎麼使用Instant Run進行動態更新的 (以美團熱更新Robust 爲例簡單列舉下)
實現原理可以圍繞着兩點
- 代碼注入
- 替換老的邏輯(加載補丁包)
Robust 中的幾個重要類介紹:
PatchesInfo 接口 (用於保存修復類和舊類的類信息)
- 補丁包說明類,可以獲取所有補丁對象,每個對象包含被修復類名及該類對應的補丁類
- 每個修復包中必須有一個類需要實現這個,用於存放此修復包中所有需要修復的類信息
- 通過這個接口獲取到指定修復的類和舊類信息
PatchedClassInfo
- private String patchedClassName;//需要修復的類名稱
- private String patchClassName;//patch中的補丁類名稱
- 存放已修復類和舊類的類名,用於後續的動態加載
ChangeQuickRedirect 接口
- 每個補丁類必須實現ChangeQuickRedirect接口,內部有兩個方法
- isSupport 判斷當前方法是否執行補丁邏輯
- accessDispatch具體修復邏輯
PatchProxy
此類是對ChangeQuickRedirect修復類做了一層包裝,最終還是調用的ChangeQuickRedirect實現類中的方法
實現 ChangeQuickRedirect 的類
每個補丁類必須實現的接口(ChangeQuickRedirect),內部有兩個方法
兩個方法
- isSupport 判斷當前方法是否執行補丁邏輯
- accessDispatch具體修復邏輯
方法參數
- 第一個參數是方法的簽名,這個簽名的格式很簡單:方法所屬類全稱:方法名:方法是否爲static類型,注意中間使用冒號進行連接的。
- 第二個參數是方法的參數信息,而對於這個參數後面分析動態插入代碼邏輯的時候會發現操作非常麻煩,才把這個參數弄到手的。
Robust 的實現爲:
- 可以看到Robust爲每個class增加了一個類型爲ChangeQuickRedirect的靜態成員,而在每個方法前都插入了使用changeQuickRedirect相關的邏輯,
- 當changeQuickRedirect不爲null時,可能會執行到accessDispatch從而替換掉之前老的邏輯
代碼注入
原方法
public long getIndex() {
return 100;
}
將方法中注入判斷代碼後
public static ChangeQuickRedirect changeQuickRedirect;
public long getIndex() {
if(changeQuickRedirect != null) {
//PatchProxy中封裝了獲取當前className和methodName的邏輯,並在其內部最終調用了changeQuickRedirect的對應函數
if(PatchProxy.isSupport(new Object[0], this, changeQuickRedirect, false)) {
return ((Long)PatchProxy.accessDispatch(new Object[0], this, changeQuickRedirect, false)).longValue();
}
}
return 100L;
}
加載補丁包
重點方法在PatchExecutor 中的patch 方法
public class PatchExecutor extends Thread {
protected Context context;
protected PatchManipulate patchManipulate;
protected RobustCallBack robustCallBack;
public PatchExecutor(Context context, PatchManipulate patchManipulate, RobustCallBack robustCallBack) {
this.context = context.getApplicationContext();
this.patchManipulate = patchManipulate;
this.robustCallBack = robustCallBack;
}
@Override
public void run() {
try {
//拉取補丁列表
List<Patch> patches = fetchPatchList();
//應用補丁列表
applyPatchList(patches);
} catch (Throwable t) {
Log.e("robust", "PatchExecutor run", t);
robustCallBack.exceptionNotify(t, "class:PatchExecutor,method:run,line:36");
}
}
/**
* 拉取補丁列表
*/
protected List<Patch> fetchPatchList() {
return patchManipulate.fetchPatchList(context);
}
/**
* 應用補丁列表
*/
protected void applyPatchList(List<Patch> patches) {
if (null == patches || patches.isEmpty()) {
return;
}
Log.d("robust", " patchManipulate list size is " + patches.size());
for (Patch p : patches) {
if (p.isAppliedSuccess()) {
Log.d("robust", "p.isAppliedSuccess() skip " + p.getLocalPath());
continue;
}
if (patchManipulate.ensurePatchExist(p)) {
boolean currentPatchResult = false;
try {
currentPatchResult = patch(context, p);
} catch (Throwable t) {
robustCallBack.exceptionNotify(t, "class:PatchExecutor method:applyPatchList line:69");
}
if (currentPatchResult) {
//設置patch 狀態爲成功
p.setAppliedSuccess(true);
//統計PATCH成功率 PATCH成功
robustCallBack.onPatchApplied(true, p);
} else {
//統計PATCH成功率 PATCH失敗
robustCallBack.onPatchApplied(false, p);
}
Log.d("robust", "patch LocalPath:" + p.getLocalPath() + ",apply result " + currentPatchResult);
}
}
}
protected boolean patch(Context context, Patch patch) {
if (!patchManipulate.verifyPatch(context, patch)) {
robustCallBack.logNotify("verifyPatch failure, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:107");
return false;
}
ClassLoader classLoader = null;
try {
File dexOutputDir = getPatchCacheDirPath(context, patch.getName() + patch.getMd5());
classLoader = new DexClassLoader(patch.getTempPath(), dexOutputDir.getAbsolutePath(),
null, PatchExecutor.class.getClassLoader());
} catch (Throwable throwable) {
throwable.printStackTrace();
}
if (null == classLoader) {
return false;
}
Class patchClass, sourceClass;
Class patchesInfoClass;
PatchesInfo patchesInfo = null;
try {
Log.d("robust", "patch patch_info_name:" + patch.getPatchesInfoImplClassFullName());
patchesInfoClass = classLoader.loadClass(patch.getPatchesInfoImplClassFullName());
patchesInfo = (PatchesInfo) patchesInfoClass.newInstance();
} catch (Throwable t) {
Log.e("robust", "patch failed 188 ", t);
}
if (patchesInfo == null) {
robustCallBack.logNotify("patchesInfo is null, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:114");
return false;
}
//classes need to patch 1.獲取補丁包中所有待修復類信息
List<PatchedClassInfo> patchedClasses = patchesInfo.getPatchedClassesInfo();
if (null == patchedClasses || patchedClasses.isEmpty()) {
// robustCallBack.logNotify("patchedClasses is null or empty, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:122");
//手寫的補丁有時候會返回一個空list
return true;
}
boolean isClassNotFoundException = false;
for (PatchedClassInfo patchedClassInfo : patchedClasses) {
String patchedClassName = patchedClassInfo.patchedClassName;
String patchClassName = patchedClassInfo.patchClassName;
if (TextUtils.isEmpty(patchedClassName) || TextUtils.isEmpty(patchClassName)) {
robustCallBack.logNotify("patchedClasses or patchClassName is empty, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:131");
continue;
}
Log.d("robust", "current path:" + patchedClassName);
try {
try {
//2.加載要被修復的類
sourceClass = classLoader.loadClass(patchedClassName.trim());
} catch (ClassNotFoundException e) {
isClassNotFoundException = true;
// robustCallBack.exceptionNotify(e, "class:PatchExecutor method:patch line:258");
continue;
}
Field[] fields = sourceClass.getDeclaredFields();
Log.d("robust", "oldClass :" + sourceClass + " fields " + fields.length);
Field changeQuickRedirectField = null;
for (Field field : fields) {
if (TextUtils.equals(field.getType().getCanonicalName(), ChangeQuickRedirect.class.getCanonicalName()) && TextUtils.equals(field.getDeclaringClass().getCanonicalName(), sourceClass.getCanonicalName())) {
//3.找到要被修復類的注入的靜態變量
changeQuickRedirectField = field;
break;
}
}
if (changeQuickRedirectField == null) {
robustCallBack.logNotify("changeQuickRedirectField is null, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:147");
Log.d("robust", "current path:" + patchedClassName + " something wrong !! can not find:ChangeQuickRedirect in" + patchClassName);
continue;
}
Log.d("robust", "current path:" + patchedClassName + " find:ChangeQuickRedirect " + patchClassName);
try {
//4.加載要修復類對應的patch中的補丁類對象
patchClass = classLoader.loadClass(patchClassName);
Object patchObject = patchClass.newInstance();
changeQuickRedirectField.setAccessible(true);
changeQuickRedirectField.set(null, patchObject);//5.將靜態變量的值設置爲補丁包中的補丁類
//patchObject爲補丁類對象
Log.d("robust", "changeQuickRedirectField set success " + patchClassName);
} catch (Throwable t) {
Log.e("robust", "patch failed! ");
robustCallBack.exceptionNotify(t, "class:PatchExecutor method:patch line:163");
}
} catch (Throwable t) {
Log.e("robust", "patch failed! ");
// robustCallBack.exceptionNotify(t, "class:PatchExecutor method:patch line:169");
}
}
Log.d("robust", "patch finished ");
if (isClassNotFoundException) {
return false;
}
return true;
}
private static final String ROBUST_PATCH_CACHE_DIR = "patch_cache";
/*
* @param c
* @return 返回緩存補丁路徑,一般是內部存儲,補丁目錄
*/
private static File getPatchCacheDirPath(Context c, String key) {
File patchTempDir = c.getDir(ROBUST_PATCH_CACHE_DIR + key, Context.MODE_PRIVATE);
if (!patchTempDir.exists()) {
patchTempDir.mkdir();
}
return patchTempDir;
}
}
重點代碼
- 註解0處: 用DexClassLoader加載補丁包中的 PatchesInfoImpl 類,通過反射拿到這個 PatchesInfoImpl 實例對象。
- 註釋1處:通過PatchesInfoImpl獲取補丁包中所有待修復類信息
- 註釋2處:用DexClassLoader加載要被修復的類
- 註釋3處:找到要被修復類的注入的靜態變量
- 註釋4處:用DexClassLoader加載要修復類對應的patch中的補丁類對象
- 註釋5處:將靜態變量的值設置爲補丁包中的補丁類,代碼中patchObject爲補丁類對象
加載補丁包原理
- 使用DexClassLoader加載指定地址的修復包
- 然後用DexClassLoader 的 loadClass方法加載補丁類,new出新對象,
- 在用反射把這新的補丁對象設置到舊類的changeQuickRedirect靜態變量中即可。