熱修復技術自從QQ空間團隊搞出來之后便漸漸趨于成熟。
我們這個系列主要介紹如何一步步手動實現(xiàn)基本的熱修復功能,無需使用第三方框架。
在開始學習之前,需要對基本的熱修復技術有些了解,以下文章可以幫助到你:
一、dex文件的生成與加載
我們在這部分主要做的流程有:
- 1.編寫基本的Java文件并編譯為.class文件。
- 2.將.class文件轉(zhuǎn)為.dex文件。
- 3.將轉(zhuǎn)好的dex文件放入創(chuàng)建好的Android工程內(nèi)并在啟動時將其寫入本地。
- 4.加載解壓后的.dex文件中的類,并調(diào)用其方法進行測試。
Note: 在閱讀本節(jié)之前最好先了解一下類加載器的雙親委派模型、DexClassLoader的使用以及反射的知識點。
編寫基本的Java文件并編譯為.class文件
首先我們在一個工程目錄下開始創(chuàng)建并編寫我們的Java文件,你可能會選擇各種IDE來做這件事,但我在這里勸你不要這么做,因為有坑在等你。等把基本流程搞清楚可以再選擇更進階的方法。這里我們可以選擇文本編輯器比如EditPlus來對Java文件進行編輯。
新建一個Java文件,并命名為:com.sahadev.bean.ClassStudent.java,并在java文件內(nèi)鍵入以下代碼
public class com.sahadev.bean.ClassStudent {
private String name;
public com.sahadev.bean.ClassStudent() {
}
public void setName(String name) {
this.name = name;
}
public String getName(){
return this.name + ".Mr";
}
}
Note: 這里要注意,不要對類添加包名,因為在后期對class文件處理時會遇到問題,具體問題會稍后說明。上面的getName方法在返回時對this.name屬性添加了一段字符串,這里請注意,后面會用到。
在文件創(chuàng)建好之后,對Java文件進行編譯:

將.class文件轉(zhuǎn)為.dex文件
好,現(xiàn)在我們使用class文件生成對應的dex文件。生成dex文件所需要的工具為dx,dx工具位于sdk的build-tools文件夾內(nèi),如下圖所示:

Tips: 為了方便使用,建議將dx的路徑添加到環(huán)境變量中。如果對dx工具不熟悉的,可以在終端中輸入dx --help以獲取幫助。
dx工具的基本用法是:
dx --dex [--output=<file>] [<file>.class | <file>.{zip,jar,apk} | <directory>]
Tips: 剛開始自己摸索的時候,就沒有仔細看命令,導致后面兩個參數(shù)的順序顛倒了,搞出了一些讓人疑惑難解的問題,最后又不得不去找dx工具的源碼調(diào)試,最后才發(fā)現(xiàn)自己的問題在哪。如果有對dx工具感興趣的,可以對dx的包進行反編譯或者獲取dx的相關源代碼進行了解。dx.lib文件位于dx.bat的下級目錄lib文件夾中,可以使用JD-GUI工具對其進行查看或?qū)С觥H绻枰@取源代碼的,請使用以下命令進行克隆:
我們使用以下命令生成dex文件:
dx --dex --output=user.dex com.sahadev.bean.ClassStudent.class
這里我為了防止出錯,提前在當前目錄下新建好了user.dex文件。上述命令依賴編譯.class文件的JDK版本,如果使用的是JDK8編譯的class會提示以下問題:
PARSE ERROR:
unsupported class file version 52.0
...while parsing com.sahadev.bean.ClassStudent.class
1 error; aborting
這里的52.0意味著class文件不被支持,需要使用JDK8以下的版本進行編譯,但是dx所需的環(huán)境還是需要為JDK8的,這里我編譯class文件使用的是JDK7,請注意。
上面我們提到了為什么先不要在ClassStudent中使用包名,因為在執(zhí)行dx的時候會報以下異常,這是因為以下第二項條件沒有通過,該代碼位于com.android.dx.cf.direct.DirectClassFile文件內(nèi):
String thisClassName = thisClass.getClassType().getClassName();
if(!(filePath.endsWith(".class") && filePath.startsWith(thisClassName) && (filePath.length()==(thisClassName.length()+6)))){
throw new ParseException("class name (" + thisClassName + ") does not match path (" + filePath + ")");
}
運行截圖如下所示:

好了,到此為止我們的目錄應該如下:

寫入dex到本地磁盤
接下來將生成好的user.dex文件放入Android工程的res\raw文件夾下:

在系統(tǒng)啟動時將其寫入到磁盤,這里不再貼出具體的寫入代碼,項目的MainActivity中包含了此部分代碼。
加載dex中的類并測試
在寫入完畢之后使用DexClassLoader對其進行加載。DexClassLoader的構造方法需要4個參數(shù),這里對這4個參數(shù)進行簡要說明:
- String dexPath:dex文件的絕對路徑。在這里我將其放入了應用的cache文件夾下。
- String optimizedDirectory:優(yōu)化后的dex文件存放路徑。DexClassLoader在構造完畢之后會對原有的dex文件優(yōu)化并生成一個新的dex文件,在這里我選擇的是.../cache/optimizedDirectory/目錄。此外,API文檔對該目錄有嚴格的說明:Do not cache optimized classes on external storage.出于安全考慮,請不要將優(yōu)化后的dex文件放入外部存儲器中。
- String libraryPath:dex文件所需要的庫文件路徑。這里沒有依賴,使用空字符串代替。
- ClassLoader parent:雙親委派模型中提到的父類加載器。這里我們使用默認的加載器,通過getClassLoader()方法獲得。
在解釋完畢DexClassLoader的構造參數(shù)之后,我們開始對剛剛的dex文件進行加載:
DexClassLoader dexClassLoader = new DexClassLoader(apkPath, file.getParent() + "/optimizedDirectory/", "", classLoader);
接來下開始load我們剛剛寫入在dex文件中的ClassStudent類:
Class<?> aClass = dexClassLoader.loadClass("com.sahadev.bean.ClassStudent");
然后我們對其進行初始化,并調(diào)用相關的get/set方法對其進行驗證,在這里我傳給ClassStudent對象一個字符串,然后調(diào)用它的get方法獲取在方法內(nèi)合并后的字符串:
Object instance = aClass.newInstance();
Method method = aClass.getMethod("setName", String.class);
method.invoke(instance, "Sahadev");
Method getNameMethod = aClass.getMethod("getName");
Object invoke = getNameMethod.invoke(instance););
最后我們實現(xiàn)的代碼可能是這樣的:
/**
* 加載指定路徑的dex
*
* @param apkPath
*/
private void loadClass(String apkPath) {
ClassLoader classLoader = getClassLoader();
File file = new File(apkPath);
try {
DexClassLoader dexClassLoader = new DexClassLoader(apkPath, file.getParent() + "/optimizedDirectory/", "", classLoader);
Class<?> aClass = dexClassLoader.loadClass("com.sahadev.bean.ClassStudent");
mLog.i(TAG, "com.sahadev.bean.ClassStudent = " + aClass);
Object instance = aClass.newInstance();
Method method = aClass.getMethod("setName", String.class);
method.invoke(instance, "Sahadev");
Method getNameMethod = aClass.getMethod("getName");
Object invoke = getNameMethod.invoke(instance);
mLog.i(TAG, "invoke result = " + invoke);
} catch (Exception e) {
e.printStackTrace();
}
}
最后附上我們的運行截圖:

二、Class文件的替換
在完成基本外部類加載之后,我們這一節(jié)開始對工程內(nèi)部的類實行替換。
Tips: 本章主要依賴文章http://blog.csdn.net/vurtne_ye/article/details/39666381中的未實現(xiàn)代碼實現(xiàn),實現(xiàn)思路也源自該文章,在閱讀本文之前可以先行了解。
這一節(jié)我們主要實現(xiàn)的流程有:
- 在類的內(nèi)部創(chuàng)建與外部dex相同的類文件,但在調(diào)用getName()方法返回字符串時會稍有區(qū)別,以便進行結果驗證
- 使用DexClassLoader加載外部的user.dex
- 將DexClassLoader中的dexElements放在PathClassLoader的dexElements之前
- 驗證替換結果
因為上節(jié)課中專門聲明了不可以對類聲明包名,但是這樣在Android工程中無法引用沒有包名的類,所以把不能聲明包名的問題解決了一下。
上一節(jié)課主要遇到的問題是在編譯Java文件時沒有使用正當?shù)拿睢邪腏ava文件應當使用以下命令:
javac -d ./ ClassStudent.java
經(jīng)過上面命令編譯后的.class文件便可以順利通過dx工具的轉(zhuǎn)換。
我們還是按照第一節(jié)的步驟將轉(zhuǎn)換后的user.dex文件放入工程中并寫入本地磁盤,以便稍后使用。
在開始之前還是先說一下具體的實現(xiàn)思路:一個類在使用之前必須要經(jīng)過加載器的加載才能使用,在加載器加載類之前會調(diào)用自身的findClass()方法進行查找。然而在Android中類的查找使用的是BaseDexClassLoader,BaseDexClassLoader對findClass()方法進行了重寫:
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
pathList是類DexPathList的實例,這里pathList.findClass的實現(xiàn)如下:
public Class findClass(String name) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
return clazz;
}
}
}
return null;
}
由此我們可以得知類的查找是通過遍歷dexElements來進行查找的。所以為了實現(xiàn)替換效果,我們需要將DexClassLoader中的Element對象放到dexElements數(shù)組的第0個位置,這樣才能在BaseDexClassLoader查找類時先找到DexClassLoader所用的user.dex中的類。
Tips: 如果對上面這句話看不懂的,沒關系,可以先了解一下類的加載機制與ClassLoader的雙親委派模型。
好了,有了基本的實現(xiàn)思路之后,我們接下來對思路進行實踐。
下面的方法是我們主要的注入方法:
public String inject(String apkPath) {
boolean hasBaseDexClassLoader = true;
File file = new File(apkPath);
try {
Class.forName("dalvik.system.BaseDexClassLoader");
} catch (ClassNotFoundException e) {
hasBaseDexClassLoader = false;
}
if (hasBaseDexClassLoader) {
PathClassLoader pathClassLoader = (PathClassLoader) getClassLoader();
DexClassLoader dexClassLoader = new DexClassLoader(apkPath, file.getParent() + "/optimizedDirectory/", "", pathClassLoader);
try {
Object dexElements = combineArray(getDexElements(getPathList(pathClassLoader)), getDexElements(getPathList(dexClassLoader)));
Object pathList = getPathList(pathClassLoader);
setField(pathList, pathList.getClass(), "dexElements", dexElements);
return "SUCCESS";
} catch (Throwable e) {
e.printStackTrace();
return android.util.Log.getStackTraceString(e);
}
}
return "SUCCESS";
}
這段代碼原封不動采用于http://blog.csdn.net/vurtne_ye/article/details/39666381文章中最后的實現(xiàn)代碼,但是該文章并沒有給出具體的注入細節(jié)。我們接下里的過程就是對沒有給全的細節(jié)進行補充與講解。
這段代碼的核心在于將DexClassLoader中的dexElements與PathClassLoader中的dexElements進行合并,然后將合并后的dexElements替換原先的dexElements。最后我們在使用ClassStudent類的時候便可以直接使用外部的ClassStudent,而不會再使用默認的ClassStudent類。默認情況下會加載默認的ClassStudent類。
首先我們通過classLoader獲取各自的pathList對象:
public Object getPathList(BaseDexClassLoader classLoader) {
Class<? extends BaseDexClassLoader> aClass = classLoader.getClass();
Class<?> superclass = aClass.getSuperclass();
try {
Field pathListField = superclass.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object object = pathListField.get(classLoader);
return object;
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
在使用以上反射的時候要注意,pathList屬性屬于基類BaseDexClassLoader。所以如果直接對DexClassLoader或者PathClassLoader獲取pathList屬性的話,會得到null。
其次是獲取pathList對應的dexElements,這里要注意dexElements是個數(shù)組對象:
public Object getDexElements(Object object) {
if (object == null)
return null;
Class<?> aClass = object.getClass();
try {
Field dexElements = aClass.getDeclaredField("dexElements");
dexElements.setAccessible(true);
return dexElements.get(object);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
接下來我們將兩個數(shù)組對象合并成為一個:
public Object combineArray(Object object, Object object2) {
Class<?> aClass = Array.get(object, 0).getClass();
Object obj = Array.newInstance(aClass, 2);
Array.set(obj, 0, Array.get(object2, 0));
Array.set(obj, 1, Array.get(object, 0));
return obj;
}
上面這段代碼我們根據(jù)數(shù)組對象的類型創(chuàng)建了一個新的大小為2的新數(shù)組,并將兩個數(shù)組的第一個元素取出,將dex中的dexElement放在了第0個位置。這樣可以確保在查找類時優(yōu)先從dex的dexElement中查找。
最后將原先的dexElements覆蓋:
public void setField(Object pathList, Class aClass, String fieldName, Object fieldValue) {
try {
Field declaredField = aClass.getDeclaredField(fieldName);
declaredField.setAccessible(true);
declaredField.set(pathList, fieldValue);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
運行驗證結果:
大功告成!
類的加載機制簡要介紹
一個類在被加載到內(nèi)存之前要經(jīng)過加載、驗證、準備等過程。經(jīng)過這些過程之后,虛擬機才會從方法區(qū)將代表類的運行時數(shù)據(jù)結構轉(zhuǎn)換為內(nèi)存中的Class。
我們這節(jié)內(nèi)容的重點在于一個類是如何被加載的,所以我們從類的加載入口開始。
類的加載是由虛擬機觸發(fā)的,類的加載入口位于ClassLoader的loadClassInternal()方法:
// This method is invoked by the virtual machine to load a class.
private Class<?> loadClassInternal(String name)
throws ClassNotFoundException
{
// For backward compatibility, explicitly lock on 'this' when
// the current class loader is not parallel capable.
if (parallelLockMap == null) {
synchronized (this) {
return loadClass(name);
}
} else {
return loadClass(name);
}
}
這段方法還有段注釋說明:這個方法由虛擬機調(diào)用用來加載一個類。我們看到這個類的內(nèi)部最后調(diào)用了loadClass()方法。那我們進入loadClass()方法看看:
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
loadClass()方法方法內(nèi)部調(diào)用了loadClass()的重載方法:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
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.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
loadClass()方法大概做了以下工作:
首先查找該類是否已經(jīng)被加載.
如果該ClassLoader有父加載器,那么調(diào)用父加載器的loadClass()方法.
如果沒有父加載器,則調(diào)用findBootstrapClassOrNull()方法進行加載,該方法會使用引導類加載器進行加載。普通類是不會被該加載器加載到的,所以這里一般返回null.
如果前面的步驟都沒找到,那調(diào)用自身的findClass()方法進行查找。
好,ClassLoader的findClass()方法是個空方法,所以這個過程一般是由子加載器實現(xiàn)的。Java的加載器這么設計是有一定的淵源的,感興趣的讀者可以自行查找書籍了解。
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
在Android中,ClassLoader的直接子類是BaseDexClassLoader,我們看一下BaseDexClassLoader的findClass()實現(xiàn):
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
Tips: 有需要虛擬機以及類加載器全套代碼的,請使用以下命令克隆:
git clone https://android.googlesource.com/platform/dalvik-snapshot
相關代碼位于項目的ics-mr1分支上。
看到這里我們可以知道,Android中類的查找是通過這個pathList進行查找的,而pathList又是個什么鬼呢?
在BaseDexClassLoader中聲明了以下變量:
/** structured lists of path elements */
private final DexPathList pathList;
所以我們可以看看DexPathList的findClass()方法做了什么:
public Class findClass(String name) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
return clazz;
}
}
}
return null;
}
這里通過遍歷dexElements中的Element對象進行查找,最終走的是DexFile的loadClassBinaryName()方法:
public Class loadClassBinaryName(String name, ClassLoader loader) {
return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);
到此為止,我們就將一個類真正的加載過程梳理完了。