熱修復(fù)技術(shù)自從QQ空間團(tuán)隊(duì)搞出來之后便漸漸趨于成熟。
我們這個(gè)系列主要介紹如何一步步手動(dòng)實(shí)現(xiàn)基本的熱修復(fù)功能澳骤,無需使用第三方框架抖僵。
在開始學(xué)習(xí)之前,需要對(duì)基本的熱修復(fù)技術(shù)有些了解违柏,以下文章可以幫助到你:
- 安卓App熱補(bǔ)丁動(dòng)態(tài)修復(fù)技術(shù)介紹
- 【騰訊Bugly干貨分享】Android Patch 方案與持續(xù)交付
- Android dex分包方案
- dodola/HotFix
一、dex文件的生成與加載
我們?cè)谶@部分主要做的流程有:
- 1.編寫基本的Java文件并編譯為.class文件香椎。
- 2.將.class文件轉(zhuǎn)為.dex文件漱竖。
- 3.將轉(zhuǎn)好的dex文件放入創(chuàng)建好的Android工程內(nèi)并在啟動(dòng)時(shí)將其寫入本地。
- 4.加載解壓后的.dex文件中的類畜伐,并調(diào)用其方法進(jìn)行測(cè)試馍惹。
Note: 在閱讀本節(jié)之前最好先了解一下類加載器的雙親委派模型、DexClassLoader的使用以及反射的知識(shí)點(diǎn)玛界。
編寫基本的Java文件并編譯為.class文件
首先我們?cè)谝粋€(gè)工程目錄下開始創(chuàng)建并編寫我們的Java文件万矾,你可能會(huì)選擇各種IDE來做這件事,但我在這里勸你不要這么做慎框,因?yàn)橛锌釉诘饶懔急贰5劝鸦玖鞒谈闱宄梢栽龠x擇更進(jìn)階的方法。這里我們可以選擇文本編輯器比如EditPlus來對(duì)Java文件進(jìn)行編輯笨枯。
新建一個(gè)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: 這里要注意馅精,不要對(duì)類添加包名严嗜,因?yàn)樵诤笃趯?duì)class文件處理時(shí)會(huì)遇到問題,具體問題會(huì)稍后說明洲敢。上面的getName方法在返回時(shí)對(duì)this.name屬性添加了一段字符串漫玄,這里請(qǐng)注意,后面會(huì)用到压彭。
在文件創(chuàng)建好之后睦优,對(duì)Java文件進(jìn)行編譯:

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

Tips: 為了方便使用忆畅,建議將dx的路徑添加到環(huán)境變量中。如果對(duì)dx工具不熟悉的,可以在終端中輸入dx --help以獲取幫助家凯。
dx工具的基本用法是:
dx --dex [--output=<file>] [<file>.class | <file>.{zip,jar,apk} | <directory>]
Tips: 剛開始自己摸索的時(shí)候缓醋,就沒有仔細(xì)看命令,導(dǎo)致后面兩個(gè)參數(shù)的順序顛倒了绊诲,搞出了一些讓人疑惑難解的問題送粱,最后又不得不去找dx工具的源碼調(diào)試,最后才發(fā)現(xiàn)自己的問題在哪掂之。如果有對(duì)dx工具感興趣的抗俄,可以對(duì)dx的包進(jìn)行反編譯或者獲取dx的相關(guān)源代碼進(jìn)行了解。dx.lib文件位于dx.bat的下級(jí)目錄lib文件夾中世舰,可以使用JD-GUI工具對(duì)其進(jìn)行查看或?qū)С龆ⅰH绻枰@取源代碼的,請(qǐng)使用以下命令進(jìn)行克赂埂:
我們使用以下命令生成dex文件:
dx --dex --output=user.dex com.sahadev.bean.ClassStudent.class
這里我為了防止出錯(cuò)胰蝠,提前在當(dāng)前目錄下新建好了user.dex文件。上述命令依賴編譯.class文件的JDK版本震蒋,如果使用的是JDK8編譯的class會(huì)提示以下問題:
PARSE ERROR:
unsupported class file version 52.0
...while parsing com.sahadev.bean.ClassStudent.class
1 error; aborting
這里的52.0意味著class文件不被支持茸塞,需要使用JDK8以下的版本進(jìn)行編譯,但是dx所需的環(huán)境還是需要為JDK8的查剖,這里我編譯class文件使用的是JDK7,請(qǐng)注意钾虐。
上面我們提到了為什么先不要在ClassStudent中使用包名,因?yàn)樵趫?zhí)行dx的時(shí)候會(huì)報(bào)以下異常梗搅,這是因?yàn)橐韵碌诙?xiàng)條件沒有通過禾唁,該代碼位于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 + ")");
}
運(yùn)行截圖如下所示:

好了,到此為止我們的目錄應(yīng)該如下:

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

在系統(tǒng)啟動(dòng)時(shí)將其寫入到磁盤无切,這里不再貼出具體的寫入代碼荡短,項(xiàng)目的MainActivity中包含了此部分代碼。
加載dex中的類并測(cè)試
在寫入完畢之后使用DexClassLoader對(duì)其進(jìn)行加載哆键。DexClassLoader的構(gòu)造方法需要4個(gè)參數(shù)掘托,這里對(duì)這4個(gè)參數(shù)進(jìn)行簡(jiǎn)要說明:
- String dexPath:dex文件的絕對(duì)路徑。在這里我將其放入了應(yīng)用的cache文件夾下籍嘹。
- String optimizedDirectory:優(yōu)化后的dex文件存放路徑闪盔。DexClassLoader在構(gòu)造完畢之后會(huì)對(duì)原有的dex文件優(yōu)化并生成一個(gè)新的dex文件,在這里我選擇的是.../cache/optimizedDirectory/目錄辱士。此外泪掀,API文檔對(duì)該目錄有嚴(yán)格的說明:Do not cache optimized classes on external storage.出于安全考慮,請(qǐng)不要將優(yōu)化后的dex文件放入外部存儲(chǔ)器中颂碘。
- String libraryPath:dex文件所需要的庫文件路徑异赫。這里沒有依賴,使用空字符串代替。
- ClassLoader parent:雙親委派模型中提到的父類加載器塔拳。這里我們使用默認(rèn)的加載器鼠证,通過getClassLoader()方法獲得。
在解釋完畢DexClassLoader的構(gòu)造參數(shù)之后靠抑,我們開始對(duì)剛剛的dex文件進(jìn)行加載:
DexClassLoader dexClassLoader = new DexClassLoader(apkPath, file.getParent() + "/optimizedDirectory/", "", classLoader);
接來下開始load我們剛剛寫入在dex文件中的ClassStudent類:
Class<?> aClass = dexClassLoader.loadClass("com.sahadev.bean.ClassStudent");
然后我們對(duì)其進(jìn)行初始化量九,并調(diào)用相關(guān)的get/set方法對(duì)其進(jìn)行驗(yàn)證,在這里我傳給ClassStudent對(duì)象一個(gè)字符串颂碧,然后調(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););
最后我們實(shí)現(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();
}
}
最后附上我們的運(yùn)行截圖:

二荠列、Class文件的替換
在完成基本外部類加載之后,我們這一節(jié)開始對(duì)工程內(nèi)部的類實(shí)行替換稚伍。
Tips: 本章主要依賴文章http://blog.csdn.net/vurtne_ye/article/details/39666381中的未實(shí)現(xiàn)代碼實(shí)現(xiàn)弯予,實(shí)現(xiàn)思路也源自該文章,在閱讀本文之前可以先行了解个曙。
這一節(jié)我們主要實(shí)現(xiàn)的流程有:
- 在類的內(nèi)部創(chuàng)建與外部dex相同的類文件锈嫩,但在調(diào)用getName()方法返回字符串時(shí)會(huì)稍有區(qū)別,以便進(jìn)行結(jié)果驗(yàn)證
- 使用DexClassLoader加載外部的user.dex
- 將DexClassLoader中的dexElements放在PathClassLoader的dexElements之前
- 驗(yàn)證替換結(jié)果
因?yàn)樯瞎?jié)課中專門聲明了不可以對(duì)類聲明包名垦搬,但是這樣在Android工程中無法引用沒有包名的類呼寸,所以把不能聲明包名的問題解決了一下。
上一節(jié)課主要遇到的問題是在編譯Java文件時(shí)沒有使用正當(dāng)?shù)拿詈锓 ?duì)含有包名的Java文件應(yīng)當(dāng)使用以下命令:
javac -d ./ ClassStudent.java
經(jīng)過上面命令編譯后的.class文件便可以順利通過dx工具的轉(zhuǎn)換对雪。
我們還是按照第一節(jié)的步驟將轉(zhuǎn)換后的user.dex文件放入工程中并寫入本地磁盤,以便稍后使用米绕。
在開始之前還是先說一下具體的實(shí)現(xiàn)思路:一個(gè)類在使用之前必須要經(jīng)過加載器的加載才能使用瑟捣,在加載器加載類之前會(huì)調(diào)用自身的findClass()方法進(jìn)行查找。然而在Android中類的查找使用的是BaseDexClassLoader栅干,BaseDexClassLoader對(duì)findClass()方法進(jìn)行了重寫:
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
pathList是類DexPathList的實(shí)例迈套,這里pathList.findClass的實(shí)現(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來進(jìn)行查找的。所以為了實(shí)現(xiàn)替換效果碱鳞,我們需要將DexClassLoader中的Element對(duì)象放到dexElements數(shù)組的第0個(gè)位置桑李,這樣才能在BaseDexClassLoader查找類時(shí)先找到DexClassLoader所用的user.dex中的類。
Tips: 如果對(duì)上面這句話看不懂的窿给,沒關(guān)系贵白,可以先了解一下類的加載機(jī)制與ClassLoader的雙親委派模型。
好了崩泡,有了基本的實(shí)現(xiàn)思路之后禁荒,我們接下來對(duì)思路進(jìn)行實(shí)踐。
下面的方法是我們主要的注入方法:
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";
}
這段代碼原封不動(dòng)采用于http://blog.csdn.net/vurtne_ye/article/details/39666381文章中最后的實(shí)現(xiàn)代碼角撞,但是該文章并沒有給出具體的注入細(xì)節(jié)呛伴。我們接下里的過程就是對(duì)沒有給全的細(xì)節(jié)進(jìn)行補(bǔ)充與講解寥掐。
這段代碼的核心在于將DexClassLoader中的dexElements與PathClassLoader中的dexElements進(jìn)行合并,然后將合并后的dexElements替換原先的dexElements磷蜀。最后我們?cè)谑褂肅lassStudent類的時(shí)候便可以直接使用外部的ClassStudent,而不會(huì)再使用默認(rèn)的ClassStudent類百炬。默認(rèn)情況下會(huì)加載默認(rèn)的ClassStudent類褐隆。
首先我們通過classLoader獲取各自的pathList對(duì)象:
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;
}
在使用以上反射的時(shí)候要注意,pathList屬性屬于基類BaseDexClassLoader剖踊。所以如果直接對(duì)DexClassLoader或者PathClassLoader獲取pathList屬性的話庶弃,會(huì)得到null。
其次是獲取pathList對(duì)應(yīng)的dexElements德澈,這里要注意dexElements是個(gè)數(shù)組對(duì)象:
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;
}
接下來我們將兩個(gè)數(shù)組對(duì)象合并成為一個(gè):
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ù)組對(duì)象的類型創(chuàng)建了一個(gè)新的大小為2的新數(shù)組歇攻,并將兩個(gè)數(shù)組的第一個(gè)元素取出,將dex中的dexElement放在了第0個(gè)位置梆造。這樣可以確保在查找類時(shí)優(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();
}
}
運(yùn)行驗(yàn)證結(jié)果:
大功告成!
類的加載機(jī)制簡(jiǎn)要介紹
一個(gè)類在被加載到內(nèi)存之前要經(jīng)過加載镇辉、驗(yàn)證屡穗、準(zhǔn)備等過程。經(jīng)過這些過程之后忽肛,虛擬機(jī)才會(huì)從方法區(qū)將代表類的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換為內(nèi)存中的Class村砂。
我們這節(jié)內(nèi)容的重點(diǎn)在于一個(gè)類是如何被加載的,所以我們從類的加載入口開始屹逛。
類的加載是由虛擬機(jī)觸發(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);
}
}
這段方法還有段注釋說明:這個(gè)方法由虛擬機(jī)調(diào)用用來加載一個(gè)類。我們看到這個(gè)類的內(nèi)部最后調(diào)用了loadClass()方法罕模。那我們進(jìn)入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()方法進(jìn)行加載手销,該方法會(huì)使用引導(dǎo)類加載器進(jìn)行加載歇僧。普通類是不會(huì)被該加載器加載到的,所以這里一般返回null.
如果前面的步驟都沒找到锋拖,那調(diào)用自身的findClass()方法進(jìn)行查找诈悍。
好,ClassLoader的findClass()方法是個(gè)空方法兽埃,所以這個(gè)過程一般是由子加載器實(shí)現(xiàn)的侥钳。Java的加載器這么設(shè)計(jì)是有一定的淵源的,感興趣的讀者可以自行查找書籍了解柄错。
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
在Android中舷夺,ClassLoader的直接子類是BaseDexClassLoader苦酱,我們看一下BaseDexClassLoader的findClass()實(shí)現(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: 有需要虛擬機(jī)以及類加載器全套代碼的,請(qǐng)使用以下命令克隆:
git clone https://android.googlesource.com/platform/dalvik-snapshot
相關(guān)代碼位于項(xiàng)目的ics-mr1分支上给猾。
看到這里我們可以知道疫萤,Android中類的查找是通過這個(gè)pathList進(jìn)行查找的,而pathList又是個(gè)什么鬼呢敢伸?
在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對(duì)象進(jìn)行查找扯饶,最終走的是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);
到此為止,我們就將一個(gè)類真正的加載過程梳理完了池颈。