原文地址:juejin.cn/post/684490…android
转载请署名,严禁抄袭程序员
本文已受权微信公众号:鸿洋(hongyangAndroid)原创首发编程
最近华为方舟编译器要开源了,笔者去看了下发布会PPT,发现做为一名Android开发者,PPT中所介绍的知识点我竟然不能彻底看懂???因而乎恶补了下PPT中的内容,整理成本文。bash
本文将用通俗的语言从底层介绍Android卡顿的历史缘由和谷歌与之斗争的过程微信
阅读完这篇文章后你将markdown
理解计算机是如何解读咱们所写的程序并执行相应功能的架构
了解Android虚拟机的进化史jvm
从底层了解形成Android卡顿的三大缘由编程语言
首先咱们须要补习下一些基础概念,来理解计算机是如何解读咱们所写的程序并执行相应功能的。oop
某些编程语言(如Java)的源代码经过编译-解释的流程可被计算机读懂
先上一段Java代码
public static void main(String[] args){ print('Hello World') } 复制代码
这是全部程序员的第一课,只须要写完这段代码并执行,电脑或手机就会打印出Hello World
。 那么问题来了,英文是人类世界的语言,计算机(CPU)是怎么理解英文的呢?
众所周知,0和1是计算机世界的语言,能够说计算机只认识0和1。 那么咱们只须要把上面那段英文代码只经过0和1表达给计算机,就可让计算机读懂并执行。
结合上图,Java源代码经过编译
变成字节码,而后字节码按照模版中的规则解释
为机器码。
机器码
机器码就是能被CPU直接解读并执行的语言。
可是若是使用上图中生成的机器码跑在另一台计算机中,极可能就会运行失败。
这是由于不一样的计算机,可以解读的机器码可能不一样。通俗而言就是能在A电脑上运行的机器码,放到B电脑上就可能就很差使了。
举个🌰,中国人A认识中文,英语;俄国人B认识俄语,英语。这时他两同时作一张中文试卷,B大概连写名字的地方都找不到。
因此这时候咱们须要字节码。
字节码
中国人A看不懂俄文试卷,俄国人B看不懂中文试卷,可是你们都看得懂英文试卷。
字节码就是个中间码
,Java能编译为字节码,同一份字节码能按照指定模版的规则解释为指定的机器码
。
字节码的好处:
1.实现了跨平台,一份源代码只须要编译成一份字节码,而后根据不一样的模版将字节码解释成当前计算机认识的机器码,这就是Java所说的“编译一次,处处运行”。
2.同一份源码
被编译成的字节码
大小远远小于机器码
。
编译语言
咱们熟知的C/C++语言,是
编译语言
,即程序员编译以后能够一步到位(编译成机器码),能够被CPU直接解读并执行。
可能有人会问,既然上文中说过字节码
有种种好处,为何不使用字节码
呢?
这是由于每种编程语言设计的初衷不一样,有些是为了跨平台而设计的,如Java,但有些是针对某个指定机器或某批指定型号的机器设计的。
举个🌰,苹果公司开发的OC语言和Swift语言,就是针对自家产品设计的,我才无论你其余人的产品呢。因此OC或Swift语言设计初衷之一就是快,可直接编译为机器码使iPhone或iPad解读并执行。这也是为何苹果手机的应用比安卓手机应用大的主要缘由。这更是为何苹果手机更流畅的缘由之一!(没有中间商赚差价)
编译-解释语言
拿开发Android的语言Java为例,Java是编译-解释语言,即程序员编译以后不能够直接编译为机器码,而是会编译成字节码(在Java程序中为.class文件,在Android程序中为.dex文件)。而后咱们须要将字节码再解释成机器码,使之能被CPU解读。
这第二次解释,即从字节码解释成机器码的过程,是程序安装或运行后,在Java虚拟机中实现的。
今年最新的Android版本已是10了,其实在这两年关于Android手机卡顿的声音已经慢慢低了下去,取而代之的是流畅如iOS之类的声音。
可是诸如超过iOS的话,还比较少,实际上是由于Android有卡顿有三大历史缘由。起步就比iOS低。
经过上文描述,咱们能够知道,iOS之因此不卡是由于他一步到位,省略了中间解释的步骤,直接跟硬件层进行通讯。而Android因为没有一步到位,每次执行都须要实时解释成机器码,因此性能较iOS明显低下。
咱们已经明确知道了字节码(中间商)是形成卡顿的主要元凶之一,咱们能否像iOS那样扔掉字节码,直接一步到位呢?
明显不能,由于iOS搞来搞去就那么几个机型。反观Android方面,光手机就有无数种机型,无数种CPU架构/型号,更别提什么平板,车载等其余设备了。有那么多类型的硬件设备表明着就有很是多不一样的硬件架构,每种架构都有本身对应的机器码解释规则。显然像iOS那样一步到位是不现实的。
那怎么办呢?既然扔不掉字节码这个中间商,那咱们只能剥削他咯,让整个解释的过程快一点,再快一点。而解释所在的“工厂”在虚拟机内。
接下来就是伟大的Android虚拟机进化之路!
DVM是Google开发的Android平台虚拟机,可读取.dex的字节码。 上文中所说的从字节码解释成机器码
的过程在Java虚拟机中,在Android平台中虚拟机指的就是这个DVM。 在Android1.0时期,程序一边运行,DVM中的解释器(翻译机)一边解释字节码
。 可想而知,这样效率绝对低下。一个字,卡。
其实解决DVM的问题思路很清楚,咱们在程序某个功能运行前就解释
就能够了。
在Android2.2时期,聪明的谷歌引入了JIT(Just In Time)机制,直译就是即时编译。
举个🌰,我常常去一家餐馆吃饭,老板已经知道我想吃什么菜了,在我到以前就把菜准备好了,这样我就省去了等菜的时间。
JIT就至关于这个聪明的老板,它会在手机打开APP时,将用户常用的功能记下来。当用户打开APP的时候立马将这些内容编译出来,这样当用户打开这些内容时,JIT已经将'菜'准备好了。这样就提升了总体效率。
虽然JIT挺聪明的,且整体思路清晰理想丰满,但现实是仍然卡的要死。
存在的问题:
聪明的谷歌又想到个方法,既然咱们能在打开APP的时候将字节码
编译成机器码
,那么咱们何不在APP安装的时候就把字节码
编译成机器码
呢?这样每次打开APP也不用重复劳动了,一劳永逸。
这确实是个思路,因而谷歌推出了ART来替代DVM,ART全称Android Runtime,它在DVM的基础上作了一些优化,它在应用被安装的时候就将应用编译成机器码
,这个过程称为AOT(Ahead-Of-Time),即预编译。
可是问题又来了,打开APP是不卡了,可是安装APP慢的要死,可能有人会说,一个APP又不是会频繁安装,能够牺牲下这点时间。 可是很差意思,安卓手机每次OTA启动(即系统版本更新或刷机后)都会从新安装全部APP,无奈吧!绝望吧!对,还记得那两年,被安卓版本更新所支配的恐惧吗!
谷歌最终祭出了终极大招,DVM+JIT很差,ART+AOT又很差。行,我把他们都混合起来,那总能够了吧!
因而谷歌在Android7.0的时候,发布了混合编译。 即安装时先不编译成机器码
,在手机不被使用的时候,AOT偷偷的把能编译成机器码
的那部分代码编译了(至于什么是能编译的部分,下文字节码的编译模板
详述)。其实就是把以前APP安装时候干的活偷偷的在手机空的时候干了。
若是来不及编译的话,再把JIT和解释器这对难兄难弟叫起来,让他们去编译或实时解释。
不得不佩服谷歌这粗暴的解决问题的方式,这样一来确实Android手机从万年卡顿慢慢的坑中出来了。
在Android8.0时期,谷歌又盯上了解释器,其实纵观上面的问题,根源就是这个解释器解释的太慢了!(什么JIT,AOT,老夫解释只有一个字,快)那咱们何不让这个解释器解释的快一点呢? 因而谷歌改进了解释器,解释模式执行效率大大提高。
这个点会在下文字节码的编译模板
中详述。
这边简单而言就是,在Android9.0上提供了预先放置热点代码的方式,应用在安装的时候就能知道经常使用代码会被提早编译。(借用知乎@weishu大神的原话)
JNI又称为 Java Native Interface,翻译过来就是Java原生接口,就是用来跟C/C++代码交互的。
若是不作Android开发的可能不知道,Android项目里的代码除了Java,颇有可能还有部分C语言的代码。
这个时候有个严重的问题,首先上图 (图片参考方舟编译器原理PPT):
在开发阶段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要一个对象的地址,但万一这个对象地址被挪动了,那就完蛋了。解决方案有两个:
(此处参考知乎@张铎在华为公布的方舟编译器到底对安卓软件生态会有多大影响?中的回答)
咱们举个🌰来理解编译模版,“Hello world”能够被翻译为“你好,世界”,一样也能够被翻译为“世界,你好”,这个差异就是
编译模版
不一样致使的,
字节码
能够经过不一样的编译模版被编译为机器码
,而编译模版的不一样将直接致使编译完后的机器码
性能截然不同。
在安卓中,ART有一套规定的,统一的编译模版,暂且称为VM模版
,这套模版虽算不上差劲,但也算不上优秀。
由于它是谷歌爸爸搞出来的,确定算不上差劲,但因为没有针对每个APP进行特定的优化,因此也算不上优秀。
问题就存在于没有针对每个APP进行优化。
在上文谷歌对于Android2.2的虚拟机优化
中已经讲到过,那时候谷歌使用JIT将用户经常使用的功能记下来(热点代码),当用户打开APP的时候立马将这些内容编译出来,即优先编译热点代码
。
可是到了Android7.0的混合编译时代,因为AOT的存在,这个功能被弱化了,这时JIT记录下的热点代码并不是是持久化的。AOT的编译优先级遵循于vm模版,AOT根据模板的内容将一些字节码
优先编译为机器码
。
那么这个时候就产生了一个问题。
先举个🌰,一家中餐馆的招牌菜是番茄炒蛋,那么番茄炒蛋的备菜确定很足,可是顾客A特立独行,他恰恰不要吃番茄炒蛋,他每次都点一个冷门的牛排套餐,那这时候只能让顾客等着老板将牛排套餐作完。
若是一个APP的热点代码(如首页),恰好游离于VM模板以外,那么AOT就其实形同虚设了。(好比vm模版优先编译名称不大于15个字符的类和方法,可是首页的类名恰好高于15个字符。此处仅为举例并无实际论证过)
下面用首页和设置页来举例:因为遵循vm模版,AOT由于某个缘由没有优先编译首页部分代码,而转而去编译了不过重要的设置页代码:
上图的流程说明了在特殊状况下,AOT编译实则不起做用,彻底是靠解释器和JIT在进行实时编译,整个编译方案退步到了Android2.2时期。
虽然这个问题存在,但并非特别严重。由于ART并无我说的那么笨。在以后应用使用过程当中,ART会记录并学习用户的使用习惯(保存热点代码
),而后更新针对当前APP的定制化vm模版,不断的补充热点代码
,补充定制化模版
。
这是否是听起来很熟悉?在手机发布大会上的宣传语“基于用户操做习惯进行学习,APP打开速度不断提升”的部分原理就是这个。
其实要一劳永逸的解决这个问题思路也不难:咱们只须要在吃饭前跟老板提早预约想吃啥就行,让老板先准备起来,这样等咱们到了就不用等餐了。
在最新的Android9.0版本中,谷歌推出了这个相似提早预约的功能:编译系统支持在具备蓝图编译规则的原生 Android 模块上使用 Clang 的配置文件引导优化 (PGO)。
说人话:谷歌容许你在开发阶段添加一个配置文件,这个配置文件内可指定“热点代码”,当应用安装完后,ART在后台悄悄编译APP时,会优先编译配置文件中指定的“热点代码”。
虽然谷歌支持,可是这块技术对于APP开发人员而言国内资料过于缺少,普及面不广。笔者先贴上官方连接,以及这篇博客,其中介绍的仍是挺详细的。(隔壁Xcode针对PGO都有UI界面了)
解决思路总结为四个字就是:华为方舟。
方舟的解决思路:
针对虚拟机问题,方舟说:我不要你这个烂虚拟机了,咱们裸奔
针对JNI调用问题,方舟说:咱们让Java在编译阶段跟C同样直接编译成机器码,干掉虚拟机,跟.so库直接调用,毫无JNI开销问题
针对编译模版问题,方舟说:咱们支持针对不一样APP进行不一样的编译优化
总结一下:方舟支持在打包编译阶段针对不一样APP进行不一样的编译优化,而后直接打包成机器码.apk(极可能已经不叫apk了),而后直接运行。
这样看起来方舟确实解决掉了三大问题,可是,代价呢?
若是按照这个思路,方舟就确定不止是一个编译器了,它应该还有一套本身的runtime。固然这些都是后话了。
关于方舟的实现只是大概讲了思路,但没有深刻,由于一来方舟没开源,二来方舟发布会PPT营销层面更多,技术细节缺乏,如今奇思妙想彻底是纸上谈兵,一切仍是静待开源吧。
自从发表文章以来,收到了一些反馈,其中有一种声音是:
形成卡顿的主要缘由是垃圾代码和保活,全家桶等国产软件的锅。
对这一点,我不能否认,垃圾代码,保活策略,全家桶是很恶心。
可是若是要将这些影响上升为形成卡顿的主要缘由,
笔者认为大家是太看得起本身的垃圾代码负优化能力了,仍是太看不起小米,华为这些系统生产厂家了,仍是以为天底下的iOS人手水平高Android一个层次呢?
若是必定要说垃圾代码形成了卡顿,也请去理解下哪些代码是所谓的垃圾代码,好比某些代码形成了内存抖动和GC频繁回收形成了卡顿,不要就扔下一句,垃圾代码而后让程序员背了全部的锅。都9102年了,别再随便甩锅给程序员了!,也请那些这样认为的人别再妄自菲薄了!
至于保活,在如今的华为小米等系统里弄一个全天候保活,互相拉起的进程,大概就会像黑进阿里的黑客同样,次日去公司报道吧。
至于一些千元机的卡顿问题,能够了解下Google新推的Android Go系统,这个系统下的APP开发要求异常的苛刻。