阅读完这篇文章后你将
-
理解计算机是如何解读我们所写的程序并执行相应功能的
-
了解Android虚拟机的进化史
-
从底层了解造成Android卡顿的三大原因
一、基础概念
首先我们需要补习下一些基础概念,来理解计算机是如何解读我们所写的程序并执行相应功能的。
1.编译&解释
某些编程语言(如Java)的源代码通过编译-解释的流程可被计算机读懂
先上一段Java代码
public static void main(String[] args){
print(‘Hello World’)
}
这是所有程序员的第一课,只需要写完这段代码并执行,电脑或手机就会打印出Hello World
。 那么问题来了,英文是人类世界的语言,计算机(CPU)是怎么理解英文的呢?
众所周知,0和1是计算机世界的语言,可以说计算机只认识0和1。 那么我们只需要把上面那段英文代码只通过0和1表达给计算机,就可以让计算机读懂并执行。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q48vfQGw-1638152908400)(https://user-gold-cdn.xitu.io/2019/8/13/16c895846efa7983?imageView2/0/w/1280/h/960/ignore-error/1)]
结合上图,Java源代码通过编译
变成字节码,然后字节码按照模版中的规则解释
为机器码。
2.机器码&字节码
- 机器码
机器码就是能被CPU直接解读并执行的语言。
但是如果使用上图中生成的机器码跑在另外一台计算机中,很可能就会运行失败。
这是因为不同的计算机,能够解读的机器码可能不同。通俗而言就是能在A电脑上运行的机器码,放到B电脑上就可能就不好使了。
举个🌰,中国人A认识中文,英语;俄国人B认识俄语,英语。这时他两同时做一张中文试卷,B大概连写名字的地方都找不到。
所以这时候我们需要字节码。
- 字节码
中国人A看不懂俄文试卷,俄国人B看不懂中文试卷,但是大家都看得懂英文试卷。
字节码就是个中间码
,Java能编译为字节码,同一份字节码能按照指定模版的规则解释为指定的机器码
。
字节码的好处:
《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享
1.实现了跨平台,一份源代码只需要编译成一份字节码,然后根据不同的模版将字节码解释成当前计算机认识的机器码,这就是Java所说的“编译一次,到处运行”。
2.同一份源码
被编译成的字节码
大小远远小于机器码
。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sNG5OcQJ-1638152908434)(https://user-gold-cdn.xitu.io/2019/8/13/16c8955c321ffa8f?imageView2/0/w/1280/h/960/ignore-error/1)]
3.编译语言&解释语言
- 编译语言
我们熟知的C/C++语言,是
编译语言
,即程序员编译之后可以一步到位(编译成机器码),可以被CPU直接解读并执行。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rUxDSIkx-1638152908435)(https://user-gold-cdn.xitu.io/2019/8/13/16c8953744f07738?imageView2/0/w/1280/h/960/ignore-error/1)]
可能有人会问,既然上文中说过字节码
有种种好处,为什么不使用字节码
呢?
这是因为每种编程语言设计的初衷不同,有些是为了跨平台而设计的,如Java,但有些是针对某个指定机器或某批指定型号的机器设计的。
举个🌰,苹果公司开发的OC语言和Swift语言,就是针对自家产品设计的,我才不管你其他人的产品呢。所以OC或Swift语言设计初衷之一就是快,可直接编译为机器码使iPhone或iPad解读并执行。这也是为什么苹果手机的应用比安卓手机应用大的主要原因。这更是为什么苹果手机更流畅的原因之一!(没有中间商赚差价)
- 编译-解释语言
拿开发Android的语言Java为例,Java是编译-解释语言,即程序员编译之后不可以直接编译为机器码,而是会编译成字节码(在Java程序中为.class文件,在Android程序中为.dex文件)。然后我们需要将字节码再解释成机器码,使之能被CPU解读。
这第二次解释,即从字节码解释成机器码的过程,是程序安装或运行后,在Java虚拟机中实现的。
二、造成卡顿的三大因素
今年最新的Android版本已经是10了,其实在这两年关于Android手机卡顿的声音已经慢慢低了下去,取而代之的是流畅如iOS之类的声音。
但是诸如超过iOS的话,还比较少,其实是因为Android有卡顿有三大历史原因。起步就比iOS低。
1.虚拟机——解释过程慢
通过上文描述,我们可以知道,iOS之所以不卡是因为他一步到位,省略了中间解释的步骤,直接跟硬件层进行通信。而Android由于没有一步到位,每次执行都需要实时解释成机器码,所以性能较iOS明显低下。
我们已经明确知道了字节码(中间商)是造成卡顿的主要元凶之一,我们可否像iOS那样扔掉字节码,直接一步到位呢?
明显不能,因为iOS搞来搞去就那么几个机型。反观Android方面,光手机就有无数种机型,无数种CPU架构/型号,更别提什么平板,车载等其他设备了。有那么多类型的硬件设备代表着就有非常多不同的硬件架构,每种架构都有自己对应的机器码解释规则。显然像iOS那样一步到位是不现实的。
那怎么办呢?既然扔不掉字节码这个中间商,那我们只能剥削他咯,让整个解释的过程快一点,再快一点。而解释所在的“工厂”在虚拟机内。
接下来就是伟大的Android虚拟机进化之路!
① Andorid 1.0 Dalvik(DVM)+解释器
DVM是Google开发的Android平台虚拟机,可读取.dex的字节码。 上文中所说的从字节码解释成机器码
的过程在Java虚拟机中,在Android平台中虚拟机指的就是这个DVM。 在Android1.0时期,程序一边运行,DVM中的解释器(翻译机)一边解释字节码
。 可想而知,这样效率绝对低下。一个字,卡。
② Android 2.2 DVM+JIT
其实解决DVM的问题思路很清楚,我们在程序某个功能运行前就解释
就可以了。
在Android2.2时期,聪明的谷歌引入了JIT(Just In Time)机制,直译就是即时编译。
举个🌰,我经常去一家餐馆吃饭,老板已经知道我想吃什么菜了,在我到之前就把菜准备好了,这样我就省去了等菜的时间。
JIT就相当于这个聪明的老板,它会在手机打开APP时,将用户经常使用的功能记下来。当用户打开APP的时候立马将这些内容编译出来,这样当用户打开这些内容时,JIT已经将’菜’准备好了。这样就提高了整体效率。
虽然JIT挺聪明的,且总体思路清晰理想丰满,但现实是仍然卡的要死。
存在的问题:
- 打开APP的时候会变慢
- 每次打开APP都要重复劳动,不能一劳永逸。
- 如果我突然点了一盘之前从来没点过的菜,那我只好等菜了,所以如果用户打开了JIT没有准备好的’菜’,就只能等DVM中的解释器去边执行边解释了。
③ Android 5.0 ART+AOT
聪明的谷歌又想到个方法,既然我们能在打开APP的时候将字节码
编译成机器码
,那么我们何不在APP安装的时候就把字节码
编译成机器码
呢?这样每次打开APP也不用重复劳动了,一劳永逸。
这确实是个思路,于是谷歌推出了ART来替代DVM,ART全称Android Runtime,它在DVM的基础上做了一些优化,它在应用被安装的时候就将应用编译成机器码
,这个过程称为AOT(Ahead-Of-Time),即预编译。
但是问题又来了,打开APP是不卡了,但是安装APP慢的要死,可能有人会说,一个APP又不是会频繁安装,可以牺牲下这点时间。 但是不好意思,安卓手机每次OTA启动(即系统版本更新或刷机后)都会重新安装所有APP,无奈吧!绝望吧!对,还记得那两年,被安卓版本更新所支配的恐惧吗!
④ Android 7.0 混合编译
谷歌最终祭出了终极大招,DVM+JIT不好,ART+AOT又不好。行,我把他们都混合起来,那总可以了吧!
于是谷歌在Android7.0的时候,发布了混合编译。 即安装时先不编译成机器码
,在手机不被使用的时候,AOT偷偷的把能编译成机器码
的那部分代码编译了(至于什么是能编译的部分,下文字节码的编译模板
详述)。其实就是把之前APP安装时候干的活偷偷的在手机空的时候干了。
如果来不及编译的话,再把JIT和解释器这对难兄难弟叫起来,让他们去编译或实时解释。
不得不佩服谷歌这粗暴的解决问题的方式,这样一来确实Android手机从万年卡顿慢慢的坑中出来了。
⑤ Android 8.0 改进解释器
在Android8.0时期,谷歌又盯上了解释器,其实纵观上面的问题,根源就是这个解释器解释的太慢了!(什么JIT,AOT,老夫解释只有一个字,快)那我们何不让这个解释器解释的快一点呢? 于是谷歌改进了解释器,解释模式执行效率大大提升。
⑥ Android 9.0 改进编译模板
这个点会在下文字节码的编译模板
中详述。
这边简单而言就是,在Android9.0上提供了预先放置热点代码的方式,应用在安装的时候就能知道常用代码会被提前编译。(借用知乎@weishu大神的原话)
2.JNI——Java和C互相调用慢
JNI又称为 Java Native Interface,翻译过来就是Java原生接口,就是用来跟C/C++代码交互的。
如果不做Android开发的可能不知道,Android项目里的代码除了Java,很有可能还有部分C语言的代码。
这个时候有个严重的问题,首先上图 (图片参考方舟编译器原理PPT):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iHqmMOCS-1638152908436)(https://user-gold-cdn.xitu.io/2019/8/8/16c707929786b5b2?imageView2/0/w/1280/h/960/ignore-error/1)]
在开发阶段Java源代码在开发阶段打包成.dex文件,C语言直接就是.so库,因为C语言本身就是编译语言。
在用户手机中,APK中的.dex文件(字节码)会被解释为.oat文件(机器码)运行在ART虚拟机中,.so库则为计算机可以直接运行的二进制代码(机器码),两份机器码要互相调用肯定是有开销的。
下面就来阐述下为什么两份机器码会不同。
这边需要深入理解字节码->机器码
的编译过程,在图上虽然都被编译成了机器码,都能被硬件直接调用,但是两份机器码的性能,效率,实现方式相差甚多,这主要是由以下两个点造成的:
- 编程语言不同导致编译出的
字节码
不同导致编译出的机器码
不同。
举个🌰,针对同样是静态语言的C和Java,对int a + b 的运算
C语言可以直接加载内存,在寄存器中计算,这是由于C语言是静态语言,a和b是确定的int对象。
在Java中虽然定义对象我们也要明确的指出对象的类型,例如int a = 0,但是Java拥有动态性,Java拥有反射,代理,谁也不敢保证a在被调用时还是int类型,所以Java的编译需要考虑上下文关系,即具体情况具体编译。
所以连字节码
已经不同了,编译出的机器码
肯定不同。
运行环境
不同导致编译出的机器码
不同
图中明显看到由Java编译而来的机器码
包裹在ART中,ART全称Android RunTime,即安卓运行环境,跟虚拟机差不多是一个意思。而C语言所在的运行环境不在ART中。
RunTime提供了基本的输入输出或是内存管理等支持,如果要在两个不同的RunTime中互相调用,则必然有额外开销。
举个🌰,由于Java有GC(垃圾回收机制),在Java中的一个对象地址不是固定的,有可能被GC挪动了。即在ART环境中跑的机器码中的对象的地址不固定。可是C语言哪管那么多幺蛾子,C就直接问Java要一个对象的地址,但万一这个对象地址被挪动了,那就完蛋了。解决方案有两个:
- 把这个对象在C里再拷一份。很明显这造成了很大的开销。
- 告诉ART,我要用这个对象了,GC这个对象的地址你不能动!你先一边呆着去。这样相对而言开销倒是小了,但如果这个地址如果一直不能被回收的话,可能造成OOM。
(此处参考知乎@张铎在华为公布的方舟编译器到底对安卓软件生态会有多大影响?中的回答)
3. 字节码的编译模板——未针对具体APP进行优化
我们举个🌰来理解编译模版,“Hello world”可以被翻译为“你好,世界”,同样也可以被翻译为“世界,你好”,这个差别就是
编译模版
不同导致的,
①. 统一的编译模版(vm模版)
字节码
可以通过不同的编译模版被编译为机器码
,而编译模版的不同将直接导致编译完后的机器码
性能大相径庭。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lE23dJST-1638152908437)(https://user-gold-cdn.xitu.io/2019/8/12/16c84c98cb8da87a?imageView2/0/w/1280/h/960/ignore-error/1)]
在安卓中,ART有一套规定的,统一的编译模版,暂且称为VM模版
,这套模版虽算不上差劲,但也算不上优秀。
因为它是谷歌爸爸搞出来的,肯定算不上差劲,但由于没有针对每一个APP进行特定的优化,所以也算不上优秀。
②. vm模版存在的问题
被编译为机器码
,而编译模版的不同将直接导致编译完后的机器码
性能大相径庭。
[外链图片转存中…(img-lE23dJST-1638152908437)]
在安卓中,ART有一套规定的,统一的编译模版,暂且称为VM模版
,这套模版虽算不上差劲,但也算不上优秀。
因为它是谷歌爸爸搞出来的,肯定算不上差劲,但由于没有针对每一个APP进行特定的优化,所以也算不上优秀。