当前位置: 首页 > 科技观察

都9102年了,还是不知道Android为什么会卡?

时间:2023-03-20 01:30:23 科技观察

近日,华为方舟编译器要开源了。去看了发布会的PPT,发现作为一个Android开发者,对于PPT介绍的知识点,并不能完全理解。所以我弥补了。PPT中的内容整理成这篇文章。本文将用通俗易懂的语言介绍Android冻结的历史原因以及谷歌与之抗争的过程。阅读本文后,您将了解计算机如何解释我们编写的程序并执行相应的功能。了解Android虚拟机进化史从底层了解Android死机的三大原因1.基本概念首先,我们需要学习一些基本概念,了解计算机是如何解释我们编写的程序并执行相应功能的。1.Compile&Interpret一些编程语言(如Java)的源代码,通过编译和解释的过程,可以被计算机读取。程序员的第一课,你只需要写这段代码并执行,电脑或者手机就会打印出HelloWorld。那么问题来了,英语是人类世界的语言,计算机(CPU)是如何理解英语的呢?众所周知,0和1是计算机世界的语言,可以说计算机只懂0和1。那么我们只需要将上面的英文代码通过0和1表达给计算机即可,这样计算机可以理解并执行它。结合上图,将Java源码编译成字节码,然后根据模板中的规则将字节码解释为机器码。2.机器码&字节码机器码机器码是CPU可以直接解释执行的语言。但是,如果将上图生成的机器码在另一台电脑上运行,很可能会失败。这是因为不同的计算机可能解释不同的机器码。通俗地说,就是一种机器码,可以在A电脑上运行,但在B电脑上就不一定能正常运行了。俄语B懂俄语和英语。这时他们俩同时拿了一张语文试卷,B估计连写名字的地方都找不到了。所以这时候我们就需要字节码了。字节码中文A看不懂俄文试卷,俄文B看不懂中文试卷,但是英文试卷大家都能看懂。字节码是一种中间代码。Java可以编译成字节码,同样的字节码可以按照指定模板的规则解释为指定的机器码。字节码的好处:实现了跨平台,只需要将一段源代码编译成字节码,然后根据不同的模板将字节码解释成当前计算机可识别的机器码,这就是Java所谓的“编译一次”,到处跑”。从相同的源代码编译出的字节码的大小比机器码小得多。3.Compiledlanguage&Interpretedlanguage编译语言我们熟悉的C/C++语言是一种编译语言,即程序员编译后一步完成(编译成机器码),可以直接解释执行由中央处理器。可能有人会问,既然上面提到的字节码有各种好处,为什么不用字节码呢?这是因为每种编程语言的初衷不同,有的是为跨平台设计的,比如Java,但有的是为给定的机器或给定型号的一批机器设计的。比如苹果开发的OC语言和Swift语言,都是为自己的产品设计的,你别人的产品我不管。所以OC或者Swift语言设计的初衷之一就是为了快,可以直接编译成机器码供iPhone或者iPad解释执行。这也是苹果手机应用比安卓手机应用大的主要原因。这也是苹果手机更流畅的原因之一!(没有中间商赚差价)编译解释型语言以Android开发语言Java为例。Java是一种编译解释型语言,即程序员不能直接将其编译成机器码,而是编译成字节码(Java程序中为.class文件,Android程序中为.dex文件)。然后我们需要将字节码解释成机器码,以便CPU可以解释。这第二次解释,即把字节码解释成机器码的过程,是在程序安装或运行后,在Java虚拟机中实现的。2、造成口吃的三大因素。今年最新的安卓版本已经是10了,其实这两年安卓手机卡顿的声音已经逐渐减少了,取而代之的是像iOS这样更流畅的声音。不过如果超过iOS的话,还是比较小的。事实上,Android的滞后有三个历史原因。起步低于iOS。1、虚拟机——解释过程慢通过上面的描述我们可以知道iOS之所以不卡是因为一步到位,省去了解释的中间步骤,直接和硬件层通信.但是由于Android没有一步到位,每次执行都需要实时解释成机器码,所以性能明显低于iOS。我们已经清楚地知道字节码(中间人)是造成卡顿的罪魁祸首之一。能不能像iOS那样把字节码扔掉,一步到位呢?显然不是,因为在iOS中只有少数。模型。反观Android,手机的型号数不胜数,CPU架构/型号数不胜数,更不用说平板电脑、汽车等设备了。硬件设备种类繁多,也就意味着硬件架构有很多种,每种架构都有自己对应的机器码解释规则。显然,像iOS那样一步到位是不现实的。那我们该怎么办呢?既然摆脱不了字节码中间人,只能利用他,让整个解释过程越来越快。解释所在的“工厂”在虚拟机中。接下来就是伟大的安卓虚拟机的进化了!①Andorid1.0Dalvik(DVM)+InterpreterDVM是Google开发的Android平台虚拟机,可以读取.dex字节码。上述将字节码解释成机器码的过程是在Java虚拟机中进行的,Android平台中的虚拟机就是指这个DVM。在Android1.0时期,程序运行时,DVM中的解释器(翻译器)对字节码进行解释。可以想象,这绝对是低效的。一个字,卡。②Android2.2DVM+JIT解决DVM问题其实思路很清晰。我们可以在程序运行之前解释程序的某个功能。Android2.2时期,聪明的Google引入了JIT(JustInTime)机制,直译就是即时编译。例如,我经常去一家餐馆,老板已经知道我想要什么,并在我到达之前就准备好了,这样可以节省我等待的时间。JIT相当于这个聪明的老板。手机打开APP时会记录用户经常使用的功能。当用户打开APP时,内容立即被编译,这样当用户打开内容时,JIT就已经准备好了‘菜’。这提高了整体效率。JIT虽然很聪明,整体思路清晰理想,但现实还是卡死了。存在问题:打开APP会变慢。每次打开APP,都要重复工作,不能一劳永逸。如果我突然点了一个我从来没有点过的菜,我必须等待这道菜,所以如果用户打开了不是JIT准备的'菜',我只能等待DVM中的解释器执行和解释起来。③Android5.0ART+AOT聪明的Google又想到了一个办法。既然我们在打开APP的时候就可以把字节码编译成机器码,那为什么不在安装APP的时候把字节码编译成机器码呢?这样就不用每次打开APP都重复工作了,一劳永逸。这确实是一个想法,所以谷歌推出了ART来替代DVM。ART的全称是AndroidRuntime。它在DVM的基础上做了一些优化。它在安装应用程序时将应用程序编译成机器代码。这个过程称为AOT。(Ahead-Of-Time),即预编译。但是问题又来了。打开APP不再卡,但安装APP速度极慢。可能有人会说,一个APP安装不频繁,这个时间可以牺牲掉。但是很抱歉,安卓手机每次OTA启动(即更新系统版本或刷机后)都会重新安装所有应用,无奈!绝望的!是的,还记得那两年被Android版本更新霸占的恐惧吗!④Android7.0混合编译谷歌终于使出了绝招,DVM+JIT不行,ART+AOT不行。好吧,我把它们都混在一起,没关系!所以谷歌在Android7.0发布的时候就发布了混合编译。也就是说,它在安装时没有被编译成机器码。在手机不用的时候,AOT偷偷把代码中可以编译的部分编译成机器码(至于可以编译的部分是什么,下面的字节码编译模板有详细说明)。其实就是在手机没电的情况下偷偷做之前安装APP时做的工作。如果来不及编译,那就调用JIT和解释器,兄弟姐妹们,让他们实时编译或者解释。不得不佩服谷歌粗暴的解决问题方式。这么一来,安卓手机也确实慢慢从万年卡顿的坑里走了出来。⑤Android8.0改进解释器在Android8.0期间,谷歌再次将重点放在了解释器上。其实从上面的问题来看,根本原因还是解释器解释的太慢了!(什么JIT,AOT,老哥只解释了一个字,Fast)那我们为什么不让这个解释器解释的更快呢?于是谷歌改进了解释器,解释模式的执行效率大大提高。⑥Android9.0改进了编译模板这一点将在下面的字节码编译模板中详细介绍。简单来说,Android9.0提供了预置热代码的方式。应用程序在安装时,可以提前知道常用代码会被编译。(借用知乎原话@weishu大神)2.JNI——Java和C慢慢相互调用。JNI又叫JavaNativeInterface,翻译过来就是JavaNativeInterface,用来和C/C++代码进行交互。如果你不做Android开发,你可能不知道Android项目中的代码不仅仅是Java,还有一些C语言代码。这时候,就出现了一个严重的问题。首先上图(图片参考方舟编译原理PPT):在开发阶段,Java源码在开发阶段被打包成.dex文件,C语言直接就是.so库,因为C语言本身就是编译的。语言。在用户手机中,APK中的.dex文件(字节码)会被解释为运行在ART虚拟机中的.oat文件(机器码),而.so库则是其运行的二进制码(机器码)计算机可以直接运行,两份机器码相互调用肯定有开销。让我们解释一下为什么这两个机器码不同。这里需要深入理解字节码->机器码的编译过程。虽然在图中都是编译成机器码,可以直接被硬件调用,但是这两种机器码的性能、效率、实现方式还是有很大区别的,这主要是以下两点造成的:不同的编程语言导致编译后的字节码不同,编译后的机器码也不同。比如对于同样是静态语言的C和Java,inta+b的运算在C语言中可以直接加载到内存中,在寄存器中计算。这是因为C语言是静态语言,a和b是确定的int对象。虽然我们在Java中定义对象,也需要明确指出对象的类型,比如inta=0,但是Java有动态,Java有反射,代理,谁也不能保证a还是int类型什么时候调用,所以Java的编译需要考虑上下文关系,即具体情况,具体编译。所以连字节码不一样,编译出来的机器码肯定也不一样。不同的运行环境导致编译后的机器码不同。图中可以明显看出Java编译出来的机器码是用ART包裹起来的。ART的全称是AndroidRunTime,即Android运行环境,几乎等同于一个虚拟机。C语言所在的运行环境不在ART中。RunTime提供基本的输入和输出或内存管理支持。如果要在两个不同的RunTime中互相调用,肯定会有额外的开销。例如,由于Java有GC(垃圾回收机制),Java中一个对象的地址是不固定的,可能会被GC移动。即对象在ART环境下运行的机器码中的地址是不固定的。但是C语言不管那么多幺蛾子,C直接向Java要一个对象的地址,但是对象地址动了就完了。有两种解决方案:在C中制作此对象的另一个副本。显然这会产生大量开销。告诉ART,我要用这个对象,你不能在GC中移动这个对象的地址!你只要留下来就走。这样开销比较小,但是如果这个地址不能被回收,可能会造成OOM。(这里参考知乎在华为发布的方舟编译器对Android软件生态有多大影响?)的回答3.字节码编译模板——未针对特定APP优化举个例子来理解编译模板,《Helloworld》”可以译为“你好,世界”,也可以译为“你好,世界”。这种差异是编译模板不同造成的。①.统一的编译模板(vmTemplate)字节码可以通过不同的编译模板编译成机器码,编译模板的不同会直接导致编译后的机器码性能差异很大。在Android中,ART有一套规定的、统一的编译模板,暂且称之为VM模板。这套模板虽然不错,但也算不上优秀。因为是谷歌爸爸做的,所以肯定不差,但因为没有针对每个app优化,所以也算不上优秀。②.vm模板的问题是没有针对每个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时期。③.SmartART这个问题虽然存在,但不是特别严重。因为ART没有我说的那么傻。在后续的应用使用中,ART会记录和学习用户的使用习惯(保存热码),然后更新当前APP的定制vm模板,不断补充热码,补充定制模板。这听起来很熟悉吗?这是手机发布会上“根据用户操作习惯学习,APP打开速度不断提升”这一口号的一部分原理。④.最后的大招,一劳永逸其实一劳永逸解决这个问题并不难:我们只需要在吃饭前提前和老板预定好我们想吃的东西,让老板先准备好即可,这样我们到的时候就不用等餐了。在最新的Android9.0版本中,谷歌引入了这个预先安排的功能:构建系统支持在具有蓝图编译规则的原生Android模块上使用Clang的配置文件引导优化(PGO)。说人话:谷歌允许你在开发阶段添加一个配置文件,你可以在其中指定“热点代码”。应用安装后,ART会编译配置文件中指定的“热点代码”firstcode”。虽然谷歌支持,但该技术国内APP开发者资料太缺乏,普及度不高。笔者先贴了官方链接,还有这篇博客,写的还是比较详细的。(隔壁的Xcode有PGO的UI界面)3.解决思路解决思路总结起来就是四个字:华为方舟方舟的解决方案:对于虚拟机问题,Ark说:你这个烂虚拟机我不要了,裸跑吧。对于JNI调用问题,Ark说:我们让Java在编译阶段像C一样直接编译成机器码,把Java干掉虚拟机,直接用.so库调用,无JNI开销问题。关于编译模板问题,方舟说:我们支持针对不同的app进行不同的编译优化。直接打包成机器码.apk(大概不再叫apk),然后直接运行。看来方舟确实解决了三大问题,但是代价是什么呢?如果按照这个思路,方舟一定不仅仅是一个编译器,它应该有自己的运行时。当然,这些都是后话。方舟的实现只是一个大概的思路,并没有深入,因为一来方舟没有开源,二来方舟大会的PPT营销方面比较多,技术细节欠缺。现在奇思妙想完全是纸上谈兵,一切还是等待开源吧。