本文共 7359 字,大约阅读时间需要 24 分钟。
【版权申明】非商业目的注明出处可自由转载
博文地址:https://blog.csdn.net/ShuSheng0007/article/details/103259918 出自:shusheng007
文章最近一次更新为2020年5月10日
从接触Android开发前后也有几年了,多数时候还是在写业务代码,很少去研究总结一下基础的东西,然而基础知识才是程序员技术及发展潜力的试金石,根基打牢了,搞啥都快。
今天心中突然有个疑问:一个App从源代码到安装文件,再到安装到设备上,最后呈现在用户面前,这期间都经历了什么?我发现自己不能完全而清楚的知晓其中的细节,于是就去调查了一下,总结在这篇文章里。
提前打个招呼,本文具有一定的技术难度和广度,所以需要你具有相当的技术基础来阅读,当然我会尽力使用最容易理解的方式去叙述,这也是咱的一贯作风,绝不能丢。一篇博文写的再牛逼,别人都看不懂,意义也不大,毕竟我写的不是科研论文。本文会涉及到如下几个方面:
本文读者应该都知道,程序是运行在设备的CPU上的,然而我们世界是多样性的,CPU也不例外。现实中存在各种架构的CPU,例如ARM, Arm64, x86, x64, MIPS,架构不同那么CPU的指令及执行方式也就不同。而我们总是希望我们的程序可以运行在各种CPU上,你的App总不能因为小米和华为手机使用了不同的芯片就二选一吧?
那这个问题怎么解决呢?
最常用的方式就是针对不同的CPU架构,将程序编译为对应CPU的机器码文件。例如你有一款App要同时支持ARM
和x86
架构的手机,那么你就要为这两种手机各编译一个安装包,而且他们之间不能互换安装。
而等到类似于Java这种虚拟机语言出现后,人们就多了一个选择。在程序和硬件设备之间增加了一个虚拟层,让程序运行在虚拟层里,虚拟层运行在硬件上面,那么程序员再也不用关心各种各样的CPU
架构了,那是虚拟层的事情,这个虚拟层俗称虚拟机。
如下图所示,在虚拟机的帮助下就可以实现:一次编码,到处运行的效果,这也是Java当年提出时候的口号。
如果想要详细了解虚拟机的知识,建议阅读 《深入理解Java虚拟机》这本国人写的神书,没想到国人也能写出如此棒的书。这里只要知道,java代码通过javac编译器编译成了 ByteCode(字节码) 文件,而字节码文件运行在虚拟机上就好了。
我都知道JVM可以执行字节码,那么其是如何执行的呢?
现代虚拟机一般有两种执行方式,根据具体的使用场景各有侧重。例如运行在client端与运行在server端的虚拟机侧重就不同。client端更注重响应性,假如一个程序半天启动不起来,用户是要骂娘。而server端更注重执行效率,启动慢点没关系,反正也不是经常重启。
Interpreter :解释执行。一边把字节码翻译成当前硬件平台的机器码一边执行。优点是启动快,缺点就是执行效率低下。Interpreter 对应的编译器称称为:解释执行编译器。
JIT(Just In Time) :即时编译。当一些代码被频繁执行到时,虚拟机就将其编译成机器码。这些被频繁执行的代码有个专有名词:“热点代码” ,相信大家最熟悉的JVM就是sun公司的的虚拟机HotSpot,其也是因为热点探测技术比较牛逼而得名。JIT 对应的编译器为:即时编译器。
值得说明的是此处只触及到了虚拟机的皮毛,如果有兴趣的同学可以查阅相关资料,虚拟机技术的水那可深啊!
下图描述了JVM执行字节码的两种策略
Java字节码是Java虚拟机规范里的一套指令集,Java虚拟机可以执行由其按照.class文件结构构成的文件,字节码由操作符与参数组成。
Dalvik字节码是JVM字节码的子集,但是JVM执行的.class
文件是基于栈的,而Dalvik/ART执行的.dex
文件是基于寄存器的,所以不可以混用。 上面叨叨了半天JVM就是因为它是Android虚拟机的基础,Android基本是将Java那套东西照搬到了移动设备上。然而因为移动设备资源受限的特殊性,例如电池、内存、CUP的运算能力及功耗等都是受限的,造成了其与Java虚拟机还是有很大的不同的。
有人说Android割裂了Java生态系统,个人觉得是有道理的,因为Java的标准字节码文件.class
文件是不能直接在Android虚拟机上运行的。
Android 虚拟机前后共有两套
.dex
文件,然后运行在Dalvik上,.dex
表示 Dalvik EXecutable,Dalvik 的原理可以类比JVM。 Android5.0 以后就被完全废弃了。.dex
文件编译为对应的.oat
二进制文件,当用户点击App的启动图标时,ART直接加载.oat
文件去执行。其中那个.oat
文件是一个****文件,已经是当前机器的可执行的文件了。从前面的分析可以看出,ART 通过安装时将.dex
预编译为机器的可执行文件,省去Dalvik在运行时才解释或者即时编译的过程而提高执行效率,详情待接下来在分析Android编译器时再说。
一个App从源代码到.apk
安装文件都经历了哪些过程呢?
下图非常清楚的描述了这一过程:
.java
源代码文件编译为.class
字节码文件.class
文件转化为.dex
文件.dex
文件一起生成APK文件。作为一个合格的Android开发者,我认为你应该要记住上面的流程。
这里分老式的 Delvik 和新式的ART 两种情况说明。
因为只有Android4.4 以下的OS才使用Dalvik,而市场上此版本及以下的设备已经很少了,所以无需过于关注相关知识了,但是也应该有所了解,因为后面的ART也是为了解决Dalvik存在的问题才提出的。
如上图所示,在Dalvik虚拟机上首次启动App一定是使用Interpreter解释执行的,期间会探测热点代码,使用JIT编译执行,如果我没记错的话,JIT应该是在Android2.2之后加入的,可见最早期的Android很缓慢。
前面说过,ART是在App安装的时候将.apk
文件减压,并将.dex
文件预编译为.oat
可执行文件,当App启动的时候就不需要在Runtime解释执行了,但是这种方式也有它自己的缺点。
.oat
文件。特别是无论一个App的某一功能是否被使用到以及被使用的频率如何,例如一个App的某个功能用户几乎不会去打开,ART都将其编译为.oat
文件就显得有点低效了。Google
的那些天才工程师既然发现了问题,那肯定就会去想办法优化的。技术的每一次进步,都是站在前面技术的肩膀上的,这一次也不例外,Google的工程师将 Interpreter、JIT与AOT 三种技术相结合来优化这个过程。
.dex
文件编译为.oat
文件,系统通过Interpreter的方式来启动App..oat
可执行文件.oat
文件了下图清楚的说明了上面的过程
一款App经过8轮这样的处理基本上就优化好了。Google的工程师没有止步于此,他们仍然在思考,为什么不把编译配置文件共享呢,那样同款设备就不必自己产生这个文件了?
按照这一思路Google通过Google Play 对这一过程做了更高级的优化:
WIFI
联网的情况.dex
文件选择性的编译为.oat
这样用户在首次安装的时候,AOT就会完成精准的预编译,那么App的启动及运行就会很流畅。但是很遗憾,这个优化距离我们国内还很遥远。。。原因众所周知(也许国内的四大厂商的应用商店也在做吧,这个我没有调查,谁知道可以在评论中告诉我).
到此你已经对App的安装和运行有了一个全局的认识了,已经很好了。如果你想继续深入技术细节就继续往下看吧,保你收获满满。
根据前面的介绍,Android 虚拟机是不能直接执行.class
文件的,而只能执行.dex
文件,所以就必须有一个将Java字节码.class
文件转化为Dalvik字节码.dex
文件的的编译器。
众所周知,Android最为人诟病的就是其碎片化,其中系统版本碎片化也很严重,往往是市场上各种版本的Android系统长期共存,版本收缩速度堪称龟速,甚至有的设备至出厂后就不能够升级,近两年稍有改善。所以一款App往往要同时支持很多版本,那就要求Dex编译器编译出来的.dex
字节码可以同时运行在多个版本的Dalvik/ART上。
Android第一版发布时候使用的是JDK6,即只支持Java6的字节码指令集。但是到现在Java已经发展到Java14了,期间增加了新的字节码,增加了新的语言特性以及新的API。Android生态系统总不能一直让开发者使用Java6来开发吧,那样估计开发者要起来反抗了?所以Google需要想办法支持Java7、8、9…
什么是脱糖呢?这个词是来至语法糖。由于我们老Android版本上的执行环境Dalvik/ART不支持新的语言特性而我们又想要使用,源代码使用了语法糖,那么编译的时候就需要脱糖,例如Lambdas表达式。
Google 在实现脱糖这个功能时也经历了各种尝试,如果有兴趣可以查看 Jake Wharton的这篇博客
脱糖包括两个方面
Java 新语言特性的支持
就是新版本Java 引入的语言特性。 例如lambda
表达式、接口的默认方法以及方法引用等,这些是被最先支持的。支持的方式是通过将这些语言特性还原为对应的老式写法。 这个在插件 Android gradle plugin 3.0.0 ,对应为Android studio 3.0 以上就支持了。
android { ... compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 }}
Java 新语言 Api的支持
对新版本API的支持。例如Java 8 新引入的java.util.stream
已经新的时间api java.time
。这个不好弄了,因为老版本Android携带的jre根本没有这套东西,怎么办呢? 编译器(D8/R8)帮你实现一套,然后打包成.dex
文件加到你的apk中,然后让你的代码使用这里面的实现。 这个在插件Android gradle plugin 4.0.0 ,对应为Android studio 4.0以上才支持。在你的modul中gradle 配置如下代码即可
android { defaultConfig { // Required when setting minSdkVersion to 20 or lower multiDexEnabled true } compileOptions { // Flag to enable support for the new language APIs coreLibraryDesugaringEnabled true // Sets Java compatibility to Java 8 sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 }}dependencies { coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.5'}
D8全称是Dope8,咱也不知道是应该翻译为笨蛋8号呢,还是酷毙了8号,最好还是别翻译了,D8挺好听。其是Android最新的Dex编译器,替换了老的Dex编译器。
以前脱糖这一个过程是作为编译的一个单独步骤进行的,编译为.class
后就是脱糖,脱糖后执行ProGuard,然后再编译为.dex
文件。D8将脱糖和编译为.dex
文件这两步合并为一步来执行了,可以看到脱糖已经在ProGuard之后完成了,D8脱糖后的字节码精确度和执行效率都更高
R8是基于D8的,可以认为R8是D8的一种高级执行模式,其与D8最大的区别是对D8产生Dalvik字节码的过程进行了优化。
D8将Java字节码转换为Dalvik字节码过程是:先将Java字节码转换为 intermediate representation(IR),然后再将IR输出为Dalvik 字节码,从IR到Dalvik字节码的过程中基本不做优化,而R8在此过程中会进行优化。
Java生态一般使用ProGuard对字节码进行优化,而R8将ProGuard这一步的功能给整合到了Dalvik字节码生成环节中了,不管是ProGuard还是R8主要从下面几个方面进行优化:
在我最开始接触Android开发的时候(我以前是写C#的,为了做Android学了Java),一直以为ProGuard的唯一作用就是为了混淆代码,足见一个人刚入门一个行当的时候是多么的无知。
R8 除了完成类似ProGuard的功能外还有一个针对Android生态系统的改进,即根据设备虚拟机和API版本来产生相应的Dalvik字节码。
什么意思呢?我们接下来简单的聊一下:这个问题主要还是由于Android系统的碎片化造成的。最初Android定义了一套Dalvik 字节码指令,并提供了一个dex 编译器,而这个编译器一直没有使用其中的某些Dalvik指令,例如not-int
,其他的手机开发商一看官方的dex编译器都不使用这些指令,那么就懒得支持了(在设备的运行环境Dalvik下支持)。但是当Google 提供新的dex编译器D8的时候,又起用了那些指令。这就尴尬了,使用D8编译的App在老的设备上就崩溃了,因为那个设备的虚拟机压根就不认识D8产生的某些字节码指令。所以D8就需要根据虚拟机版本及API版本来确定产生的相应的字节码,说来说去就是要向下兼容。如果市场上永远只有一个Android版本,根本就不会有这些屁事,但是我们我们是成年人,我们的承认现实!
ProGuard 与 R8的优化功能谁更厉害呢?毫无疑问是ProGuard,这是广大开发者费了15年的时间不断优化的结果,而R8只有Google在搞,而且时间很短,而且只在Android生态系统中使用。 如果仅局限于Android生态系统讨论的话,R8随着不断的发展应该会比ProGuard 更适合。
本文图片均出自
本文只从宏观的角度阐述了一下Android编译相关的问题,如果读者对某个部分感兴趣,应该从字节码层面进行具体的研究。
如果你都看到这里了,我相信本文值得你一赞,给美文点赞是咱程序员的美德。
参考文章: