Android动态加载入坑指南
曾几何时,国内各大公司掀起了一股研究Android动态加载的技术,两年多过去了,动态加载技术俨然成了Android开发中必须掌握的技术。那么动态加载技术是什么呢,这里谈谈我的个人看法,如有雷同,纯属偶然。
什么是动态加载技术
JVM 类加载机制
BootStrapClassLoader
是顶级的类加载器,它是唯一一个不继承自ClassLoader
中的类加载器,它高度集成于 JVM是ExtensionClassLoader
的父加载器,它的类加载路径是JDK\jre\lib
和 用户指定的虚拟机参数-Xbootclasspath
的值。ExtensionClassLoader
是BootStrapClassLoader
的子加载器,同时是SystemClassLoader
(有的地方称AppClassLoader
)的父加载器,它的类加载路径是JDK\jre\lib\ext
和系统属性java.ext.dirs
的值。SystemClassLoader
是ExtensionClassLoader
的子加载器,同时是我们的应用程序的类加载器,我们在应用程序中编写的类一般情况下(如果没有到动态加载技术的话)都是通过这个类加载加载的。它的类加载路径是环境变量CLASSPATH
的值或者用户通过命令行可选项-cp (-classpath)
指定的值。- 类加载器由于父子关系形成树形结构,开发人员可以开发自己的类加载器从而实现动态加载功能,但必须给这个类加载器指定树上的一个节点作为它的父加载器。
- 因为类加载器是通过包名和类名(或者说类的全限定名),所以由于委派式加载机制的存在,全限定名相同的类不会在有 祖先—子孙 关系的类加载器上分别加载一次,不管这两个类的实现是否一样。
- 不同的类加载器加载的类一定是不同的类,即使它们的全限定名一样。如果全限定名一样,那么根据上一条,这两个类加载器一定没有 祖先-子孙 的关系。这样来看,可以通过自定义类加载器使得相同全限定名但实现不同的类存在于同一 JVM 中,也就是说,类加载器相当于给类在包名之上又加了个命名空间。
- 如果两个相同全限定名的类由两个非 祖先-子孙 关系的类加载器加载,这两个类之间通过
instanceof
和equals()
等进行比较时总是返回false
。
安卓应用和普通的 java 应用不同,它们运行于 Dalvik 虚拟机。JVM 是基于栈的虚拟机,而 Dalvik 是基于寄存器的虚拟机。Android采用 dex 作为储存类字节码信息的文件。当 java 程序编译成 class 后,编译器会使用 dx 工具将所有的class 文件整合到一个 dex 文件,目的是使其中各个类能够共享数据,在一定程度上降低了冗余,同时也是文件结构更加紧凑。
DexClassLoader & PathClassLoader说明
- package dalvik.system;
- import java.io.File;
- public class DexClassLoader extends BaseDexClassLoader {
- public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
- super(dexPath, new File(optimizedDirectory), libraryPath, parent);
- }
- }
- package dalvik.system;
- public class PathClassLoader extends BaseDexClassLoader {
- public PathClassLoader(String dexPath, ClassLoader parent) {
- super(dexPath, null, null, parent);
- }
- public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) {
- super(dexPath, null, libraryPath, parent);
- }
- }
可以看到,这两个类加载器都是继承自 BaseDexClassLoader,只是分别实现了自己的构造方法。
- BaseDexClassLoader
- public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) {
- super(parent);
- this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
- }
我们发现BaseDexClassLoader作为一个基类,其构造极其简单,它做了两件事:连接了父加载器;构造了一个 DexPathList 实例保存在 pathList 中。
- 第一个参数指的是我们要加载的 dex 文件的路径,它有可能是多个 dex 路径,取决于我们要加载的 dex 文件的个数,多个路径之间用
:
隔开。 - 第二个参数指的是优化后的 dex 存放目录。实际上,dex 其实还并不能被虚拟机直接加载,它需要系统的优化工具优化后才能真正被利用。优化之后的 dex 文件我们把它叫做 odex (optimized dex,说明这是被优化后的 dex)文件。其实从 class 到 dex 也算是经历了一次优化,这种优化的是机器无关的优化,也就是说不管将来运行在什么机器上,这种优化都是遵循固定模式的,因此这种优化发生在 apk 编译。而从 dex 文件到 odex 文件,是机器相关的优化,它使得 odex 适配于特定的硬件环境,不同机器这一步的优化可能有所不同,所以这一步需要在应用安装等运行时期由机器来完成。需要注意的是,在较早版本的系统中,这个目录可以指定为外部存储中的目录,较新版本的系统为了安全只允许其为应用程序私有存储空间(
/data/data/apk-package-name/
)下的目录,一般我们可以通过Context#getDir(String dirName)
得到这个目录。 - 第三个参数的意义是库文件的的搜索路径,一般来说是
.so
库文件的路径,也可以指明多个路径。 - 第四个参数就是要传入的父加载器,一般情况我们可以通过
Context#getClassLoader()
得到应用程序的类加载器然后把它传进去。
好了,到这里就很清楚了,Dalvik 虚拟机要加载的 dex 文件的路径(DexPathList),那么Dalvik是如何找到Dex的呢?有人会说反射,对,大方向对了。那么我们看看系统究竟是怎么做的。
- public DexPathList(ClassLoader definingContext, String dexPath,
- String librarySearchPath, File optimizedDirectory) {
- if (definingContext == null) {
- throw new NullPointerException("definingContext == null");
- }
- if (dexPath == null) {
- throw new NullPointerException("dexPath == null");
- }
- if (optimizedDirectory != null) {
- if (!optimizedDirectory.exists()) {
- throw new IllegalArgumentException(
- "optimizedDirectory doesn't exist: "
- + optimizedDirectory);
- }
- if (!(optimizedDirectory.canRead()
- && optimizedDirectory.canWrite())) {
- throw new IllegalArgumentException(
- "optimizedDirectory not readable/writable: "
- + optimizedDirectory);
- }
- }
- this.definingContext = definingContext;
- ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
- // save dexPath for BaseDexClassLoader
- this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
- suppressedExceptions, definingContext);
- // Native libraries may exist in both the system and
- // application library paths, and we use this search order:
- //
- // 1. This class loader's library path for application libraries (librarySearchPath):
- // 1.1. Native library directories
- // 1.2. Path to libraries in apk-files
- // 2. The VM's library path from the system property for system libraries
- // also known as java.library.path
- //
- // This order was reversed prior to Gingerbread; see http://b/2933456.
- this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
- this.systemNativeLibraryDirectories =
- splitPaths(System.getProperty("java.library.path"), true);
- List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
- allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
- this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories,
- suppressedExceptions,
- definingContext);
- if (suppressedExceptions.size() > 0) {
- this.dexElementsSuppressedExceptions =
- suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
- } else {
- dexElementsSuppressedExceptions = null;
- }
- }
这里我们主要看如下几行代码:
- his.definingContext = definingContext;
- ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
- // save dexPath for BaseDexClassLoader
- this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
- suppressedExceptions, definingContext);
- // Native libraries may exist in both the system and
- // application library paths, and we use this search order:
- //
- // 1. This class loader's library path for application libraries (librarySearchPath):
- // 1.1. Native library directories
- // 1.2. Path to libraries in apk-files
- // 2. The VM's library path from the system property for system libraries
- // also known as java.library.path
- //
- // This order was reversed prior to Gingerbread; see http://b/2933456.
- this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
- this.systemNativeLibraryDirectories =
- splitPaths(System.getProperty("java.library.path"), true);
- List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
- allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
- this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories,
- suppressedExceptions,
- definingContext);
这段代码主要是给 dexElements和nativeLibraryPathElements赋值。我们知道Android在通过默认的虚拟机dex后,会继续优化为odex 文件。
- private static List<File> splitPaths(String searchPath, boolean directoriesOnly) {
- List<File> result = new ArrayList<>();
- if (searchPath != null) {
- for (String path : searchPath.split(File.pathSeparator)) {
- if (directoriesOnly) {
- try {
- StructStat sb = Libcore.os.stat(path);
- if (!S_ISDIR(sb.st_mode)) {
- continue;
- }
- } catch (ErrnoException ignored) {
- continue;
- }
- }
- result.add(new File(path));
- }
- }
- return result;
- }
这个方法很简单就是用,分隔的路径分割后保存为 File 类型的列表返回。现在看看 makeDexElements()
这个方法:
- private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
- List<IOException> suppressedExceptions,
- ClassLoader loader) {
- return makeElements(files, optimizedDirectory, suppressedExceptions, false, loader);
- }
- private static Element[] makeElements(List<File> files, File optimizedDirectory,
- List<IOException> suppressedExceptions,
- boolean ignoreDexFiles,
- ClassLoader loader) {
- Element[] elements = new Element[files.size()];
- int elementsPos = 0;
- /*
- * Open all files and load the (direct or contained) dex files
- * up front.
- */
- for (File file : files) {
- File zip = null;
- File dir = new File("");
- DexFile dex = null;
- String path = file.getPath();
- String name = file.getName();
- if (path.contains(zipSeparator)) {
- String split[] = path.split(zipSeparator, 2);
- zip = new File(split[0]);
- dir = new File(split[1]);
- } else if (file.isDirectory()) {
- // We support directories for looking up resources and native libraries.
- // Looking up resources in directories is useful for running libcore tests.
- elements[elementsPos++] = new Element(file, true, null, null);
- } else if (file.isFile()) {
- if (!ignoreDexFiles && name.endsWith(DEX_SUFFIX)) {
- // Raw dex file (not inside a zip/jar).
- try {
- dex = loadDexFile(file, optimizedDirectory, loader, elements);
- } catch (IOException suppressed) {
- System.logE("Unable to load dex file: " + file, suppressed);
- suppressedExceptions.add(suppressed);
- }
- } else {
- zip = file;
- if (!ignoreDexFiles) {
- try {
- dex = loadDexFile(file, optimizedDirectory, loader, elements);
- } catch (IOException suppressed) {
- /*
- * IOException might get thrown "legitimately" by the DexFile constructor if
- * the zip file turns out to be resource-only (that is, no classes.dex file
- * in it).
- * Let dex == null and hang on to the exception to add to the tea-leaves for
- * when findClass returns null.
- */
- suppressedExceptions.add(suppressed);
- }
- }
- }
- } else {
- System.logW("ClassLoader referenced unknown path: " + file);
- }
- if ((zip != null) || (dex != null)) {
- elements[elementsPos++] = new Element(dir, false, zip, dex);
- }
- }
- if (elementsPos != elements.length) {
- elements = Arrays.copyOf(elements, elementsPos);
- }
- return elements;
- }
通过代码我们可以大致了解到,这个方法就是将之前的File对象通过重新组合成一个新的Elements对象,然后我们Loader读取的就是Element对象。看一下 loadDexFile() 怎样加载 DexFile 的
- private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
- Element[] elements) throws IOException {
- if (optimizedDirectory == null) {
- return new DexFile(file, loader, elements);
- } else {
- String optimizedPath = optimizedPathFor(file, optimizedDirectory);
- return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
- }
- }
先说明下无论是 DexFile(File file, Classloader loader, Elements[] elements)
还是 DexFile.loadDex() 最终都会调用 DexFile(String sourceName, String outputName, int flags, ClassLoader loader, DexPathList.Element[] elements)
这个构造方法。所以这个方法的逻辑就是:如果 optimizedDirectory
为 null,那么就直接利用 file 的路径构造一个 DexFile
;否则就根据要加载的 dex(或者包含了 dex 的 zip) 的文件名和优化后的 dex 存放的目录组合成优化后的 dex(也就是 odex)文件的输出路径,然后利用原始路径和优化后的输出路径构造出一个DexFile.
分析完这两字段,现在我们回过头来看看 DexPathList 这个对象,这个对象持有 dexElements 和 nativeLibraryPathElements 这两个属性,也就是说它保存了 dex 和 本地方法库。
为了加深大家对DexPathList的理解,我们来看看官方的说明。
BaseClassLoader 加载器的类加载过程
我们知道,一个类加载器的入口方法是 loadClass()。这是Java语音所共有的。类加载器通过findClass()找到所需要加载的类。
- 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;
- }
BaseDexClassLoader 也继承自 ClassLoader,因此我们就从 findClass() 方法来分析下 BaseClassLoader 加载类的过程。
- @Override
- 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;
- }
这个方法极其简单,主要风格findclass找到类 Class c = pathList.findClass(name, suppressedException)
这里BaseClassLoader
把查找类的任务委托给了 pathList
。那么我们来看一下Android的DexPathList的findClass又做了什么事情。
- 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;
- }
它遍历了 dexElements
中的所有 DexFile
,通过 DexFile
的loadClassBinaryName()
方法加载目标类。dexElements
又把查找类的任务委托给了DexFile
- private DexFile(String sourceName, String outputName, int flags, ClassLoader loader,
- DexPathList.Element[] elements) throws IOException {
- if (outputName != null) {
- try {
- String parent = new File(outputName).getParent();
- if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
- throw new IllegalArgumentException("Optimized data directory " + parent
- + " is not owned by the current user. Shared storage cannot protect"
- + " your application from code injection attacks.");
- }
- } catch (ErrnoException ignored) {
- // assume we'll fail with a more contextual error later
- }
- }
- mCookie = openDexFile(sourceName, outputName, flags, loader, elements);
- mFileName = sourceName;
- //System.out.println("DEX FILE cookie is " + mCookie + " sourceName=" + sourceName + " outputName=" + outputName);
- }
到这里我们就已经很明白了,openDexFile调用openDexFileNative()方法,(
- mCookie = openDexFile(sourceName, outputName, flags, loader, elements);
它做的事就是把对应的 dex 文件加载到内存中,然后返回给 java 层一个类似句柄一样的东西 Object:mCookie。
在构造方法中 DexFile
就完成了 dex 文件的加载过程。现在我们回到 DexFile
对象的loadClassBinaryName()
:
- public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
- return defineClass(name, loader, mCookie, this, suppressed);
- }
- private static Class defineClass(String name, ClassLoader loader, Object cookie,
- DexFile dexFile, List<Throwable> suppressed) {
- Class result = null;
- try {
- result = defineClassNative(name, loader, cookie, dexFile);
- } catch (NoClassDefFoundError e) {
- if (suppressed != null) {
- suppressed.add(e);
- }
- } catch (ClassNotFoundException e) {
- if (suppressed != null) {
- suppressed.add(e);
- }
- }
- return result;
- }
看到这里我们明白了,class 对象在 java 层加载过程的尽头就是这个 defineClass()
方法,这个方法调用本地法 defineClassNative()
从 dex 中查找目标类,如果找到了,就把这个代表这个类的 Class
对象返回。到此,Android的加载过程我们终于看完了。
DexClassLoader()
和 PathClassLoader()。
DexClassLoader
用来加载 .dex 文件以及包含 dex 文件的 .jar、.zip 和未安装的 .apk 文件,因此需要指定优化后的 dex 文件的输出路径;PathClassLoader
一般用来加载已经安装到设备上的.apk
,因为应用在安装的时候已经对 apk 文件中的 dex 进行了优化,并且会输出到 /data/dalvik-cache
目录下(android M 在这目录下找不到,应该是改成了 /data/app/com.example.app-x/oat
目录下),所以它不需要指定优化后 dex 的输出路径。Android动态加载入坑指南的更多相关文章
- Android动态加载技术初探
一.前言: 现在,已经有实力强大的公司用这个技术开发应用了,比如淘宝,大众点评,百度地图等,之所以采用这个技术,实际上,就是方便更新功能,当然,前提是新旧功能的接口一致,不然会报Not Found等错 ...
- Android动态加载jar/dex
前言 在目前的软硬件环境下,Native App与Web App在用户体验上有着明显的优势,但在实际项目中有些会因为业务的频繁变更而频繁的升级客户端,造成较差的用户体验,而这也恰恰是Web App的优 ...
- [转载] Android动态加载Dex机制解析
本文转载自: http://blog.csdn.net/wy353208214/article/details/50859422 1.什么是类加载器? 类加载器(class loader)是 Java ...
- Android 动态加载 (二) 态加载机制 案例二
探秘腾讯Android手机游戏平台之不安装游戏APK直接启动法 重要说明 在实践的过程中大家都会发现资源引用的问题,这里重点声明两点: 1. 资源文件是不能直接inflate的,如果简单的话直接在程序 ...
- Android 动态加载 (一) 态加载机制 案例一
在目前的软硬件环境下,Native App与Web App在用户体验上有着明显的优势,但在实际项目中有些会因为业务的频繁变更而频繁的升级客户端,造成较差的用户体验,而这也恰恰是Web App的优势.本 ...
- Android应用开发提高系列(4)——Android动态加载(上)——加载未安装APK中的类
前言 近期做换肤功能,由于换肤程度较高,受限于平台本身,实现起来较复杂,暂时搁置了该功能,但也积累了一些经验,将分两篇文章来写这部分的内容,欢迎交流! 关键字:Android动态加载 声明 欢迎转载, ...
- Android动态加载代码技术
Android动态加载代码技术 在开发Android App的过程当中,可能希望实现插件式软件架构,将一部分代码以另外一个APK的形式单独发布,而在主程序中加载并执行这个APK中的代码. 实现这个任务 ...
- 【Android】Android动态加载Jar、APK的实现
本文介绍Android中动态加载Jar.APK的实现.而主要用到的就是DexClassLoader这个类.大家都知道Android和普通的Java虚拟机有差别,它只能加载经过处理的dex文件.而加载这 ...
- 深入浅出Android动态加载jar包技术
在实际项目中,由于某些业务频繁变更而导致频繁升级客户端的弊病会造成较差的用户体验,而这也恰是Web App的优势,于是便衍生了一种思路,将核心的易于变更的业务封装在jar包里然后通过网络下载下来,再由 ...
随机推荐
- noip模拟题-赛斯石
题目背景 白露横江,水光接天,纵一苇之所如,凌万顷之茫然.--苏轼 真程海洋近来需要进购大批赛斯石,你或许会问,什么是赛斯石? 首先我们来了解一下赛斯,赛斯是一个重量单位,我们用sisi作为其单位.比 ...
- hdu 5895 广义Fibonacci数列
Mathematician QSC Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 131072/131072 K (Java/Othe ...
- C++11的原子量与内存序浅析
一.多线程下共享变量的问题 在多线程编程中经常需要在不同线程之间共享一些变量,然而对于共享变量操作却经常造成一些莫名奇妙的错误,除非老老实实加锁对访问保护,否则经常出现一些(看起来)匪夷所思的情况.比 ...
- VS2012不能加载想要打开的项目/解决方案
今天回宿舍用自己的电脑敲代码,想要打开之前的项目,可是VS2012打开之后项目却显示“无法加载” 查了之后才知道原来是由于某个安装包缺少引起的,具体做法请看如下 链接:http://jingyan.b ...
- H3C S3100交换机配置VLAN和远程管理
一.基本设置 1. console线连接成功 2. 进入系统模式 <H3C>system-view //提示符由<H3C> 变为 [H3C] 3. 更改设备名称 [H3C]sy ...
- 京东消息中间件JMQ
http://blog.csdn.net/javahongxi/article/details/54411464 [京东技术]京东的MQ经历了JQ->AMQ->JMQ的发展,其中JQ的基于 ...
- 设置元素text-overflow: ellipsis后引起的文本对齐问题
.ellipsis { white-space: nowrap; text-overflow: ellipsis; overflow: hidden; } 给元素设置了这个属性之后,该行内元素和旁边的 ...
- oss web直传
签名信息 auth.php <?php function gmt_iso8601($time) { $dtStr = date("c", $time); $mydatetim ...
- FJUT寒假作业第三周数蚂蚁(记录第一道并查集)
http://210.34.193.66:8080/vj/Contest.jsp?cid=162#P7 思路:用并查集合并集合,最后遍历,找到集合的根的个数. 并查集是森林,森林中的每一颗树是一个集合 ...
- C++编译连接过程中关于符号表的报错分析
是这样的,在学习郑莉老师的多文件结构和编译预处理命令章节时候,看到书里有这么一张图描述如下:#include指令作用是将指定的文件嵌入到当前源文件中#include指令所在的位置. 然后我就想5_10 ...