Dex动态加载是为了解决什么问题?

在Android系统中,一个App的所有代码都在一个Dex文件里面。

Dex是一个类似Jar的存储了多个Java编译字节码的归档文件。

因为Android系统使用Dalvik虚拟机,所以需要把使用Java Compiler编译之后的class文件转换成Dalvik能够执行的class文件。这里需要强调的是,Dex和Jar一样是一个归档文件,里面仍然是Java代码对应的字节码文件。

当Android系统启动一个应用的时候,有一步是对Dex进行优化,这个过程有一个专门的工具来处理,叫DexOpt。DexOpt的执行过程是在第一次加载Dex文件的时候执行的。

这个过程会生成一个ODEX文件,即Optimised Dex。执行ODex的效率会比直接执行Dex文件的效率要高很多。

但是在早期的Android系统中,DexOpt有一个问题,也就是这篇文章想要说明并解决的问题。

DexOpt会把每一个类的方法id检索起来,存在一个链表结构里面。但是这个链表的长度是用一个short类型来保存的,导致了方法id的数目不能够超过65536个。

当一个项目足够大的时候,显然这个方法数的上限是不够的。尽管在新版本的Android系统中,DexOpt修复了这个问题,但是我们仍然需要对老系统做兼容。

Dex动态加载思路

一种有效的解决思路是把Dex文件分割成多个较小的Dex。这就如同很多项目会把自己分割成多个Jar文件一样,不同的功能在不同的Jar文件里面,通过一些配置和额外的操作,可以让虚拟机有选择性的加载Jar文件。

但是在Android系统中,一个应用是只允许有一个Dex文件的。也就是说在编译期的时候,所有的Jar文件最终会被合并成一个Dex文件。我们没有办法在Apk文件里面打包两个Dex,让DexOpt分别对两个Dex文件做处理,而Android系统也不会同时为一个Apk加载两个Dex。

如果我们把Dex分成多个文件,然后在程序运行的时候,再把多的那几个动态的加载进来是否可行呢?

也就是说我们能否在运行时阶段把代码加入虚拟机中。对于虚拟机来说,其实所有的代码都是在运行时被加载进来的。而不同于C语言还存在着静态链接。虚拟机在所有Java代码执行之前被启动,然后开始把字节码加载到环境中执行,我们可以理解成所有的代码都是动态加载到虚拟机里的。

而说到加载,不得不说的是ClassLoader。它的工作就是加载.class文件。在Android的Dalvik环境中,对应的是DexClassLoader,它们的功能是完全一样的。

ClassLoader的一大特点就是它是一个树状结构(双亲委派)。每个ClassLoader都有一个父亲ClassLoader。也就是说,ClassLoader不是把所有的Class放到一个巨大的数组或别的什么数据结构中来处理。ClassLoader在加载一个Jar中的类的时候,需要制定另一个ClassLoader作为父亲节点,当我们需要通过ClassLoader得到一个类类型的时候,ClassLoader会把请求优先交给父ClassLoader来处理,而父ClassLoader又会交给它的父,一直到根ClassLoader。

如果根ClassLoader有这个类,而返回这个类的类类型,否则把这个请求交给这个请求的来源子ClassLoader。这是一种向上传递,向下分发的机制。这种情况下,对于调用着来说,子ClassLoader永远都是包含最多Class的ClassLoader。

有一点我们需要注意,父ClassLoader只会向请求来源分发自己的处理结果。所以如果来源是自己,那么如果没有请求类它就会返回空,而不是遍历所有子ClassLoader去请求是否有被请求的类。

在Android系统中,对于一个应用来说,其实有两个ClassLoader,一个是SystemClass-Loader,这个ClassLoader里面除了Java标准的类库之外,还有一个android.jar,所有Android Framework层的类都在这里。

而另外一个重要的ClassLoader就是基于Android Context的ClassLoader。所有属于当前应用的类都是用这个ClassLoader来加载的,我们可以在Android源码中看到,所有的Activity,Service,View都是使用这个ClassLoader来反射并创建的。我们暂时把它叫做ContextClassLoader。

加载外部Dex

首先构建一个Dex文件,这一步并不复杂,首先我们把所需要的.class文件或者是Jar文件和一些源码一起编译生成一个Jar文件。然后使用Android SDK提供的dx工具把Jar文件转成Dex文件。

我们可以提前对它进行ODex操作,让它在被DexClassLoader加载的时候,跳过DexOpt的部分工作,从而加快加载的过程。

现在的工作就是在运行时加载这个Dex文件了。我们可以在Application启动的onCreate方法里面加载Dex,但是如果你的Dex太大,那么它会让你的App启动变慢。

我们也可以使用线程去加载,但我们必须保证加载完成之后再进行某个外部类的请求。当然也可以真正等到需要某个外部类的时候再进行Dex加载。这根本上取决于Dex文件本身的大小,太大了可以预加载,而比较小可以等到实际需要的时候再加载。

我们暂且把这个加载了外部Dex的ClassLoader成为ExternalClassLoader.

上面我们提到了树形结构和系统中的多个ClassLoader,当我们加载外部Dex的时候,我们是否需要指定一个父ClassLoader呢?我们当然需要一个父ClassLoader,否则ExternalClassLoader连一些基本的Java类都没有,它根本不可能成功的加载一个Dex。

进一步的,我们要选择哪一个ClassLoader来作为我们的父亲呢?是SystemClassLoader还是ContextClass-Loader?

这是根据情况来定的,如果外部的Dex文件里没有任何和Android相关的代码,那么SystemClassLoader是我们的首选,否则我们就应该用ContextClass-Loader。如果是后者的情况,我们的树可以被看成一个链表。

外部的View, Acitivity等的处理

我们知道,我们编写的四大组件都不是由我们自己来创建的,是由系统来给我们构造并管理其生命周期的。那么这个过程是什么样的呢?

拿Activity来举例,我们需要通过调用当前Activity/Context的startActivity,传入一个Intent来调用启动一个新的Activity。系统有一个ActivityManager来处理这里的逻辑。这里的逻辑相当的复杂,但简单来说,ActivityManager会收到并处理这个Intent,从而决定是是启动一个新的,还是把旧的放到前台。它会先查找这个Activity在哪个应用里面,这是通过扫描每个应用的Android-Manifest来确定。这些信息是在PackageManager里面被检索的。总之如果这个Activity不在任何的manifest里面,它就不可能被启动。

所以仅有一个Activity类是不够的,我们需要在manifest里面声明它。上面是Activity的情况,Service之类的也是同理。那么View怎么办?

尽管我们可以直接创建View,但是大部分的View都不是我们创建的,而是通过XML布局文件Inflate出来的。也就是说,我们在XML定义了一些外部Dex里面的View,那么显然这个XML是不能被成功的Inflate的。因为除非系统会使用我们的ExternalClassLoader,否则它肯定是找不到我们的类的:ContextClassLoader里面并没有外部Dex中的类。

也就是说问题的根本在于,对于那些Android系统为我们创建的对象,它是不能包含在外部Dex里面的。而Android系统中大部分的组件类的生命周期都交给了系统来管理。我们不可能自己来创建这些类对象。

那么另一种思路:我们是不是可以通过使用我们的ExternalClassLoader来代替ContextClassLoader呢?尽管系统的ContextClassLoader是私有的,但是我们可以通过反射强制的把它替换成我们的ExternalClassLoader。

而对于那些外部的组件(Activity等),尽管我们没有它们的类,但是并不影响我们在AndroidManifest里面声明这个Activity。因为Android系统只是把它作为一个检索,并不会真正检查它里面的组件是不是真的在虚拟机环境中已经被加载了,只有真正使用Intent启动某个组件的时候才会去检查。而只要我们保证这个时候我们已经加载了外部的ClassLoader,那么这个组件就可以被正常的启动。

还有一点,除了我们要为外部可能有的组件在AndroidManifest里面做声明一外,那些外部组件可能用到的权限我们也需要一一声明,例如如果外部Activity使用了相机功能,那么如果我们的Manifest里面没有声明使用相机功能的权限的话,即便这个Activity能成功为加载出来,仍然是不能使用的。

Dex动态加载的更多相关文章

  1. [转载] Android动态加载Dex机制解析

    本文转载自: http://blog.csdn.net/wy353208214/article/details/50859422 1.什么是类加载器? 类加载器(class loader)是 Java ...

  2. Android 插件开发,做成动态加载

    为什么需要插件开发: 相信你对Android方法数不能超过65K的限制应该有所耳闻,随着应用程序功能不断的丰富,总有一天你会遇到一个异常: Conversion to Dalvik format fa ...

  3. Android应用安全之外部动态加载DEX文件风险

    1. 外部动态加载DEX文件风险描述 Android 系统提供了一种类加载器DexClassLoader,其可以在运行时动态加载并解释执行包含在JAR或APK文件内的DEX文件.外部动态加载DEX文件 ...

  4. Android动态加载jar/dex

    前言 在目前的软硬件环境下,Native App与Web App在用户体验上有着明显的优势,但在实际项目中有些会因为业务的频繁变更而频繁的升级客户端,造成较差的用户体验,而这也恰恰是Web App的优 ...

  5. Android 插件技术:动态加载dex技术初探

    1.Android动态加载dex技术初探 http://blog.csdn.net/u013478336/article/details/50734108 Android使用Dalvik虚拟机加载可执 ...

  6. 动态加载框架DL分析

    动态加载框架DL分析 插件化开发,主要解决三个问题1.动态加载未安装的apk,dex,jar等文件2.activity生命周期的问题,还有service3.Android的资源调用的问题 简单说一下怎 ...

  7. Android动态加载学习笔记(一)

    前言 上周五DPAndroid小分队就第二阶段分享内容进行了讨论,结果形成了三个主题:性能优化.动态加载.内核远离.我选择的是第二项——动态加载.在目前的Android开发中,这一部分知识还是比较流行 ...

  8. Android中插件开发篇之----动态加载Activity(免安装运行程序)

    一.前言 又到周末了,时间过的很快,今天我们来看一下Android中插件开发篇的最后一篇文章的内容:动态加载Activity(免安装运行程序),在上一篇文章中说道了,如何动态加载资源(应用换肤原理解析 ...

  9. Android中的动态加载机制

    在目前的软硬件环境下,Native App与Web App在用户体验上有着明显的优势,但在实际项目中有些会因为业务的频繁变更而频繁的升级客户端,造成较差的用户体验,而这也恰恰是Web App的优势.本 ...

随机推荐

  1. llinux 查看自己的公网ip

    如何在LINUX服务器下查看公网IP地址,可以使用下面的方法: [root@web ~]#curl http://members.3322.org/dyndns/getip [root@web ~]# ...

  2. 10月16日上午MySQL数据库作业设计表解析

    作业设计表:多张表存储学生成绩及各种信息 需要从表里面体现: 关于学生的:代号 姓名 性别 年龄 班级 关于课程的:代号 名称 关于老师的:代号 姓名 关于成绩的:例如:闫超--网页--90 要能查看 ...

  3. 11 Clever Methods of Overfitting and how to avoid them

    11 Clever Methods of Overfitting and how to avoid them Overfitting is the bane of Data Science in th ...

  4. GLEW OpenGL Access violation when using glGenVertexArrays

    http://stackoverflow.com/questions/20766864/glew-opengl-access-violation-when-using-glgenvertexarray ...

  5. php 多条数据更新

    mysql更新语句很简单,更新一条数据的某个字段,一般这样写: 1 UPDATE mytable SET myfield = 'value' WHERE other_field = 'other_va ...

  6. Python之路【第十六篇续】Django进阶篇

    Django请求生命周期 首先:对于所有的web框架来说本质就是一个socket服务端,浏览器是socket客户端 路由系统 在Django的urls中我们可以根据一个URL对应一个函数名来定义路由规 ...

  7. 2.servlet的会话机制session

    session的说明: 1.session是服务端技术,存放在服务器 2.一个用户浏览器对应一个session域对象,一对一的对应关系 3.session的默认生命周期是30min,可以通过web.x ...

  8. Java7的异常处理新特性-addSuppressed()方法等

    开发人员对异常处理的try-catch-finally语句块都比较熟悉.如果在try语句块中抛出了异常,在控制权转移到调用栈上一层代码之前,finally语句块中的语句也会执行.但是finally语句 ...

  9. 【8-17】c++学习笔记01

    控制台程序不自动退出方法: system("pause"); getchar() 使用执行 ctrl+F5,开始调试 F5会出现闪退 动态内存分配 //construct c st ...

  10. Netbeans 设置模板

    /** * @Description * @author ${user} * @date ${date} ${time} * @copyright ${copyright} */ 工具-->模板 ...