Android類加載機制html
咱們先來看一下BaseDexClassLoader源碼中比較重要的code
java
能夠看出最終在此處找到了某一個類android
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);複製代碼
按照這個原理,咱們能夠把有問題的類打包到一個dex(patch.dex)中去,而後把這個dex插入到Elements的最前面,當遍歷findClass的時候,咱們修復的類就會被查找到,從而替代有bug的類便可,那麼下面來進行這一個過程的操做吧。編程
新建一個Hotfix的工程,而後新建一個BugClass類數組
package ydc.hotfix;
public class BugClass {
public String bug() {
return "fix bug class";
}
}複製代碼
在新建一個LoadBugClass類安全
public class LoadBugClass {
public String getBugString() {
BugClass bugClass = new BugClass();
return bugClass.bug();
}
}複製代碼
注意 LoadBugClass應用了BugClass類。bash
而後在界面層是這樣調用的:微信
ok,假設咱們把該apk發佈出去了,那麼用戶看到效果應該是「 測試調用方法:fix bug class」。這個時候公司領導認爲這樣的提示對於用戶是致命的。那麼咱們要把BugClass 類中的bug()方法中字符串替換一下,僅僅是修復一句話而已,實在沒有必要走打包發佈下放市場等複雜的流程。app
public String bug() {
return "fix bug class";
}複製代碼
ok,把這個有問題的地方修正爲:eclipse
public String bug() {
return "楊德成正在修復提示語fix bug class";
}複製代碼
三、先把BugClass.class文件作成成jar,注意路徑,必定要定位到該位置執行如下命令:
jar cvf path.jar ydc/hotfix/BugClass.class複製代碼
依然在該路徑下執行如下命令:
dx --dex --output=path_dex.jar path.jar
五、拷貝path_dex
咱們把path_dex文件拷貝到assets目錄下
首先咱們看一下hotfix的源碼:
根據截圖所示,作了兩個動做複製代碼
a、建立一個私有目錄,並把補丁包文件寫入到該目錄下
File dexPath = new File(getDir(「dex」, Context.MODE_PRIVATE), 「path_dex.jar」);複製代碼
public class Utils {
private static final int BUF_SIZE = 2048;
public static boolean prepareDex(Context context, File dexInternalStoragePath, String dex_file) {
BufferedInputStream bis = null;
OutputStream dexWriter = null;
try {
bis = new BufferedInputStream(context.getAssets().open(dex_file));
dexWriter = new BufferedOutputStream(new FileOutputStream(dexInternalStoragePath));
byte[] buf = new byte[BUF_SIZE];
int len;
while ((len = bis.read(buf, 0, BUF_SIZE)) > 0) {
dexWriter.write(buf, 0, len);
}
dexWriter.close();
bis.close();
return true;
} catch (IOException e) {
if (dexWriter != null) {
try {
dexWriter.close();
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
if (bis != null) {
try {
bis.close();
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
return false;
}
}
}複製代碼
public static void patch(Context context, String patchDexFile, String patchClassName) {
if (patchDexFile != null && new File(patchDexFile).exists()) {
try {
if (hasLexClassLoader()) {
injectInAliyunOs(context, patchDexFile, patchClassName);
} else if (hasDexClassLoader()) {
injectAboveEqualApiLevel14(context, patchDexFile, patchClassName);
} else {
injectBelowApiLevel14(context, patchDexFile, patchClassName);
}
} catch (Throwable th) {
}
}
}複製代碼
根據上文提到過的ClassLoader 體系原理,咱們的補丁包應該走的是hasDexClassLoader()分支,該方法代碼以下:
private static boolean hasDexClassLoader() {
try {
Class.forName("dalvik.system.BaseDexClassLoader");
return true;
} catch (ClassNotFoundException e) {
return false;
}
}複製代碼
系統中確定會存在」dalvik.system.BaseDexClassLoader」類,那麼接下來應該進入injectAboveEqualApiLevel14(context, patchDexFile, patchClassName)方法,代碼以下:
private static void injectAboveEqualApiLevel14(Context context, String str, String str2)
throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
Object a = combineArray(getDexElements(getPathList(pathClassLoader)),
getDexElements(getPathList(
new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader()))));
Object a2 = getPathList(pathClassLoader);
setField(a2, a2.getClass(), "dexElements", a);
pathClassLoader.loadClass(str2);
}複製代碼
根據Android系統源碼解讀源以上代碼
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();複製代碼
private static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException,
IllegalAccessException {
return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}複製代碼
根據以上代碼片斷,能夠看出這裏根據引用類名稱」BaseDexClassLoader」查找有個叫」pathList」屬性名的被引用類型。
private static Object getField(Object obj, Class cls, String str)
throws NoSuchFieldException, IllegalAccessException {
Field declaredField = cls.getDeclaredField(str);
declaredField.setAccessible(true);
return declaredField.get(obj);
}複製代碼
上面這個片斷經過反射找到對應的被引用類」DexPathList」,上個」BaseDexClassLoader」系統源碼:
b、getDexElements(getPathList(pathClassLoader))方法解讀:
private static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {
return getField(obj, obj.getClass(), "dexElements");
}複製代碼
上面的這個代碼片斷根據a步驟獲得的DexPathList對象獲取到了沒有打補丁以前的dexElements有序數組對象
private static Object getField(Object obj, Class cls, String str)
throws NoSuchFieldException, IllegalAccessException {
Field declaredField = cls.getDeclaredField(str);
declaredField.setAccessible(true);
return declaredField.get(obj);
}複製代碼
根據代碼可知依然使用反射原理獲取DexPathList對象中的有序數組dexElements。
DexPathList類系統源碼以下:
new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader()))
根據該類的系統源碼看出其實該類的構造函數並無作具體的事情
真正作之情的是它的直接父類BaseDexClassLoader的構造函數,如圖所示
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);複製代碼
看到沒,根據傳入參數初始化了咱們補丁包對應的 DexPathList對象,注意這一步僅僅是初始化哦
private static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException,
IllegalAccessException {
return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}複製代碼
private static Object getField(Object obj, Class cls, String str)
throws NoSuchFieldException, IllegalAccessException {
Field declaredField = cls.getDeclaredField(str);
declaredField.setAccessible(true);
return declaredField.get(obj);
}複製代碼
上面這兩段代碼根據引用名」dalvik.system.BaseDexClassLoader」和被引用類屬性名」pathList」獲得DexPathList對象
private static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {
return getField(obj, obj.getClass(), "dexElements");
}複製代碼
private static Object getField(Object obj, Class cls, String str)
throws NoSuchFieldException, IllegalAccessException {
Field declaredField = cls.getDeclaredField(str);
declaredField.setAccessible(true);
return declaredField.get(obj);
}複製代碼
上面的這兩端片斷根據 DexPathList類及屬性名dexElements獲取到咱們補丁包對應的有序數組dexElements上面已經獲得了兩個有序數組dexElements,一個存放的的是沒有打補丁以前的dex有序數組dexElements,另一個是咱們的補丁包對應的dex有序數組dexElements,那麼是否是到了該合併兩個數組的時候了呢,沒錯
Object a = combineArray(getDexElements(getPathList(pathClassLoader)),
getDexElements(getPathList(
new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader()))));複製代碼
到這裏終於知道這整句代碼到底幹了什麼事情了,Object a 就是咱們合併後的有序dex數組dexElements合併過程以下:
private static Object combineArray(Object obj, Object obj2) {
Class componentType = obj2.getClass().getComponentType();
int length = Array.getLength(obj2);
int length2 = Array.getLength(obj) + length;
Object newInstance = Array.newInstance(componentType, length2);
for (int i = 0; i < length2; i++) {
if (i < length) {
Array.set(newInstance, i, Array.get(obj2, i));
} else {
Array.set(newInstance, i, Array.get(obj, i - length));
}
}
return newInstance;
}複製代碼
其實就是把補丁包對應的dex插入到原來有序數組dexElements的最前面了。
Object a2 = getPathList(pathClassLoader);複製代碼
private static void setField(Object obj, Class cls, String str, Object obj2)
throws NoSuchFieldException, IllegalAccessException {
Field declaredField = cls.getDeclaredField(str);
declaredField.setAccessible(true);
declaredField.set(obj, obj2);
}複製代碼
依然是使用反射機制設置新值。
pathClassLoader.loadClass(str2);複製代碼
str參數是經過如下代碼傳入,即(ydc.hotfix.BugClass)HotFix.patch(this, dexPath.getAbsolutePath(), 「ydc.hotfix.BugClass」);複製代碼
這時候loadClass到的就是咱們補丁包中的BugClass類了,這是由於咱們把補丁包對應的dex文件插入到dexElements最前面。因此找到就BugClass直接返回了,代碼以下:
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;
}複製代碼
按照咱們以前的推論,到這裏應該就完成了補丁動態修復了,那麼真的是這樣的嗎,咱們不防運行下項目看看。
很不幸,運行時報錯:
-這是因爲LoadBugClass引用了BugClass,可是發現這這兩個類所在的dex不在一塊兒,其中:
BugClass在path_dex.jar中結果發生了錯誤。
究其緣由是 pathClassLoader.loadClass(str2)的時候,會去校驗LoadBugClass所在的dex和BugClass所在的dex是不是同一個,不是則會報錯。那麼校驗的前提是有一個叫CLASS_ISPREVERIFIED的類標誌,若是引用者被打上這個標識,就會去校驗,就會致使報錯,那麼咱們能夠想象若是引用者LoadBugClass 沒被打上這個標識,是否就會運行經過了呢,沒錯,就是這個原理。
而後把這個被引用類打包成一個單獨的dex文件。這樣就能夠防止了LoadBugClass類被打上CLASS_ISPREVERIFIED的標誌了,那咱們如今來開始作這件事情。
一、動態被注入類的製做
b、在該Module之下,新建一個AntilazyLoad空類。
```
package dodola.hackdex;
/**
d、依然要把這個dex文件插入到dexElements有序數組的中,插入原理和補丁包插入原理徹底一致,並且這個dex文件須要在程序的入口進行插入,保證它是在有序數組的最前面,由於咱們要把該dex文件中的AntilazyLoad要動態注入到其它包裏面的某一個類的構造方法中。切記,dexElements裏面能夠塞入無數個dex文件。
/**
* Created by sunpengfei on 15/11/4.
*/
public class HotfixApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
File dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "hackdex_dex.jar");
Utils.prepareDex(this.getApplicationContext(), dexPath, "hackdex_dex.jar");
HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hackdex.AntilazyLoad");
try {
this.getClassLoader().loadClass("dodola.hackdex.AntilazyLoad");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}複製代碼
ok,下面就是如何注入的問題了,這個時候應該到了咱們的AOP三劍客之一」javassist」閃亮登場了。
javassist這貨是個好東西啊,它能夠以無侵入的方式重構你的原代碼。我以前編寫過另一個三劍客之一的文章,原理基本同樣。參考地址:blog.csdn.net/xinanheisha…
a、建立buildSrc模塊,這個項目是使用Groovy開發的,聽說這貨具有Java, JavaScript, Phython, Ruby等等語言的優勢,並且Groovy依賴於Java的,和Java無縫掛接的
b、導入javassist
```
apply plugin: 'groovy'
repositories {
mavenCentral()
}
dependencies {
compile gradleApi()
compile 'org.codehaus.groovy:groovy-all:2.3.6'
compile 'org.javassist:javassist:3.20.0-GA'
}
- c、PatchClass 代碼截圖以下

其實很簡單的,這幾句的意思就是經過反射相關類,而後在相關類的構造方法中插入一句輸出語句。複製代碼
CtClass c = classes.getCtClass("ydc.hotfix.BugClass")
if (c.isFrozen()) {
c.defrost()
}
println("====添加構造方法====")
def constructor = c.getConstructors()[0];
constructor.insertBefore("System.out.println(dodola.hackdex.AntilazyLoad.class);")
//constructor.insertBefore("System.out.println(888);")
c.writeFile(buildDir)
執行完這段代碼以後,也無形中應用了AntilazyLoad這個類。
- d、這個工程不須要引用到主app(Module)中,只須要在 app->build.gradle中配置一個任務:

在配置一下,侵入時期

ok,總算把整個過程寫完了,準備開始運行了,無論你激不激動,反正本人是挺激動的了。在運行以前,先看一下咱們的引用者類

沒錯,能夠確認這是咱們的源代碼,化成灰我也能夠認出它來。在看一下運行以後的引用者類

沒錯,就是這個效果,咱們的源碼被javassist 赤裸裸的侵犯了,是否是瞬間覺的本身的「東西」很不安全,這就是AOP編程的強大之處啊。
項目講解到這裏,我想估計沒有幾我的能有耐心的看到這裏來了,由於以爲文章實在太長,須要有多大耐心才能扛到這裏,連我本身也懷疑本身如何寫出來的,不過我認爲,這麼強大並且實用的技術點,不是可以三五兩語就能說清的,咱們要有足夠的耐心來探索咱們所不知的,有耐心,咱們就有但願,有但願就不會失望!
ok,咱們見證一下奇蹟。

看到這效果,我手已累,鍵盤已壞。。。。
>Demo下載地址:
>
> http://download.csdn.net/download/xinanheishao/9902530
演示環境:demo導入不能正常運行,建議先調整環境,跑起來,再進階。複製代碼
classpath 'com.android.tools.build:gradle:1.3.0'
#Thu Jul 13 16:40:06 CST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2-all.zip
```
若是默認的jdk環境找不到,手動指向一下
若是你已經準備好足夠信心的話,能夠按照文章,本身嘗試一方
最後感謝騰訊空間給出的解決方法思路和HotFix開源做者。
原文地址
項目相關:
相關demo下載地址:
若是你以爲此文對您有所幫助,歡迎入羣 QQ交流羣 :232203809
微信公衆號:終端研發部