Android | 从类加载到程序执行

简介: Android | 从类加载到程序执行

目录

image.png

前置知识



1. Java 类加载的委派模型


Java 类加载是一种委托机制(parent delegate),即:除了顶级启动类加载器(bootstrap classloader)之外,每个类加载器都有一个关联的上级类加载器(parent 字段)。当一个类加载器准备执行类加载时,它首先会委托给上级加载器去加载,而上级加载器可能还会继续向上委托,递归这个过程。如果上级构造器无法加载,才会返回由自己加载。


image.png


更多内容:类加载:Java 虚拟机 | 类加载机制


2. Android 中的类加载器


在 Java 中,JVM 加载的是 .class 文件,而在 Android 中,Dalvik 和 ART 加载的是 dex 文件。这里的 dex 文件不仅仅指 .dex 后缀的文件,而是指携带 classed.dex 项的任何文件(例如:jar / zip / apk)。


这一节我们就来分析 Android ART 虚拟机 中的类加载器:


ClassLoader 实现类 作用
BootClassLoader 加载 SDK 中的类
PathClassLoader 加载应用程序的类
DexClassLoader 加载指定的类


2.1 BootClassLoader 类加载器


在 Java / Android 中,BootClassLoader 是委托模型中的顶级加载器,作为委托链的最后一个成员,它总是最先尝试加载类的。


  • 1、BootClassLoader 是单例的,一个进程只会有一个 BootClassLoader 对象,并在 JVM 启动的时候启动;
  • 2、BootClassLoader 的 parent 字段为空,没有上级类加载器(可以通过判断一个 ClassLoader#getParent() 是否来空来判断是否为 BootClassLoader);
  • 3、BootClassLoader#findClass(),最终调用 native 方法。


BootClassLoader 是 ClassLoader 的非静态内部类,源码如下:

ClassLoader.java


class BootClassLoader extends ClassLoader {
    public static synchronized BootClassLoader getInstance() {
        单例
    }
    public BootClassLoader() {
        没有上级类加载器,parent 为 null
        super(null);
    }
    @Override
    protected Class<?> findClass(String name) {
        注意 ClassLoader 参数:传递 null
        return Class.classForName(name, false, null);
    }
    @Override
    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        1、检查是否加载过
        Class<?> clazz = findLoadedClass(className);
        2、尝试加载
        if (clazz == null) {
            clazz = findClass(className);
        }
        return clazz;
    }
}
-------------------------------------------------
Class.java
static native Class<?> classForName(String className, boolean shouldInitialize, ClassLoader classLoader) 
复制代码


2.2 BaseDexClassLoader 类加载器


在 Android 中,Java 代码的编译产物是 dex 格式字节码,所以 Android 系统提供了 BaseDexClassLoader 类加载器,用于从 dex 文件中加载类。


BaseDexClassLoader.java


public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        从 DexPathList 的路径中加载类
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            throw new ClassNotFoundException(...);
        }
        return c;
    }
    添加 dex 路径
    public void addDexPath(String dexPath, boolean isTrusted) {
        pathList.addDexPath(dexPath, isTrusted);
    }
    添加 so 动态库路径
    public void addNativePath(Collection<String> libPaths) {
        pathList.addNativePath(libPaths);
    }
}
复制代码


可以看到,BaseDexClassLoader 将 findClass() 的任务委派给  DexPathList 对象处理,这个 DexPathList 指定了搜索类和 so 动态库的路径。

【todo】


2.3 PathClassLoader & DexClassLoader 类加载器


PathClassLoader & DexClassLoader 是 BaseDexClassLoader 的子类,从源码可以看出,它们其实都没有重写方法,所以主要的逻辑还是在 BaseDexClassLoader。 并且它们只在 Android 9.0 之前有区别:


DexClassLoader.java - Android 8.0


public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
    super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
复制代码

DexClassLoader.java - Android 9.0


public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
    super(dexPath, null, librarySearchPath, parent);
}
复制代码

PathClassLoader.java


public PathClassLoader(String dexPath,  String librarySearchPath, ClassLoader parent) {
    super(dexPath, null, librarySearchPath, parent);
}
复制代码


参数 描述
dexPath 加载 dex 文件的路径
optimizedDirectory 加载 odex 文件的路径(优化后的 dex 文件)
librarySearchPath 加载 so 库文件的路径
parent 上级类加载器


可以看到,在 Android 9.0 之前,DexClassLoader 的构造方法需要传入optimizedDirectory参数。不过从 Android 9.0 开始,DexClassLoader 也不需要传这个参数了,所以 Android 9.0 开始两个类就完全一样了。

image.png


3. 程序的执行:编译 & 解释


程序员通过源码的形式编写程序,而 CPU 只能识别 / 运行本地代码。将源码转换为本地代码有两种做法:解释和编译。


  • 解释: 通过解释器边翻译边执行,多次执行同一份代码需要重复解释翻译,效率低,但移植性更好;
  • 编译: 通过编译器将源程序完整的地翻译为本地代码,编译一次得到的产物可以反复执行,效率较高,但编译耗时。


“编译” 这个词在狭义和广义上有不同的理解,狭义上的编译是指将 .java 文件转换为*.class 文件或 .dex 文件的过程,也称为 编译前端。而广义的编译还包括运行期即时编译(JIT,Just in Time Compile)或者(静态的)提前编译(AOT,Ahead of Time Compile),这两种编译称为 编译后端

image.png

image.png


Java 没有采用极端的完全解释执行或者编译执行,而是采用了介于两者之间的执行方式。无论是 .class 文件还是 .dex 文件,都只是编译过程的中间产物,并没有完全编译为本地代码。在运行时,还需要虚拟机进行解释执行或者进一步编译。


下面,我们来讨论 Dalvik 和 ART 虚拟机上的程序执行。


4. Dalvik


4.1 Dalvik 上的 JIT


在 Dalvik 的早期的版本中是只有解释器的,同一份代码需要重复解释翻译多次,效率低,为了优化这个问题。从 Android 2.2 版本开始加入了 JIT 编译器,JIT 在运行时编译生成本地代码,就不用重复解释翻译,这样就加快了执行的速度。


虽然 JIT 编译可以提高代码执行速度,但是编译本身是耗时的事情,所以只应该对 “热点” 代码进行编译。那么即时编译器是如何探测热点代码的呢?主要有两种:基于采样 & 基于计数器


Dalvik 中的 JIT 采用的是基于计数器的热点探测,主要流程如下:


  • 0、设定一个“热门”代码的阈值;
  • 1、检查是否存在编译后的本地代码?有则执行;
  • 2、否则,记录代码的执行次数,每次执行时都比对一下看看有没有到阈值?
  • 2.1 是则向编译器发送即时编译请求,并以解释方式执行方法;
  • 2.2 否则继续以解释方式执行方法;


image.png


—— 引用自 paul.pub/android-dal… 强波(华为)著


4.2 dexopt 优化


在 Dalvik 虚拟机中,应用安装时会执行 dexopt 优化。这个过程主要是将 apk 中的 .dex 文件优化为 odex(optimized dex) 文件,保存在data/dalvik-cache目录,并将原来 apk 中的 .dex 文件删除。这样做的优点主要是:


  • 1、优化了 dex 文件;
  • 2、预先从 apk 中提取出 .dex 文件,启动速度略有加快。


5. ART


从 Android 4.4 开始,Android 系统就集成了 ART 虚拟机,不过默认是没有启用的,需要在开发者选项中手动开启。从 Android 5.0 开始,ART 虚拟机才被正式启用。


5.1 ART 上的 AOT(Android L 5.0)


在 ART 虚拟机中,应用安装时会执行 AOT 编译。即在程序运行之前提前使用 dex2oat 工具将 apk 中的 .dex 文件变化为 OAT 文件。OAT 文件遵循 ELF 格式,是 Unix 系统上的可执行文件。程序运行的时候就可以直接执行已经编译好的代码,相当于使用 AOT 编译提前预热。

image.png


—— 图片引用自网络


5.2 JIT 的回归(Android N 7.0)


AOT 编译虽然可以提前编译出本地代码,但是单纯的 AOT 编译会存在两种情况下用户等待时间过长的问题:


  • 1、应用安装时间过长;
  • 2、系统版本升级时,所有应用需要重新 AOT 编译。


image.png

—— 图片引用自网络


为了解决用户等待时间过长的问题,从 Android N 7.0 开始,Android 重新引入了 JIT,采用了 AOT 编译、解释和 JIT 编译混合的运行方式。主要工作流程如下:


  • 1、在应用安装时,不再进行 AOT 编译(安装速度变快了);
  • 2、在程序执行时,使用解释执行 + JIT 编译的方式,并且将经过 JIT 编译的热点方法记录到 profile 配置文件中;
  • 3、在设备闲置时,编译守护进程根据 profile 文件的记录的热点代码进行 AOT 编译。

image.png


—— 引用自 paul.pub/android-dal… 强波(华为)著


6. 总结


  • 1、Java 类加载是一种委托机制,当一个类加载器准备执行类加载时,它首先会委托给上级加载器去加载,而上级加载器可能还会继续向上委托,递归这个过程。如果上级构造器无法加载,才会返回由自己加载;
  • 2、JVM 加载的是 .class 文件,而 Dalvik 和 ART 加载的是 dex 文件,在 Android 中的类加载器主要是 BootClassLoader & PathClassLoader & DexClassLoader;
  • 3、将源码转换为本地代码有两种做法:解释和编译。解释是边翻译边执行,多次执行同一份代码需要重复解释翻译,效率低,但移植性更好; 编译是将源程序翻译为本地代码,编译一次得到的产物可以反复执行,效率较高,但编译耗时。
  • 4、Dalvik 从 Android 2.2 开始采用 JIT 编译,Dalvik 还会使用 dexopt 将 dex 文件优化为 odex 文件;
  • 5、ART 从 Android 5.0 正式启用,采用了 AOT 编译生成 oat 文件,存在安装 / 系统升级时用户等待时间过程的副作用。从 Android 7.0 开始,Android 重新引入了 JIT,采用了 AOT 编译、解释和 JIT 编译混合的运行方式。
目录
相关文章
|
6月前
|
设计模式 算法 前端开发
Android面经分享,失业两个月,五一节前拿到Offer,设计思想与代码质量优化+程序性能优化+开发效率优化
Android面经分享,失业两个月,五一节前拿到Offer,设计思想与代码质量优化+程序性能优化+开发效率优化
|
6月前
|
Linux Android开发
测试程序之提供ioctl函数应用操作GPIO适用于Linux/Android
测试程序之提供ioctl函数应用操作GPIO适用于Linux/Android
114 0
|
数据采集 编解码 Ubuntu
Android流媒体开发之路二:NDK C++开发Android端RTMP直播推流程序
Android流媒体开发之路二:NDK C++开发Android端RTMP直播推流程序
275 0
|
存储 传感器 定位技术
《移动互联网技术》 第四章 移动应用开发: Android Studio开发环境的使用方法:建立工程,编写源程序,编译链接,安装模拟器,通过模拟器运行和调试程序
《移动互联网技术》 第四章 移动应用开发: Android Studio开发环境的使用方法:建立工程,编写源程序,编译链接,安装模拟器,通过模拟器运行和调试程序
185 0
|
Java Android开发 开发者
1024程序节|Android框架之一 BRVAH【BaseRecyclerViewAdapterHelper】使用demo
BRVAH是一个强大的RecyclerAdapter框架(什么是RecyclerView?),它能节约开发者大量的开发时间,集成了大部分列表常用需求解决方案。为什么会有它?请查看「Android开源框架BRVAH由来篇」该框架于2016年4月10号发布的第1个版本到现在已经一年多了,经历了800多次代码提交,140多次版本打包,修复了1000多个问题,获得了9000多star,非常感谢大家的使用以及反馈。
240 0
|
5月前
|
安全 Java Android开发
05. 【Android教程】Android 程序签名打包
05. 【Android教程】Android 程序签名打包
61 1
|
4月前
|
Oracle Java 关系型数据库
Android studio 安装以及第一个程序
Android studio 安装以及第一个程序
125 0
|
5月前
|
Java Android开发
程序与技术分享:Android使用Dagger注入的方式初始化对象的简单使用
程序与技术分享:Android使用Dagger注入的方式初始化对象的简单使用
141 0
|
5月前
|
安全 网络协议 网络安全
程序与技术分享:Android应用安全之数据传输安全
程序与技术分享:Android应用安全之数据传输安全
|
6月前
|
XML Java Android开发
如何美化android程序:自定义ListView背景
如何美化android程序:自定义ListView背景
62 2