Android dex分包原理介紹
QQ空間熱修復方案基于Android dex分包基礎之上,簡單概述android dex分包的原理就是:就是把多個dex文件塞入到app的classloader之中,但是android dex拆包方案中的類是沒有重復的,如果classes.dex和classes1.dex中有重復的類,當classes.dex和classes1.dex中都具有同一個類的時候,那么classloader會選擇加載哪個類呢?這要從classloader的源碼入手,加載類是通過classloader的loadClass方法實現的,所以我們看一下loadClass的源碼:
/**
* Loads the class with the specified name. Invoking this method is * equivalent to calling {@code loadClass(className, false)}.
* <p>
* <strong>Note:</strong> In the Android reference implementation, the * second parameter of {@link * anyway.
* </p>
*
* @return the {@code Class} object.
* @param className
* the name of the class to look for.
* @throws ClassNotFoundException
* if the class can not be found.
*/
public Class<?> loadClass(String className) throws ClassNotFoundException { return loadClass(className, false);
}
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className); if (clazz == null) {
ClassNotFoundException suppressed = null; try {
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
suppressed = e;
} if (clazz == null) { try {
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed); throw e;
}
}
} return clazz;
}
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
簡單來說就是ClassLoader用loadClass方法調用了findClass方法,點進去發現findClass是抽象方法,而這個方法的實現是在它的子類BaseDexClassLoader中,而BaseDexClassLoader重載了這個方法,得到BaseDexClassLoader,進入到BaseDexClassLoader類的findClass方法中
#BaseDexClassLoader @Override protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name); if (clazz == null) { throw new ClassNotFoundException(name);
} return clazz;
} #DexPathList 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;
} #DexFile public Class loadClassBinaryName(String name, ClassLoader loader) { return defineClass(name, loader, mCookie);
} private native static Class defineClass(String name, ClassLoader loader, int cookie);
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
一個ClassLoader可以包含多個dex文件,每個dex文件是一個Element,多個dex文件排列成一個有序的數組dexElements,當找類的時候,會按順序遍歷dex文件,然后從當前遍歷的dex文件中找類,如果找類則返回,如果找不到從下一個dex文件繼續查找。
理論上,如果在不同的dex中有相同的類存在,那么會優先選擇排在前面的dex文件的類
所以,QQ空間正是基于ClassLoader的這個原理,把有問題的類打包到一個dex(patch.dex)中去,然后把這個dex插入到Elements的前面
關于如何進行dex分包后面再單獨開一篇博客進行分析。
CLASS_ISPREVERIFIED的問題
采用dex分包方案會遇到的問題,也就是CLASS_ISPREVERIFIED的問題,簡單來概括就是:
在虛擬機啟動的時候,當verify選項被打開的時候,如果static方法、private方法、構造函數等,其中的直接引用(層關系)到的類都在同一個dex文件中,那么該類就會被打上CLASS_ISPREVERIFIED標志。
那么,我們要做的就是,阻止該類打上CLASS_ISPREVERIFIED的標志。
注意下,是阻止引用者的類,也就是說,假設你的app里面有個類叫做AClass,再其內部引用了BClass。發布過程中發現BClass有編寫錯誤,那么想要發布一個新的BClass類,那么你就要阻止AClass這個類打上CLASS_ISPREVERIFIED的標志。
也就是說,你在生成apk之前,就需要阻止相關類打上CLASS_ISPREVERIFIED的標志了。如何阻止,簡單來說,讓AClass在構造方法中,去引用別的dex文件,比如:C.dex中的某個類即可。
所以總結下來,防止這個錯誤,只需要:
1、動態改變BaseDexClassLoader對象間接引用的dexElements;2、在app打包的時候,阻止相關類去打上CLASS_ISPREVERIFIED標志。
熱修復框架HotFix解析
采用QQ空間的熱修復方案而實現的開源熱修復框架就是HotFix,說到了使用dex分包方案會遇到CLASS_ISPREVERIFIED問題,而解決方案就是在dx工具執行之前,將所有的class文件,進行修改,再其構造中添加System.out.println(dodola.hackdex.AntilazyLoad.class),然后繼續打包的流程。注意:AntilazyLoad.class這個類是獨立在hack.dex中。
dex分包方案實現需要關注以下問題:
1.如何解決CLASS_ISPREVERIFIED問題
2.如何將修復的.dex文件插入到dexElements的前面
那么如何達到這個目的呢?在HotFix中采用的javassist來達到這個目的,以下是HotFix中的PatchClass.groovy代碼
public class PatchClass { /**
* 植入代碼
* @param buildDir 是項目的build class目錄,就是我們需要注入的class所在地
* @param lib 這個是hackdex的目錄,就是AntilazyLoad類的class文件所在地
*/ public static void process(String buildDir, String lib) {
println(lib)
ClassPool classes = ClassPool.getDefault()
classes.appendClassPath(buildDir)
classes.appendClassPath(lib) CtClass c = classes.getCtClass("dodola.hotfix.BugClass") if (c.isFrozen()) {
c.defrost()
}
println("====添加構造方法====")
def constructor = c.getConstructors()[0];
constructor.insertBefore("System.out.println(dodola.hackdex.AntilazyLoad.class);")
c.writeFile(buildDir)
CtClass c1 = classes.getCtClass("dodola.hotfix.LoadBugClass") if (c1.isFrozen()) {
c1.defrost()
}
println("====添加構造方法====")
def constructor1 = c1.getConstructors()[0];
constructor1.insertBefore("System.out.println(dodola.hackdex.AntilazyLoad.class);")
c1.writeFile(buildDir)
} static void growl(String title, String message) {
def proc = ["osascript", "-e", "display notification \"${message}\" with title \"${title}\""].execute() if (proc.waitFor() != 0) {
println "[WARNING] ${proc.err.text.trim()}" }
}
}
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
-
38
-
39
-
40
-
41
-
42
-
43
-
44
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
-
38
-
39
-
40
-
41
-
42
-
43
-
44
其實內部做的邏輯就是:通過ClassPool對象,然后添加classpath。然后從classpath中找到LoadBugClass,拿到其構造方法,在其中插入一行代碼。
到這里插入代碼的操作已經完成,但是還存在另外一個問題,那就是如何在dx之前去進行上述腳本的操作?
答案就在HotFix的app/build.gradle中
apply plugin: 'com.android.application' task('processWithJavassist') << {
String classPath = file('build/intermediates/classes/debug')//項目編譯class所在目錄 dodola.patch.PatchClass.process(classPath, project(':hackdex').buildDir
.absolutePath + '/intermediates/classes/debug')//第二個參數是hackdex的class所在目錄 }
buildTypes {
debug {
minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' }
release {
minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' }
}
applicationVariants.all { variant -> variant.dex.dependsOn << processWithJavassist //在執行dx命令之前將代碼打入到class中 }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
可以看到在build.gradle中,在執行dx之前,會先執行processWithJavassist這個任務。這樣會執行PatchClass.groovy的腳本,在構造方法中進行注入
將修復的.dex文件插入dexElements
尋找class是遍歷dexElements;然后我們的AntilazyLoad.class實際上并不包含在apk的classes.dex中,并且根據上面描述的需要,我們需要將AntilazyLoad.class這個類打成獨立的hack_dex.jar,注意不是普通的jar,必須經過dx工具進行轉化。
具體做法:
. -- -- . .
還記得之前我們將所有的類的構造方法中都引用了AntilazyLoad.class,所以我們需要把hack_dex.jar插入到dexElements,而在hotfix中,就是在Application中完成這個操作的
ublic 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();
}
}
}
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
在app的私有目錄創建一個文件,然后調用Utils.prepareDex將assets中的hackdex_dex.jar寫入該文件。 Utils.prepareDex中其實就是文件的讀寫操作,注意:前提是你把hackdex_dex.jar放入到assets中
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;
}
}
}
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
接下來HotFix.patch就是去反射去修改dexElements了
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) {
}
}
}
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
可以看到patch方法中有幾個分支,說白了是根據不同的系統中ClassLoader的類型來做相應的處理
private static void injectInAliyunOs(Context context, String patchDexFile, String patchClassName)
throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException,
InstantiationException, NoSuchFieldException {
PathClassLoader obj = (PathClassLoader) context.getClassLoader() String replaceAll = new File(patchDexFile).getName().replaceAll("\\.[a-zA-Z0-9]+", ".lex") Class cls = Class.forName("dalvik.system.LexClassLoader") Object newInstance = cls.getConstructor(new Class[] {String.class, String.class, String.class, ClassLoader.class}).newInstance(
new Object[] {context.getDir("dex", 0).getAbsolutePath() + File.separator + replaceAll,
context.getDir("dex", 0).getAbsolutePath(), patchDexFile, obj}) cls.getMethod("loadClass", new Class[] {String.class}).invoke(newInstance, new Object[] {patchClassName}) setField(obj, PathClassLoader.class, "mPaths",
appendArray(getField(obj, PathClassLoader.class, "mPaths"), getField(newInstance, cls, "mRawDexPath"))) setField(obj, PathClassLoader.class, "mFiles",
combineArray(getField(obj, PathClassLoader.class, "mFiles"), getField(newInstance, cls, "mFiles"))) setField(obj, PathClassLoader.class, "mZips",
combineArray(getField(obj, PathClassLoader.class, "mZips"), getField(newInstance, cls, "mZips"))) setField(obj, PathClassLoader.class, "mLexs",
combineArray(getField(obj, PathClassLoader.class, "mLexs"), getField(newInstance, cls, "mDexs"))) }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
上述方法中的LexClassLoader應該是阿里自己的ClassLoader,可以看到上面將修復的文件的結尾都換成了.lex的結尾,這些文件就是專門需要通過LexClassLoader進行加載的
我們分 API 14以上和以下進行分析
API 14以下
private static void injectBelowApiLevel14(Context context, String str, String str2)
throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
PathClassLoader obj = (PathClassLoader) context.getClassLoader() DexClassLoader dexClassLoader =
new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader()) dexClassLoader.loadClass(str2) setField(obj, PathClassLoader.class, "mPaths",
appendArray(getField(obj, PathClassLoader.class, "mPaths"), getField(dexClassLoader, DexClassLoader.class, "mRawDexPath")
)) setField(obj, PathClassLoader.class, "mFiles",
combineArray(getField(obj, PathClassLoader.class, "mFiles"), getField(dexClassLoader, DexClassLoader.class, "mFiles")
)) setField(obj, PathClassLoader.class, "mZips",
combineArray(getField(obj, PathClassLoader.class, "mZips"), getField(dexClassLoader, DexClassLoader.class, "mZips"))) setField(obj, PathClassLoader.class, "mDexs",
combineArray(getField(obj, PathClassLoader.class, "mDexs"), getField(dexClassLoader, DexClassLoader.class, "mDexs"))) obj.loadClass(str2) }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
通過setField方法將mPaths屬性,修改為通過appendArray方法創造的新元素
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);
}
private static Object appendArray(Object obj, Object obj2) {
Class componentType = obj.getClass().getComponentType(); int length = Array.getLength(obj); Object newInstance = Array.newInstance(componentType, length + 1);
Array.set(newInstance, 0, obj2); for (int i = 1; i < length + 1; i++) {
Array.set(newInstance, i, Array.get(obj, i - 1));
} return newInstance;
}
而appendArray中就是創建一個新的Array,把obj2插入到obj的前面,注意這里的obj2長度只有1
所以,在injectBelowApiLevel14的以下方法中,就是把mRawDexPath的元素插入到mPaths中所有元素之前,而重新組合而成的新mPaths替換掉舊的mPaths
setField(obj, PathClassLoader.class, "mPaths", appendArray(getField(obj, PathClassLoader.class, "mPaths"), getField(dexClassLoader, DexClassLoader.class, "mRawDexPath") ));
接下來的替換,是通過combineArray生成的新元素替換掉舊元素,這里分別是mFiles,mZips,mDexs
setField(obj, PathClassLoader.class, "mFiles", combineArray(getField(obj, PathClassLoader.class, "mFiles"), getField(dexClassLoader, DexClassLoader.class, "mFiles") )); setField(obj, PathClassLoader.class, "mZips", combineArray(getField(obj, PathClassLoader.class, "mZips"), getField(dexClassLoader, DexClassLoader.class, "mZips"))); setField(obj, PathClassLoader.class, "mDexs", combineArray(getField(obj, PathClassLoader.class, "mDexs"), getField(dexClassLoader, DexClassLoader.class, "mDexs")));
于是我們需要看一下combineArray方法里面做了什么
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;
}
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
邏輯也很簡單,也就是兩個數組的合并而已
API14以上
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);
}
根據context拿到PathClassLoader,然后通過getPathList(pathClassLoader),拿到PathClassLoader中的pathList對象,在調用getDexElements通過pathList取到dexElements對象。
private static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException { return getField(obj, obj.getClass(), "dexElements");
}
private static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException,
IllegalAccessException { return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}
同樣是通過combineArray方法,對數組進行合并,合并完成后,將新的數組通過反射的方式設置給pathList.
通過上面的一系列流程,那么hack_dex.jar已經插入到dexElements前面了,補丁插入的過程也和hack_dex.jar的插入流程是一致的
到這里,dex分包方案實現熱修復的HotFix的分析就已經完畢了。
本站文章版權歸原作者及原出處所有 。內容為作者個人觀點, 并不代表本站贊同其觀點和對其真實性負責,本站只提供參考并不構成任何投資及應用建議。本站是一個個人學習交流的平臺,網站上部分文章為轉載,并不用于任何商業目的,我們已經盡可能的對作者和來源進行了通告,但是能力有限或疏忽,造成漏登,請及時聯系我們,我們將根據著作權人的要求,立即更正或者刪除有關內容。本站擁有對此聲明的最終解釋權。