热修复探究(一)
来源:互联网 发布:在线c语言代码编辑器 编辑:程序博客网 时间:2024/04/30 10:42
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 URLsprivate final ZipFile[] mZips; // source zip files, with resourcesprivate final DexFile[] mDexs; // opened, prepped DEX filesprotected 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
@Overrideprotected 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的实现的差别,然后调整反射调用的代码。
具体可以看Github上面的两个开源库:
https://github.com/bunnyblue/DroidFix
https://github.com/dodola/RocooFix
这里的代码都是取自那里,这篇文章分析热修复加载dex的部分,下篇会分析,CLASS_ISPREVERIFIED的实现,即class对比和patch的生成。
关于6.X的,有看到源代码,发现和5.X的基本一样,不明白作者为什么在V19和V23使用了两种不同的反射方式去实现,可能是在实践中踩出来的经验吧。
- 热修复探究(一)
- 热修复探究(二)
- 热修复探究(三)
- 热修复技术探究
- Android热修复学习(一)
- android热修复框架Tinker(一)
- Android热修复技术(一)
- iOS之动态热修复技术JSPatch(一)
- Android热修复(Hot Fix)案例全剖析(一)
- MultiDex与热修复实现原理(一)ClassLoader原理
- Android热修复(一):AndFix的使用
- Android热补丁动态修复技术(一):从Dex分包原理到热补丁
- Android热补丁动态修复技术(一):从Dex分包原理到热补丁
- Android热补丁动态修复技术(一):从Dex分包原理到热补丁
- Android热补丁动态修复技术(一):从Dex分包原理到热补丁
- Android 热修复框架 AndFix (一)
- Android 热修复框架 AndFix (一)
- Android 热修复框架 Tinker ( 一 )
- javascript-面向对象
- 单点系统的高可用与性能优化
- iOS笔记
- Java ConcurrentModificationException异常原因和解决方法
- Hadoop 基础知识---之MapReduce篇
- 热修复探究(一)
- Ethervane StopWatch秒表
- 学习技术的三部曲:WHAT、HOW、WHY
- 2016多校联训第二场 Keep On Movin hdoj5744
- Hust oj 1394 XianGe的游戏I(水题)
- h5页面上拉加载下拉刷新问题
- VMware Workstation虚拟机与主机ping不通?
- JQuery学习之旅(7)
- 爬楼梯(leetcode-70)