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包里然后通过网络下载下来,再由 ...
随机推荐
- CodeForces - 724G:Xor-matic Number of the Graph
两点之间的任意路径都可表示为 随便某一条路径xor任何多个环, 然后可以用线性基来做,这样不会重复的, 另外必须一位一位的处理,xor是不满足结合律的 #include<cstdio> ...
- bzoj 1407: [Noi2002]Savage
Description 解题报告: 因为给定答案范围,暴力枚举时间,然后再两两枚举野人,判断是否有可能在某一年相遇,我们设这一年为\(x\),那么显然相交的条件是: \(x*(p[i]-p[j])+y ...
- hdu 4777 树状数组+合数分解
Rabbit Kingdom Time Limit: 6000/3000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others) T ...
- 【集训第二天·翻水的老师】--ac自动机+splay树
今天是第二天集训.(其实已经是第三天了,只是昨天并没有机会来写总结,现在补上) 上午大家心情都很愉快,因为老师讲了splay树和ac自动机. 但到了下午,我们的教练竟然跑出去耍了(excuse me? ...
- bzoj省选十连测推广赛
A.普通计算姬 题意:给丁一棵树,每个点有一个权值,用sum(x)表示以x为根的子树的权值和,要求支持两种操作: 1 u v :修改点u的权值为v. 2 l r : 求∑sum[i] l&l ...
- Jenkins简明入门(三) -- Blue Ocean,让一切变得简单
我们在上一节Jenkins简明入门(二) 中见识到了Jenkins能做些什么:利用Jenkins完成python程序的build.test.deployment. 同时,也有一种简单的方法,不需要写J ...
- python2.7入门---操作mysql数据库增删改查
Python 标准数据库接口为 Python DB-API,Python DB-API为开发人员提供了数据库应用编程接口.Python 数据库接口支持非常多的数据库,你可以选择适合你项目的数据库: G ...
- Servlet生命周期与工作原理(转载)
Servlet生命周期分为三个阶段: 1,初始化阶段 调用init()方法 2,响应客户请求阶段 调用service()方法 3,终止阶段 调用destroy()方法 Servlet初始化阶段: 在 ...
- 开源Spring解决方案--lm.solution
Github 项目地址: https://github.com/liumeng0403/lm.solution 一.说明 1.本项目未按java项目传统命名方式命名项目名,包名 如:org.xxxx. ...
- OpenCv error :unresolved external symbol(链接库没有加上)
Error 如下:Linking...: error LNK2001: unresolved external symbol _cvDestroyWindow: error LNK2001: unre ...