1 问题

在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修复了这个问题,但是我们仍然需要对老系统做兼容。

2 思路

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

可以让虚拟机有选择性的加载Jar文件。但是在Android系统中,一个应用是只允许有一个Dex文件的。也就是说在编译期的时候,所有的Jar文件最终会被合并成一个Dex文件。

我们没有办法在Apk文件里面打包两个Dex,让DexOpt分别对两个Dex文件做处理,而Android系统也不会同时为一个Apk加载两个Dex。

2.1 动态加载

如果我们把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,一个是SystemClassLoader,这个ClassLoader里面除了Java标准的类库之外,还有一个android.jar,所有

Android Framework层的类都在这里。而另外一个重要的ClassLoader就是基于AndroidContext的ClassLoader。所有属于当前应用的类都是用这个ClassLoader来加载的,我们

可以在Android源码中看到,所有的Activity,Service,View都是使用这个ClassLoader来反射并创建的。我们暂时把它叫做ContextClassLoader。

3 加载外部Dex

3.1 构建一个Dex文件

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

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

3.2 DexClassLoader

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

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

大了可以预加载,而比较小可以等到实际需要的时候再加载。我们暂且把这个加载了外部Dex的ClassLoader成为ExternalClassLoader

上面我们提到了树形结构和系统中的多个ClassLoader,当我们加载外部Dex的时候,我们是否需要指定一个父ClassLoader呢?我们当然需要一个父ClassLoader,

否则我们ExternalClassLoader连一些基本的Java类都没有,它根本不可能成功的加载一个Dex。进一步的,我们要选择哪一个ClassLoader来作为我们的父亲呢?是SystemClassLoader还是

ExternalClassLoader?这是根据情况来定的,如果外部的Dex文件里没有任何和Android相关的代码,那么SystemClassLoader是我们的首选,否则我们就应该用ContextClassLoader。

如果是后者的情况,我们的树可以被看成一个链表。

3.3 外部的View, Acitivity等

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

当前Activity/Context的startActivity,传入一个Intent来调用启动一个新的Activity。系统有一个ActivityManager来处理这里的逻辑。这里的逻辑相当的复杂,但简单来说,

ActivityManager会收到并处理这个Intent,从而决定是是启动一个新的,还是把旧的放到前台。它会先查找这个Activity在哪个应用里面,这是通过扫描每个应用的AndroidManifest来确定。

这些信息是在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能成功为加载出来,仍然是不能使用的。

4 核心代码段

加载外部Dex

mClassLoader = new DexClassLoader ( f . getAbsolutePath ( ) ,

mContext . getCacheDir ( ) . getAbsolutePath ( ) ,

null , mContext . getClassLoader ( ) ) ;

让系统使用ExternalClassLoader

t r y {

F ie ld mMainThread = ge t F ie ld ( A c t i v i t y . class , ”mMainThread”) ;

Object mainThread = mMainThread . get ( a c t i v i t y ) ;

Class t hreadClass = mainThread . get Class ( ) ;

F ie ld mPackages = ge t F ie ld ( threadClass , ”mPackages”) ;

WeakReference<?> r e f ;

Map< St ring , ?> map =(Map< St ring , ? >) mPackages . get (mainThread ) ;

r e f = (WeakReference<? >) map . get (mContext . getPackageName ( ) ) ;

Object apk = r e f . get ( ) ;

Class apkClass = apk . get Class ( ) ;

F ie ld mClassLoader = ge t F ie ld ( apkClass , ”mClassLoader”) ;

mClassLoader . set (apk , classLoader ) ;

} catch ( I llegalArgument Except ion e) {

i f (DEBUG) {

e . print St ackTrace ( ) ;

}

} catch ( I llega lAc c essEx c ept ion e) {

i f (DEBUG) {

e . print St ackTrace ( ) ;

}

}

Android系统下的动态Dex加载与app速度优化的更多相关文章

  1. Android系统下的动态Dex加载

    1 问题在Android系统中,一个App的所有代码都在一个Dex文件里面.Dex是一个类似Jar的存储了多有Java编译字节码的归档文件.因为Android系统使用Dalvik虚拟机,所以需要把使用 ...

  2. Android系统编程入门系列之加载界面Activity

    上回说到应用初始化加载及其生命周期,在Android系统调用Applicaiton.onCreate()之后,继续创建并加载清单文件中注册的首个界面即主Activity,也可称之为入口界面.主Acti ...

  3. 64位操作系统下IIS报“试图加载格式不正确的程序”错误

    缘由:在64位操作系统下IIS发布32位的项目,报“项目依赖的dll无法读取,试图加载格式不正确的程序”错误. 原因:程序集之间的通讯要么全是64位环境下的,要么全是32位环境下的.不能混编访问.不然 ...

  4. Android系统编程入门系列之加载服务Service

    之前几篇文章简单梳理了在Android系统的四大组件之一,最主要的界面Activity中,使应用程序与用户进行交互响应的相关知识点,那对于应用程序中不需要与用户交互的逻辑,又要用到哪些内容呢?本文开始 ...

  5. Java_动态重新加载Class机制

    Java动态重新加载Class 项目中使用到了动态重新加载Class的机制,作用是让一些代码上线之前可以在线上环境测试一下,当然,这是非常不好的测试机制,我刚来的时候也为这种机制感到惊讶—怎么可以在线 ...

  6. 为什么Android应该根据屏幕分辨率来加载不同的图片文件

    1.图片在xxhdpi,手机是hdpi的 我们有一个手机是hdpi的.我们还有一个图片,我们把他放在xxhdpi下.当手机显示的时候,系统会去hdpi中找,发现没有图片,最终在xxhpi中找到.终于找 ...

  7. linux动态库加载RPATH, RUNPATH

    摘自http://gotowqj.iteye.com/blog/1926771 linux动态库加载RPATH, RUNPATH 链接动态库 如何程序在连接时使用了共享库,就必须在运行的时候能够找到共 ...

  8. 使用javassist运行时动态重新加载java类及其他替换选择

    在不少的情况下,我们需要对生产中的系统进行问题排查,但是又不能重启应用,java应用不同于数据库的存储过程,至少到目前为止,还不能原生的支持随时进行编译替换,从这种角度来说,数据库比java的动态性要 ...

  9. esri-leaflet入门教程(5)- 动态要素加载

    esri-leaflet入门教程(5)- 动态要素加载 by 李远祥 在上一章节中已经说明了esr-leaflet是如何加载ArcGIS Server提供的各种服务,这些都是服务本身来决定的,API脚 ...

随机推荐

  1. 应用SecureCRT(发送接收文件)

    使用 SecureCRT 和 cz. sz,可以从 Linux 服务器上下载/上传文件. Linux 上要安装 lszrz 包 (1)编译安装root 账号登陆后,依次执行以下命令 cd /tmp w ...

  2. 使用单调队列优化的 O(nm) 多重背包算法

    我搜索了一下,找到了一篇很好的博客,讲的挺详细:链接. 解析 多重背包的最原始的状态转移方程: 令 c[i] = min(num[i], j / v[i]) f[i][j] = max(f[i-1][ ...

  3. 几个不错的编辑器BoneEdit

    https://github.com/bonecode?tab=repositories

  4. Lua I/0输入输出

    I/O库为文件操作提供了两种不同的模型,简单模型和完整模型.简单模型假设一个当前输入文件和一个当前输出文件,他的I/O操作均作用于这些文件.完整模型则使用显式的文件句柄,并将所有的操作定义为文件句柄上 ...

  5. 【转】Android Recovery模式

    原文网址:http://leox.iteye.com/blog/975303 (muddogxp 原创,转载请注明) Recovery简介 Android利用Recovery模式,进行恢复出厂设置,O ...

  6. 数据结构(KD树):HDU 4347 The Closest M Points

    The Closest M Points Time Limit: 16000/8000 MS (Java/Others)    Memory Limit: 98304/98304 K (Java/Ot ...

  7. LeetCode (85): Maximal Rectangle [含84题分析]

    链接: https://leetcode.com/problems/maximal-rectangle/ [描述] Given a 2D binary matrix filled with '0's ...

  8. python操作RabbiMQ

    RabbitMQ是一个在AMQP基础上完整的,可复用的企业消息系统.他遵循Mozilla Public License开源协议. MQ全称为Message Queue, 消息队列(MQ)是一种应用程序 ...

  9. 如何高性能的给UIImageView加个圆角

    文/natewang(简书作者)原文链接:http://www.jianshu.com/p/268f3839d2e6著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”. 其实你只需要的是圆角 ...

  10. Selenium终极自动化测试环境搭建(一) Selenium+Eclipse+Junit+TestNG

    Selenium终极自动化测试环境搭建(一)Selenium+Eclipse+Junit+TestNG 第一步 安装JDK JDk1.7. 下载地址:http://www.oracle.com/tec ...