熱修復(java層dex分包方式實現)
修復原理:
首先要了解 android 載入classes.dex檔案的流程或原理。
1、android 如何載入classes.dex檔案的?
// 用來載入apk 的dex檔案:PathClassLoader
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
通過了解PathClassLoader 載入dex檔案得知,將修復好的dex檔案插入到dexElements集合中,
根據類的載入機制,一個類只能被載入一次,所以當虛擬機器載入 修復好的dex檔案時,就會載入
修復好的類,而有問題的類將不被載入。因此達到修復的目的。
實現步驟:
1: 總網路下載補丁包,在app啟動的時候載入補丁包,就到達了修復有問題的類的目的。
這裡 用本地拷貝的方式來模擬從網路下載。將補丁包放在assets資源目錄下.
2:由於PathClassLoader 和 DexClassLoader 都繼承自BaseClassLoader,所以這裡 通過反射拿到
BaseClassLoader中的DexPathList pathList 。
3: 拿到DexPathList pathList物件後,再獲取pathList中的存放dex的陣列 Element[] dexElements。
/** list of dex/resource (class path) elements */
private final Element[] dexElements;
4:通過DexClassLoader 拿到補丁包中dex檔案的 pathList,最終拿到補丁包中的Element[] dexElements。
5:用補丁包中的Element[] dexElements 替換 app中的Element[] dexElements。
6:重新啟動app即可
完整實現:
崩潰程式碼所在類:將本類程式碼修復後 重新打包成dex檔案,放入assets目錄下 來測試。
//生成新的 classes2.dex
//dx --dex --output=D:\classes2.dex class路徑(包含完整包名)
package example.nzh.om.myhandler.hotfix; public class CommonUtil { public static String getStr() { String[] array = {"123", "456", "789"}; return array[3]; } }
修復方法:onclick
package example.nzh.om.myhandler.hotfix;
import android.app.Activity;
import android.content.Context;
import android.net.http.HttpResponseCache;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Toast;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.HashSet;
import dalvik.system.BaseDexClassLoader;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;
import example.nzh.om.myhandler.R;
public class HotFixTestActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_hot_fix_test);
}
public void test(View view) {
String s = CommonUtil.getStr();
if (!TextUtils.isEmpty(s)) {
Toast.makeText(this, s, Toast.LENGTH_LONG).show();
}
}
// 存放修復好的dex
HashSet<File> fixDex = new HashSet<>();
public void fix(View view) {
//1:將下載好的classes.dex修復補丁包 拷貝到 /data/data/packageName/odex 目錄。
try {
String testFile = "classes2.dex";
String newDexDir = "odex";
String dir = File.separator + "data" +
File.separator + "data" +
File.separator + "example.nzh.om.myhandler" +
File.separator + newDexDir;
File dexDir = new File(dir);
if (!dexDir.exists()) {
dexDir.mkdir();
}
InputStream in = getAssets().open(testFile);
FileOutputStream fos = new FileOutputStream(dir + File.separator + testFile);
Toast.makeText(this, dir + File.separator + testFile, Toast.LENGTH_LONG).show();
byte[] buffer = new byte[1024];
int len;
while ((len = in.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
fos.flush();
fos.close();
//?2:遍歷該目錄下的所有修復好的dex檔案
File dir2 = dexDir;//getApplicationContext().getDir(newDexDir, Context.MODE_PRIVATE);
File[] files = dir2.listFiles();
for (File file : files) {
if (file.getName().startsWith("classes") && file.getName().endsWith(".dex")) {
fixDex.add(file);
}
}
if (fixDex.size() == 0) {
Toast.makeText(this, "修復包拷貝失敗!", Toast.LENGTH_SHORT).show();
return;
} else {
Toast.makeText(this, "修復包拷貝成功!", Toast.LENGTH_SHORT).show();
}
//2.1 載入應用程式的dex,通過( 用context.get---> PathClassLoader)
// 得到的classloader就是PathClassLoader
//2.2 載入修復後的dex檔案 用new DexClassLoader
//2.3 合併兩個檔案:反射拿到baseClassLoader中的pathList,再從DexPathList中反射獲取Element[]
// 最終替換Element[] elements變數。
// 修復:
PathClassLoader pathClassLoader = (PathClassLoader) this.getClassLoader();
String optimizedDir = dir2.getAbsolutePath() + File.separator + "optimized_dir";
File opt = new File(optimizedDir);
if (!opt.exists()) {
opt.mkdir();
}
for (File file : fixDex) {
DexClassLoader dexClassLoader = new DexClassLoader(file.getAbsolutePath()
, optimizedDir
, null
, this.getClassLoader());
//反射第一步:拿到pathList物件(BaseClassLoader-->pathList(DexPathList))
Class baseClassLoaderCls = Class.forName("dalvik.system.BaseDexClassLoader");
String feildNamePathList = "pathList";
Object dexObj = reflectField(dexClassLoader, baseClassLoaderCls, feildNamePathList);//修復後的dex 的DexPathList
Object pathObj = reflectField(pathClassLoader, baseClassLoaderCls, feildNamePathList);// 源程式的dex的DexPathList
//反射第二步:得到DexPathList-->Element[]
String feildNameElement = "dexElements";
//修復後的ClassLoader裡面的Elements 資料
Object fixedDexArray = reflectField(dexObj, dexObj.getClass(), feildNameElement);
//源程式中的ClassLoader裡面的Elements 資料
Object pathDexArray = reflectField(pathObj, pathObj.getClass(), feildNameElement);
//合併陣列
Object fianlDex = combineArray(fixedDexArray, pathDexArray);
//更換DexPathList中的Element[] elments
setFeild(pathObj, pathObj.getClass(), "dexElements", fianlDex);
}
//生成新的 classes2.dex
//dx --dex --output=D:\classes2.dex class路徑(包含完整包名)
Toast.makeText(this, "修復成功", Toast.LENGTH_SHORT).show();
} catch (Exception e) {
e.printStackTrace();
}
}
//合併陣列,將修復dex 插入陣列最前面。
public Object combineArray(Object arrayNew, Object arrayOld) {
int i = Array.getLength(arrayNew);
int j = i + Array.getLength(arrayOld);
//複製陣列,長度為兩個陣列長度之和。型別和新的陣列一樣。
Object result = Array.newInstance(arrayNew.getClass().getComponentType(), j);
for (int k = 0; k < j; k++) {
if (k < i) {
//新陣列的0-i個元素 插入新的陣列資料
Array.set(result, k, Array.get(arrayNew, k));
} else {
// i-end 插入舊陣列的資料
Array.set(result, k, Array.get(arrayOld, k - i));
}
}
return result;
}
/**
* 反射成員變數
*
* @param obj
* @param clazz
* @param fieldName
*/
public Object reflectField(Object obj, Class clazz, String fieldName) throws Exception {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
}
public void setFeild(Object obj, Class clazz, String fieldName, Object value) throws Exception {
Field f = clazz.getDeclaredField(fieldName);
f.setAccessible(true);
f.set(obj, value);
}
}
最後使用:Manifest.xml 引用MyApplication
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
}
@Override
protected void attachBaseContext(Context base) {
MultiDex.install(base);
super.attachBaseContext(base);
}
}