在上篇博客中,我們初步瞭解了Android熱修復的基本流程,具體可以看我的博客Android熱修復(Hot Fix)案例全剖析(一),那麼本篇博客,我將爲大家全面剖析Android熱修復的實現案例。
1.將下載的修復補丁拷貝到應用的內部緩存目錄中
在上一篇文章中,我們已經生成了用於修復Bug的classes2.dex補丁包,通常我們會在APP後臺子線程中自動調用熱修復接口,並下載修復補丁,這裏爲了方便演示,我們把已經下載好的dex補丁文件放到SD卡中,然後將下載的修復補丁拷貝到應用的內部緩存目錄中cacheDir,之所以這樣做是因爲下一步我們需要使用類加載器ClassLoader在內部緩存中加載classese.dex包。下面是我寫的一個將classes2.dex包拷貝到內部緩存目錄中的方法。
/**
* 修復方法
*/
private void castielFixMethod() {
// 創建一個內部緩存目錄,把我們SD卡中的"classes2.dex"文件拷貝到內部緩存目錄中cache
File fileSDir = getDir(MyConstants.DEX_DIR, Context.MODE_PRIVATE);
String name = "classes2.dex";
String filePath = fileSDir.getAbsolutePath() + File.separator + name;
File file = new File(filePath);
if (file.exists()) {// 判斷是否已經存在dex文件
Log.i("WY", "已經存在dex文件");
file.delete();
}
// 通過IO流將dex文件寫到我們的緩存目錄中去
InputStream is = null;
FileOutputStream fos = null;
// 版權所有,未經許可請勿轉載:猴子搬來的救兵http://blog.csdn.net/mynameishuangshuai
try {
is = new FileInputStream(Environment.getExternalStorageDirectory());
fos = new FileOutputStream(filePath);
int len = 0;
byte[] buffer = new byte[1024];
while ((len = is.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
File f = new File(filePath);
Log.i("WY", "filePath:" + f.getAbsolutePath());
if (f.exists()) {
Toast.makeText(this, "新的dex文件已經覆蓋", Toast.LENGTH_LONG).show();
}
// 動態加載修復dex包
FixDexUtils.loadFixedDex(this);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fos.close();
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.實現熱修復工具類
這裏首先給大家普及一下類加載的原理:
在Android系統啓動的時候會創建一個Boot類型的ClassLoader實例,用於加載一些系統Framework層級需要的類。由於Android應用裏也需要用到一些系統的類,所以APP啓動的時候也會把這個Boot類型的ClassLoader傳進來。此外,APP也有自己的類,這些類保存在APK的dex文件裏面,所以APP啓動的時候,也會創建一個自己的ClassLoader實例,用於加載自己dex文件中的類。
ClassLoader去加載Dex文件,首先Dex文件是放在/data/apk/packagename~1/base/apk,由於apk是一個類似於壓縮包的東西,Android其實是使用一個優化的臨時緩存目錄optimizeDir(dex),專門把Dex文件解壓進去,這樣以後就從這個臨時緩存目錄中加載,提高效率。
在1代碼中我們提到了loadFixedDex()方法,便是我們的核心熱修復工具類,我給大傢俱體講一下:
ClassLoader有一個簡單的實現類-PathClassLoader。該類作爲Android的默認的類加載器,本身繼承自BaseDexClassLoader,BaseDexClassLoader重寫了findClass方法,該方法是ClassLoader的核心。
每個ClassLoader有一個pathList變量,是標識dex文件的路徑,我們通過該路徑加載dex文件,默認不分包的時候只有一個dex文件,當然谷歌在頂層設計時允許我們有多個dex文件。
ClassLoader去找optimizeDir(dex)目錄,然後把目錄添加到pathList裏面去,接着去找目錄下面的所有的dex文件,把這些dex文件當做一個數組放到dexElements中去,這樣就可以有多個dex文件。
pathList{
dexElements{
[classes.dex,classes2.dex]
}
}
ClassLoader每加載一個類,它會先找classes.dex,如果找不到就去classes2.dex中找,如果裏面又一個dex有問題,比如說classes2.dex出問題了,我們就需要弄一個修復的新的classes2.dex文件放到數組中去,替換掉有問題的;但是classes2.dex中可能有多個類,除了有問題的類,也可能有很多正確的類,我們在替換時沒必要把所有的類都替換掉,所以我們只要替換有問題的類。
爲此,我們可以採用一個策略,把新的替換的dex文件放到數組的最前面,最終數組的形態爲:
[classes2.dex,classes.dex,classes2.dex]
這裏解釋下,ClassLoader類加載器先加載我們修復的正確的dex文件,然後順序加載數組中其他的dex元素,到了最後加載到舊的classes2.dex元素,由於前面已經加載了更新的classes2.dex(更新的dex文件中只包含修復的class),那麼舊的classes2.dex元素中的有Bug的class就不會再加載,而是隻加載其餘的沒有錯誤的class。
整個流程其實非常簡單,但是如果我們要實現這個過程卻有個障礙,那就是由於我們的APK程序可能正在運行,谷歌並沒有提供相關的接口方法去實現這一步驟,爲此,我們需要使用反射的手段去實現。
1.首先需要反射ClassLoader類,找到裏面的pathList變量,然後找到dexElements[]數組,該數組在修復之前只有兩個元素,分別是classes.dex和classes2.dex(出錯的),假設值數組1;
2.接着我們要往dexElements[]數組中添加classes2.dex文件。
Android中要想實現加載dex文件,需要使用DexClassLoader類加載classes2.dex(補丁),加載到dexElements[]數組中去,假設值數組2。
3.最後,我們需要把兩個dexElements[]數組合並,作爲一個新數組dexElements[],該數組中包含元素爲classes2.dex(補丁),classes.dex和classes2.dex(出錯的),完成後將數組返回賦值給系統的ClassLoader。
最後貼出熱修復工具類源碼
public class FixDexUtils {
private static HashSet<File> loadedDex = new HashSet<File>();
public static void loadFixedDex(Context context) {
if (context == null) {
return;
}
// 首先拿到緩存目錄
File fileSDir = context.getDir(MyConstants.DEX_DIR,
Context.MODE_PRIVATE);
File[] listFils = fileSDir.listFiles();
// 遍歷緩存文件
for (File file : listFils) {
// 如果文件是以"classes"開始或者以".dex"結尾,說明這是從SDK中拷貝回來的修復包
if (file.getName().startsWith("classes")
|| file.getName().endsWith(".dex")) {
Log.i("WY", "當前dexName:" + file.getName());
loadedDex.add(file);
}
}
doDexInject(context, fileSDir);
}
private static void doDexInject(Context context, File fileDir) {
if (Build.VERSION.SDK_INT >= 23) {
Log.i("WY", "Unable to do dex inject on SDK"
+ Build.VERSION.SDK_INT);
}
// .dex 的加載需要一個臨時目錄
String optimizeDir = fileDir.getAbsolutePath() + File.separator
+ "opt_dex";
File fopt = new File(optimizeDir);
if (!fopt.exists())
fopt.mkdirs();
try {
// 根據.dex 文件創建對應的DexClassLoader 類
for (File file : loadedDex) {// 循環迭代,用於多個修復包同時注入
DexClassLoader classLoader = new DexClassLoader(
file.getAbsolutePath(), fopt.getAbsolutePath(), null,
context.getClassLoader());
// 注入
inject(classLoader, context);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void inject(DexClassLoader classLoader, Context context) {
// 獲取到系統的DexClassLoader 類
PathClassLoader pathLoader = (PathClassLoader) context.getClassLoader();
try {
Object dexElements = combineArray(
getDexElements(getPathList(classLoader)),
getDexElements(getPathList(pathLoader)));
Object pathList = getPathList(pathLoader);
setField(pathList, pathList.getClass(), "dexElements", dexElements);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 通過反射獲取DexPathList中dexElements
*/
private static Object getDexElements(Object paramObject)
throws IllegalArgumentException, NoSuchFieldException,
IllegalAccessException {
return getField(paramObject, paramObject.getClass(), "dexElements");
}
/**
* 通過反射獲取BaseDexClassLoader中的PathList對象
*/
private static Object getPathList(Object baseDexClassLoader)
throws IllegalArgumentException, NoSuchFieldException,
IllegalAccessException, ClassNotFoundException {
return getField(baseDexClassLoader,
Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}
/**
* 通過反射獲取指定字段的值
*/
private static Object getField(Object obj, Class<?> cl, String field)
throws NoSuchFieldException, IllegalArgumentException,
IllegalAccessException {
Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
return localField.get(obj);
}
/**
* 通過反射設置字段值
*/
private static void setField(Object obj, Class<?> cl, String field,
Object value) throws NoSuchFieldException,
IllegalArgumentException, IllegalAccessException {
Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
localField.set(obj, value);
}
/**
* 合併兩個數組
*/
private static Object combineArray(Object arrayLhs, Object arrayRhs) {
Class<?> localClass = arrayLhs.getClass().getComponentType();
int i = Array.getLength(arrayLhs);
int j = i + Array.getLength(arrayRhs);
Object result = Array.newInstance(localClass, j);
for (int k = 0; k < j; ++k) {
if (k < i) {
Array.set(result, k, Array.get(arrayLhs, k));
} else {
Array.set(result, k, Array.get(arrayRhs, k - i));
}
}
return result;
}
}