最近一直在閱讀微信Tinker的源碼,為了加深理解和記憶,便自己動手實現(xiàn)了一下熱修復(fù),做了一個比較簡單的demo,并寫了這篇博客與大家分析,理解有誤的地方還請大家指出~
閱讀本文大概需要5分鐘時間。
類加載器
說到熱修復(fù),那么肯定要先說一下Android中的類加載器,這里我們簡單介紹一下。
在Android中,我們應(yīng)用自己的Class是交由PathClassLoader來加載的,PathClassLoader的代碼很簡單,只有兩個構(gòu)造函數(shù)
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
我們看到他的父類其實是BaseDexClassLoader,那么奧妙一定是在這里面,我們看一下BaseDexClassLoader的源碼
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
...
@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;
}
...
}
這里我們可以看到findClass查找Class的方法,BaseDexClassLoader重寫了ClassLoader的findClass方法,在方法內(nèi)部調(diào)用了DexPathList的findClass方法,我們進去看看這個方法做了什么
final class DexPathList {
/**
* List of dex/resource (class path) elements.
* Should be called pathElements, but the Facebook app uses reflection
* to modify 'dexElements' (http://b/7726934).
*/
private Element[] dexElements;
...
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
...
}
可以看到,DexPathList中有一個Element數(shù)組的成員變量,根據(jù)注釋可以知道這個數(shù)組是用于存放dex的路徑節(jié)點的,通過它的findClass方法可以知道,通過遍歷數(shù)組然后找到需要的類并返回,我們的切入點便是這里,將我們需要進行熱修復(fù)的“補丁”插入到這個數(shù)組的前端,那么在尋找Class的時候便會先找到我們已經(jīng)修復(fù)的類,從而實現(xiàn)熱修復(fù)。
為了控制本文篇幅,這里就不對Android的類加載器進行太多講解,對這一塊還不是特別清楚的童鞋可以先看看鴻洋大神前陣子推送的jjlanbupt寫的《Android插件化框架系列之類加載器》。
動手實踐

這里比較簡單,就是點擊SHOW按鈕彈出個Toast,我們現(xiàn)在要做的就是通過熱修復(fù)把Toast中的文本進行替換。SHOW按鈕的點擊事件如下
@Override
public void onClick(View v) {
int viewId = v.getId();
...
if (viewId == R.id.btn_show) {
Toast.makeText(this, Test.test(), Toast.LENGTH_SHORT).show();
}
}
這里我們看一下Test類,也是比較簡單
public class Test {
public static String test() {
return "hello world";
}
}
ok,目標(biāo)明確,我們要通過熱修復(fù)更改test方法中返回的字符串,開始擼代碼
public class Test {
public static String test() {
return "I am change!!!";
}
}
首先,我們把字符串改了之后把它編譯為class,再打包成jar,如果忘了如何把java編譯后打包成jar的方法的同學(xué),可以網(wǎng)上搜一下,這里就不再啰嗦了。
打包了jar后,別忘記要使用sdk中的dx工具將jar包轉(zhuǎn)換成dx格式的jar包,工具目錄在sdk目錄下的**...\build-tools\ **中

在控制臺使用該命令即可,現(xiàn)在我們已經(jīng)完成了補丁包的制作,我們先來看一下執(zhí)行熱修復(fù)的代碼
private static void patch(Context base) {
try {
//獲取PathClassLoader加載器
ClassLoader classLoader = MainActivity.class.getClassLoader();
//反射獲得BaseDexClassLoader中的pathList成員變量
Field dexPathListField = classLoader.getClass().getSuperclass().getDeclaredField("pathList");
//設(shè)為可訪問
dexPathListField.setAccessible(true);
//獲得PathClassLoader中的pathList對象
Object pathList = dexPathListField.get(classLoader);
//反射獲得DexPathList中的dexElements成員變量
Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
//設(shè)為可訪問
dexElementsField.setAccessible(true);
//獲得pathList中的dexElements數(shù)組對象
Object dexElements[] = (Object[]) dexElementsField.get(pathList);
//反射獲得pathList中的makeDexElements方法
Method method= pathList.getClass().getDeclaredMethod("makeDexElements", ArrayList.class, File.class,
ArrayList.class);
//設(shè)為可訪問
method.setAccessible(true);
List<File> files = new ArrayList<>();
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
//獲得我們的patch補丁包
File patchFile = new File(copyAssetsFile("patch.jar", base));
files.add(patchFile);
//指定解壓目錄
File optimizedDirectory = new File(base.getFilesDir().getAbsolutePath() + File.separator + "patch");
if (!optimizedDirectory.exists()) {
optimizedDirectory.mkdirs();
}
//執(zhí)行makeDexElements方法,解析我們的補丁包獲得dexElements數(shù)組
Object dexElementsResult[] = (Object[]) method.invoke(pathList, files, optimizedDirectory, suppressedExceptions);
//創(chuàng)建一個新的數(shù)組
Object finalResult[] = (Object[]) Array.newInstance(dexElements.getClass().getComponentType(), dexElements.length + dexElementsResult.length);
//先把我們的補丁包的dexElements數(shù)組放入剛創(chuàng)建的數(shù)組
System.arraycopy(dexElementsResult, 0, finalResult, 0, dexElementsResult.length);
//再把原來的dexElements數(shù)組放入
System.arraycopy(dexElements, 0, finalResult, dexElementsResult.length, dexElements.length);
//將新的數(shù)組設(shè)置回去
dexElementsField.set(pathList, finalResult);
for (Object o : finalResult) {
Log.d(MainActivity.class.getSimpleName(), o.toString());
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
這里主要就是使用反射獲取到dexElements數(shù)組,然后將我們自己的補丁包插入。因為我這里偷了個懶把補丁包直接放在assets目錄下,copyAssetsFile方法是將assets目錄下的文件復(fù)制到我們的app目錄下,因為PathClassLoader只能讀取app目錄下的文件,代碼也很簡單,這里也貼一下

private static String copyAssetsFile(String assetsFileName,Context context) {
String src = context.getFilesDir().getAbsolutePath() + File.separator + assetsFileName;
InputStream inputStream = null;
OutputStream outputStream = null;
try {
inputStream = context.getAssets().open(assetsFileName);
outputStream = new BufferedOutputStream(new FileOutputStream(src));
byte[] temp = new byte[1024];
int len;
while ((len = (inputStream.read(temp))) != -1) {
outputStream.write(temp, 0, len);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
if (outputStream != null) {
outputStream.flush();
outputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return src;
}
到這里我們代碼就搞定了,但是有兩個地方還是需要注意一下:
- 如果是在Activity中進行熱修復(fù),并且開啟了instant run,那么會導(dǎo)致看不到熱修復(fù)的效果,因為Android Studio的instant run會將ClassLoader替換成DelegateClassLoader,這里建議大家打包成apk然后提取出來安裝運行就可以。
- 為了避免掉進CLASS_ISPREVERIFIED的坑中,這里我們使用Art的機型來運行(Android 5.0以上),具體原因大家可以參考安卓App熱補丁動態(tài)修復(fù)技術(shù)介紹,微信Tinker由于采用了是全量替換dex的方法,所以也可以說是從另一個角度解決了問題。
最后我們看一下運行效果,重啟app,先點擊patch按鈕,再點擊show按鈕

可以看到,patch成功。
總結(jié)
最近看Tinker的源碼,確實對各種異常情況處理的很到位,收獲頗豐。各位有興趣的同學(xué)也可以去閱讀一番,自己動手實踐實踐,確實能加深自己的理解。若本文中哪里錯誤的地方大家也可以在評論中指出,謝謝大家~