算是記錄知識的博客,所以精簡爲主。
熱修復分爲兩種:
Java層修復,以QZone,Tinker爲代表。需要重新啓動後才能完成修復。
Native層修復,以阿里係爲代表。可以達成及時修復。
實現demo用的是Java層的修復。java層修復的原理是通過類加載器的機制來實現——雙親委派機制。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
這上面是系統源碼,可以看出在加載一個class的時候,他會先讓父類去加載,父類如果可以加載,那就父類加載,父類加載不了,那就會拋出一個異常,讓子類去加載。這麼做的主要原因就是可以避免一個類被重複加載。所以你肯定遇到過使用第三方庫,編譯時提示你class重複這個問題。那麼類替換的熱修復方案就是,讓fix類先被加載,這樣有問題的class就不會被加載了。
在5.0之前會有一個CLASS_ISPREVERIFIED的錯誤。這個錯誤的解釋是:
- 在apk安裝的時候,虛擬機會將dex優化成odex後纔拿去執行。在這個過程中會對所有class一個校驗。
- 校驗方式:假設A該類在它的static方法,private方法,構造函數,override方法中直接引用到B類。如果A類和B類在同一個dex中,那麼A類就會被打上CLASS_ISPREVERIFIED標記
- 被打上這個標記的類不能引用其他dex中的類,否則就會報圖中的錯誤
- 在我們的Demo中,MainActivity和Cat本身是在同一個dex中的,所以MainActivity被打上了CLASS_ISPREVERIFIED。而我們修復bug的時候卻引用了另外一個dex的Cat.class,所以這裏就報錯了
- 而普通分包方案則不會出現這個錯誤,因爲引用和被引用的兩個類一開始就不在同一個dex中,所以校驗的時候並不會被打上CLASS_ISPREVERIFIED
上文是在其他博客中引用的。如果你的minSDKVersion大於5.0就不用考慮這件事兒了。上面解決方案是插樁,這裏不做贅述。
首先需要把一個class變成dex,使用SDK下Tools自帶的dx工具就行了。
dx --dex --output out.dex in.class
記住class需要放在相應的包名路徑中。
得到了class之後,就用下面的工具類就行。
/**
* 修復的工具類
* on 2020/4/6
*/
public class FixUtil {
/**
* 講dex存到自己的文件夾下
* @param context
* @param dirName 存放dex的目錄
* @param dexName dex名字
*/
public static void loadDex(Context context,String dirName,String dexName) {
//構建存放dex目錄
File dexDir = context.getDir(dirName, Context.MODE_PRIVATE);
//拿到dex文件拷貝
String dexPath = dexDir.getAbsoluteFile() + File.separator + dexName;
File dexFile = new File(dexPath);
if (!dexFile.exists()) {
try {
dexFile.createNewFile();
FixUtil.copyFiles(context, dexName, dexFile);
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void FixDex(Context context,File dexDir){
//1.獲得要修復的dexList
List<File> dexList = getDexList(context, dexDir);
try {
//2.找到pathList對象
Field pathListField = FixUtil.getField(context.getClassLoader(), "pathList");
Object pathList = pathListField.get(context.getClassLoader());
//3.獲取系統的dexElements
Field dexElementsField = FixUtil.getField(pathList, "dexElements");
Object[] oldDexElements = (Object[]) dexElementsField.get(pathList);
//4.獲取makeDexElements
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
Method makeDexElements = FixUtil.getMethod(pathList, "makeDexElements", List.class, File.class
, List.class, ClassLoader.class);
//5.加載修復的dexList
Object[] fixDexElements = (Object[]) makeDexElements.invoke(pathList, dexList, dexDir, suppressedExceptions,
context.getClassLoader());
//6.創建新的Elements數組
Object[] newDexElements = (Object[]) Array.newInstance(oldDexElements.getClass().getComponentType()
, oldDexElements.length + fixDexElements.length);
//7.合併Elements數組
System.arraycopy(fixDexElements, 0, newDexElements, 0, fixDexElements.length);
System.arraycopy(oldDexElements, 0, newDexElements, fixDexElements.length, fixDexElements.length);
//8.將系統的Elements重新設置
dexElementsField.set(pathList, newDexElements);
Log.d("TAG", "修復完成");
} catch (Exception e) {
Log.d("TAG", "修復失敗" + e.getMessage());
e.printStackTrace();
}
}
/**
* 獲取文件夾下所有.dex文件
* @param context
* @param dexDir
* @return
*/
public static List<File> getDexList(Context context, File dexDir) {
File[] files = dexDir.listFiles();
List<File> fileList = new ArrayList<>();
for (File file : files) {
if (file.getName().endsWith(".dex")) {
fileList.add(file);
}
}
return fileList;
}
/**
* 將文件拷貝到私有目錄下
* @param context
* @param fileName
* @param desFile
*/
public static void copyFiles(Context context, String fileName, File desFile) {
InputStream in = null;
OutputStream out = null;
try {
in = context.getAssets().open(fileName);
out = new FileOutputStream(desFile.getAbsolutePath());
byte[] bytes = new byte[1024];
int len = 0;
while ((len = in.read(bytes)) != -1)
out.write(bytes, 0, len);
out.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (in != null)
in.close();
if (out != null)
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 反射屬性
* @param object
* @param name
* @return
* @throws NoSuchFieldException
*/
public static Field getField(Object object, String name) throws NoSuchFieldException {
Class<?> aClass = object.getClass();
while (aClass != null) {
try {
Field declaredField = aClass.getDeclaredField(name);
if (!declaredField.isAccessible()) {
declaredField.setAccessible(true);
}
return declaredField;
} catch (NoSuchFieldException e) {
e.printStackTrace();
aClass = aClass.getSuperclass();
}
}
throw new NoSuchFieldException("找不到對應屬性");
}
/**
* 反射方法
* @param object
* @param name
* @param param
* @return
* @throws NoSuchMethodException
*/
public static Method getMethod(Object object, String name, Class... param) throws NoSuchMethodException {
Class<?> aClass = object.getClass();
while (aClass != null) {
try {
Method declaredField = aClass.getDeclaredMethod(name, param);
if (!declaredField.isAccessible()) {
declaredField.setAccessible(true);
}
return declaredField;
} catch (NoSuchMethodException e) {
e.printStackTrace();
aClass = aClass.getSuperclass();
}
}
throw new NoSuchMethodException("找不到對應屬性");
}
}
我將dex存放在Asset文件夾下模擬的網絡加載,如果是網絡加載的話替換下這塊就行。然後記住在Application裏面去加載你存放dex文件夾下面的所有fix文件。爲什麼要重新啓動才能加載?因爲你沒法去把一個已經加載的class給卸載咯。還有記住別開混淆!