熱修復探究(一)
Android ClassLoader
前言
這次部落格會分兩篇,這篇介紹各個Android版本是怎麼反射載入生成的patch檔案的,下篇會詳細的分析class對比和patch的生成。
寫這次文章的原因是因為最近在研究熱修復,發現其實他們實現的程式碼很少,其實就一個類,然後裡面針對不同的版本做反射處理,就想好好找找不同版本的對於類載入的機制。
其次呢,關於bug版本和修復版本的class檔案對比,dex的patch檔案生成的指令碼也想了解一下。
區別
首先Android中載入類一般使用的是PathClassLoader
和DexClassLoader
。
區別:
PathClassLoader
- Android uses this class for its system class loader and for its application class
loader(s).
可以看出,Android是使用這個類作為其系統類和應用類的載入器。並且對於這個類呢,只能去載入已經安裝到Android系統中的apk檔案。
DexClassLoader
- A class loader that loads classes from {@code .jar} and {@code .apk} files
containing a {@code classes.dex} entry. This can be used to execute code not
installed as part of an application.
可以看出,該類可以用來從.jar和.apk型別的檔案內部載入classes.dex檔案。可以用來執行非安裝的程式程式碼。
DexClassLoader
建構函式:DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)
- dexPath:被解壓的dex路徑,不能為空。
- optimizedDirectory:解壓後的.dex檔案的儲存路徑,不能為空。這個路徑強烈建議使用應用程式的私有路徑,不要放到sdcard上,否則程式碼容易被注入攻擊。
- libraryPath:so庫的存放路徑,可以為空,若有so庫,必須填寫。
- parent:父親載入器,一般為context.getClassLoader(),使用當前上下文的類載入器。
不同版本的不同實現
我們會發現,根本找不到原始碼,那其實程式碼在android_libcore裡面。
https://github.com/Evervolv/android_libcore
這個庫裡面可以找到不同版本的lib_core的實現。
Gingerbread 2.3 9
首先看Gingerbread的實現,在2.3的時候PathClassLoader和DexClassLoader是分別實現的。著重看findClass方法。
PathClassLoader
private final String path;
private final String[] mPaths;
private final File[] mFiles;
private final ZipFile[] mZips;
private final DexFile[] mDexs;
@Override protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data = null;
int length = mPaths.length;
for (int i = 0; i < length; i++) {
if (mDexs[i] != null) {
Class clazz = mDexs[i].loadClassBinaryName(name, this);
if (clazz != null)
return clazz;
} else if (mZips[i] != null) {
String fileName = name.replace('.', '/') + ".class";
data = loadFromArchive(mZips[i], fileName);
} else {
File pathFile = mFiles[i];
if (pathFile.isDirectory()) {
String fileName = mPaths[i] + "/" +
name.replace('.', '/') + ".class";
data = loadFromDirectory(fileName);
}
}
}
throw new ClassNotFoundException(name + " in loader " + this);
}
- The entries of the second list should be directories containing
- native library files. Both lists are separated using the
- character specified by the “path.separator” system property,
- which, on Android, defaults to “:”.
裡面有這段內容,就是說,dex檔案路徑儲存在path裡面,以分隔符分開,在Android裡面分隔符是”:”。
這裡其實只能對mDexs[]處理,其餘的zip,files並不能處理,後面有註釋說明,詳細的可以看原始碼。
DexClassLoader
private final File[] mFiles; // source file Files, for rsrc URLs
private final ZipFile[] mZips; // source zip files, with resources
private final DexFile[] mDexs; // opened, prepped DEX files
protected Class<?> findClass(String name) throws ClassNotFoundException {
int length = mFiles.length;
for (int i = 0; i < length; i++) {
if (mDexs[i] != null) {
String slashName = name.replace('.', '/');
Class clazz = mDexs[i].loadClass(slashName, this);
if (clazz != null) {
if (VERBOSE_DEBUG)
System.out.println(" found");
return clazz;
}
}
}
throw new ClassNotFoundException(name + " in loader " + this);
}
可以看到也只是可以從載入進來的dex檔案裡面找Class。
所以,熱修復對他的反射處理如下:
/**
* Installer for platform versions 4 to 13.
*/
private static final class V4 {
private static void install(ClassLoader loader, List<File> additionalClassPathEntries)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, IOException {
/* The patched class loader is expected to be a descendant
* of dalvik.system.DexClassLoader. We modify its fields mPaths,
* mFiles, mZips and mDexs to append additional DEX file entries.
*/
int extraSize = additionalClassPathEntries.size();
Field pathField = findField(loader, "path");
// 舊的path
StringBuilder path = new StringBuilder((String) pathField.get(loader));
String[] extraPaths = new String[extraSize];
File[] extraFiles = new File[extraSize];
ZipFile[] extraZips = new ZipFile[extraSize];
DexFile[] extraDexs = new DexFile[extraSize];
for (ListIterator<File> iterator = additionalClassPathEntries.listIterator();
iterator.hasNext(); ) {
File additionalEntry = iterator.next();
// 新增新的dex檔案路徑到path裡面
String entryPath = additionalEntry.getAbsolutePath();
path.append(':').append(entryPath);
int index = iterator.previousIndex();
extraPaths[index] = entryPath; // paths
extraFiles[index] = additionalEntry; // files
extraZips[index] = new ZipFile(additionalEntry); // zipfiles
extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0); // dexfiles
}
// 重新設定path
pathField.set(loader, path.toString());
// 重新設定mPaths,mFiles,mZips,mDexs
expandFieldArray(loader, "mPaths", extraPaths);
expandFieldArray(loader, "mFiles", extraFiles);
expandFieldArray(loader, "mZips", extraZips);
expandFieldArray(loader, "mDexs", extraDexs);
}
}
實現即是這樣的,其實很簡單啦,就是把patch的dex檔案路徑加到path裡面,再使用反射修改掉裡面的四個欄位,重新賦值。
Ice 4.0 14
然後Ice的實現,看在4.0的具體實現。在4.0以後DexClassLoader和PathClassLoader都繼承了BaseDexClassLoader,處理的程式碼都在BaseDexClassLoader裡面。
BaseDexClassLoader
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
程式碼很簡潔,可以看到findClass是使用的DexPathList的例項,pathList去找到對應的class。
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裡面尋找,所以需要使用反射修改掉dexElements,即dex陣列檔案。
相關實現如下:
/**
* Installer for platform versions 14, 15, 16, 17 and 18.
*/
private static final class V14 {
private static void install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
/* The patched class loader is expected to be a descendant
of dalvik.system.BaseDexClassLoader. We modify its
dalvik.system.DexPathList pathList field to append additional
DEX file entries.
*/
Field pathListField = findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
expandFieldArray(dexPathList, "dexElements",
makeDexElements(dexPathList,
new ArrayList<File>(additionalClassPathEntries),
optimizedDirectory));
}
/**
* A wrapper around {@code private static final
dalvik.system.DexPathList#makeDexElements}.
*/
private static Object[] makeDexElements(
Object dexPathList, ArrayList<File> files, File
optimizedDirectory)
throws IllegalAccessException, InvocationTargetException,
NoSuchMethodException {
Method makeDexElements =
findMethod(dexPathList, "makeDexElements",
ArrayList.class, File.class);
return (Object[]) makeDexElements.invoke
(dexPathList, files, optimizedDirectory);
}
}
實現也挺簡單,直接把我們要加的dex檔案新增dexElements陣列的最前面即可。
kitkat 4.4 19
BaseDexClassLoader
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;
}
DexPathList
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;
}
可以看到只是新增了一些異常,合成dex檔案的時候需要傳遞多一個引數用來儲存找尋每個dex檔案發生異常IO資訊。
對應的處理:
/**
* Installer for platform versions 19.
*/
private static final class V19 {
private static void install(ClassLoader loader, List<File>
additionalClassPathEntries, File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException,
NoSuchMethodException {
/* The patched class loader is expected to be a descendant of
* dalvik.system.BaseDexClassLoader. We modify its
* dalvik.system.DexPathList pathList field to append additional
* DEX file entries.
*/
Field pathListField = findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new
ArrayList<IOException>();
expandFieldArray(dexPathList, "dexElements",
makeDexElements(dexPathList,
new ArrayList<File>(additionalClassPathEntries),
optimizedDirectory, suppressedExceptions));
if (suppressedExceptions.size() > 0) {
for (IOException e : suppressedExceptions) {
Log.w(TAG, "Exception in makeDexElement", e);
}
Field suppressedExceptionsField =
findField(dexPathList, "dexElementsSuppressedExceptions");
IOException[] dexElementsSuppressedExceptions =
(IOException[]) suppressedExceptionsField.get(dexPathList);
if (dexElementsSuppressedExceptions == null) {
dexElementsSuppressedExceptions =
suppressedExceptions.toArray(
new IOException[suppressedExceptions.size()]);
} else {
IOException[] combined =
new IOException[suppressedExceptions.size() +
dexElementsSuppressedExceptions.length];
suppressedExceptions.toArray(combined);
System.arraycopy(dexElementsSuppressedExceptions, 0, combined,
suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
dexElementsSuppressedExceptions = combined;
}
suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions);
}
}
/**
* A wrapper around
* {@code private static final
dalvik.system.DexPathList#makeDexElements}.
*/
private static Object[] makeDexElements(
Object dexPathList, ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions)
throws IllegalAccessException, InvocationTargetException,
NoSuchMethodException {
Method makeDexElements =
findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
ArrayList.class);
return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
suppressedExceptions);
}
}
makeDexElements方法作了反射呼叫,並且如果找尋我們自己的新增dex出了問題也會去把做異常處理,最終會對dexElementsSuppressedExceptions這個異常陣列做處理。
5.X的這部分程式碼並沒有什麼差別。
關於6.0和7.0的,這裡並沒有程式碼,現在就不分析,找到程式碼會繼續分析。其實思路很簡單,一個版本一個版本的去對比findClass的實現的差別,然後調整反射呼叫的程式碼。
這裡的程式碼都是取自那裡,這篇文章分析熱修復載入dex的部分,下篇會分析,CLASS_ISPREVERIFIED的實現,即class對比和patch的生成。
關於6.X的,有看到原始碼,發現和5.X的基本一樣,不明白作者為什麼在V19和V23使用了兩種不同的反射方式去實現,可能是在實踐中踩出來的經驗吧。