Android应用开发以及设计思想深度剖析4.docx
- 文档编号:8572109
- 上传时间:2023-01-31
- 格式:DOCX
- 页数:20
- 大小:412.14KB
Android应用开发以及设计思想深度剖析4.docx
《Android应用开发以及设计思想深度剖析4.docx》由会员分享,可在线阅读,更多相关《Android应用开发以及设计思想深度剖析4.docx(20页珍藏版)》请在冰豆网上搜索。
Android应用开发以及设计思想深度剖析4
特别声明:
本系列文章LiAnLab.org著作权所有,转载请注明出处。
作者系LiAnLab.org资深Android技术顾问吴赫老师。
本系列文章交流与讨论:
@宋宝华Barry
紧接连载三,我们接下从性能的角度分别分析Android系统为应用程序提供的支撑。
1.4性能
Android使用Java作为编程语言,这一直被认为是一局雄心万丈,但凶险异常的险棋。
Java的好处是多,前面我们只是列举了一小部分,但另一种普遍的现象是,Java在图形编程上的应用环境并不是那么多。
除了出于Java编程的目的,我们是否使用过Java编写的应用程序?
我们的传统手机一般都支持JavaME版本,有多少人用过?
我们是否见过Java写就的很流畅的应用程序?
是否有过流行的Java操作系统?
答应应该是几乎为零。
通过这些疑问,我们就可以多少了解,Android这个Java移动操作系统难度了,还不用说,我们前面看到各种设计上的考量。
首先,得给Java正名,并非Java有多么严重的性能问题,事实上,Java本是一种高效的运行环境。
Android在设计上的两个选择,一个Linux内核,一个Java编程语言,都是神奇的万能胶,从服务器、桌面系统、嵌入式平台,它们都在整个计算机产业里表现了强大的实力,在各个领域里都霸主级的神器(当然,两者在桌面领域表现都不是很好,都受到了Windows的强大压力)。
从Java可以充当服务器的编程语言,我们就可以知道Java本身的性能是够强大的。
标准的Java虚拟机还有个特点,性能几乎会与内存与处理器能力成正比,内存越大CPU能力越强,当JIT的威力完全发挥出来时,Java语言的性能会比C/C++写出来的代码的性能还要好。
但是,这不是嵌入式环境的特色,嵌入式设备不仅是CPU与内存有限,而且还不能滥用,需要尽可能节省电源的使用。
这种特殊的需求下,还需要使用Java虚拟机,就得到一个通用性极佳,但几乎无用的JAVAME环境,因为JAVAME的应用程序与本地代码的性能差异实在太大了。
在iPhone引发的“后PC时代”之前,大家没得选择,每台手机上几乎都支持,但使用率很低。
而我们现在看到的Android系统,性能上是绝对不成问题,即便是面对iPhone这样大量使用加速引擎的嵌入式设备,也没有明确差异,这里面窍门在哪里呢?
我们前面也说明了,Java在嵌入式环境里有性能问题,运行时过低的执行效率会引发使用体验问题,同时还有版权问题,几乎没有免费的嵌入式Java解决方案。
如果正向去解,困难重重,那我们是否可以尝试逆向的解法呢?
在计算机工程领域,逆向思维从来都是重要武器之一,当我们在设计、算法上遇到问题时,正向解决过于痛苦时,可以逆向思考一下,往往会有奇效。
比如我们做算法优化,拼了命使用各种手法提高性能,收效也不高,这时我们也可以考虑一下降低其他部分的性能。
成功案例在Android里就有,在Android环境里,大量使用Thumb2这样非优化指令,只优化性能需求最强的代码,这时系统整体的性能反而比全局优化性能更好。
我们做安全机制,试验各种理论模型,最终都失败于复杂带来的开销,而我们反向思考一下,借用进程模型来重新设计,这时我们就得到了“沙盒”这种模型,反而简便快捷。
只要思路开阔,在计算机科学里条条大路通罗马,应该是没有解不掉的问题的。
回到Android的Java问题,如果我们正向地顺着传统Java系统的设计思路会走入痛苦之境,何不逆向朝着反Java的思路去尝试呢?
这时还带来了边际收益,Java语言的版权问题也绕开了。
于是,天才的Android设计者们,得到了基于Dalvik虚拟机方案:
基于寄存器式访问的DalvikVM
Java语言天生是用于提升跨平台能力的,于是在设计与优化时,考虑得更多是如何兼容更多的环境,于它可以运行在服务器级别的环境里,也在运行在嵌入式环境。
这种可伸缩的能力,便源自于它使用栈式的虚拟机实现。
所谓的栈式虚拟机,就是在Java执行环境在边解释边执行的时候,尽可能使用栈来保存操作数与结果,这样可设计更精练的虚拟指令的翻译器,性能很高,但麻烦就在于需要过多地访问内存,因为栈即内存。
比如,我们写一个最简单的Java方法,读两个操作数,先加,然后乘以2,最后返回:
[java] viewplaincopy
1.public int test01( int i1, int i2 ) {
2.int i3 = i1 + i2;
3.return i3 * 2;
4.}
这样的代码,使用Java编译器生成的.class文件里,最后就会有这样的伪代码(虚拟机解析执行的代码):
[plain] viewplaincopy
1.0000:
iload_1 // 01
2.
3.0001:
iload_2 // 02
4.
5.0002:
iadd
6.
7.0003:
istore_3 // 03
8.
9.0004:
iload_3 // 03
10.
11.0005:
iconst_2 // #+02
12.
13.0006:
imul
14.
15.0007:
ireturn
出于栈式虚拟机的特点,这样的伪指令追成的结果会操作到栈。
比如iload_1,iload_2,iadd,istore_3,这四行,就是分别将参数1,2读取到栈里,再调用伪指令add将栈进行加操作,然后将结果写入栈顶,也就是操作数3。
这种操作模式,会使我们的虚拟机在伪指令解析器上实现起来简单,因为简单而可以实现得高效,因为伪指令必然很少(事实上,通用指令用8位的opcode就可以表达完)。
可以再在这种设计基础上,针对不同平台进行具体的优化,由于是纯软件的算法,通用性非常好,无论是32位还是64位机器,都可以很灵活地针对处理器特点进行针对性的实现与优化。
而且这种虚拟机实现,相当于所有操作都会在栈上有记录,对每个伪指令(opcode)都相当于一次函数调用,这样实现JIT就更加容易。
但问题是内存的访问过多,在嵌入式设备上则会造成性能问题。
嵌入式平台上,出于成本与电池供电的因素,内存是很有限的,Android刚开始时使用的所谓顶级硬件配置,也才不过192MB。
虽然现在我们现在的手机动不动就上G,还有2G的怪兽机,但我们也不太可能使用太高的内存总线频率,频率高了则功耗也就会更高,而内存总线的功耗限制也使内存的访问速度并不会太高,与PC环境还是有很大差异的。
所以,直到今天,标准Java虚拟的ARM版本也没有实现,只是使用JAVAME框架里的一种叫KVM的一种嵌入式版本上的特殊虚拟机。
基于Java虚拟机的实现,于是这时就可以使用逆向思维进行设计,栈式虚拟机的对立面就是寄存器式的。
于是Android在系统设计便使用了DanBornstein开发的,基于寄存器式结构的虚拟机,命名源自于芬兰的一个小镇Dalvik,也就是Dalivk虚拟机。
虽然Dalvik虚拟机在实现上有很多技巧的部分,有些甚至还是黑客式的实现,但其核心思想就是寄存器式的实现。
所谓的寄存器式,就是在虚拟机执行过程中,不再依赖于栈的访问,而转而尽可能直接使用寄存器进行操作。
这跟传统的编程意义上的基于寄存器式的系统构架还是有概念上的区别,即便是我们的标准的栈式的标准Java虚拟机,在RISC体系里,我们也会在代码执行被优化成大量使用寄存器。
而这里所指的寄存器,是指执行过程里的一种算法上的思路,就是不依赖于栈式的内存访问,而通过伪指令(opcode)里的虚拟寄存器来进行翻译,这种虚拟寄存器会在运行态被转义成空闲的寄存器,进行直接数操作。
如果这里的解释不够清晰,大家可以把栈式看成是正常的函数式访问,几乎所有的语言都基于栈来实现函数调用,此时虚拟机里每个伪指令(opcode),都类似于基于栈进行了函数调用。
而基于寄存器式,则可以被看成是宏,宏在执行之前就会被编译器翻译成直接执行的一段代码,这时将不再有过多的压栈出栈操作,而是会尽可能使用立即数进行运算。
我们上面标准虚拟机里编译出来的代码,经过dx工具转出来的.dex文件,格式就会跟上面很不一样:
代码编译伪指令
[plain] viewplaincopy
1.9000 0203 0000 add-int v0, v2, v3
2.
3.da00 0002 0002 mul-int/lit8 v0, v0, #int 2 // #02
4.
5.0f00 0004 return v0
对着代码,我们可以看到(或者可以猜测),在Dalvik的伪指令体系里,指令长度变成了16bit,同时,根本去除了栈操作,而通过使用00,02,03这样的虚拟寄存器来进行立即数操作,操作完则直接将结果返回。
大家如果对Dalvik的伪指令体系感兴趣,可以参考Dalvik的指令说明:
像Dalvik这样虚拟机实现里,当我们进行执行的时候,我们就可以通过将00,02,03这样的虚拟寄存器,找一个空闲的真实的寄存器换上去,执行时会将立即数通过这些寄存器进行运算,而不再使用频繁的栈进行存取操作。
这时得到的代码大小、执行性能都得到了提升。
如果寄存器式的虚拟机实现这么好,为什么不大家都使用这种方式呢?
也不是没有过尝试,寄存器试的虚拟机实现一直是学术研究上的一个热点,只是在Dalvik虚拟机之前,没有成功过。
寄存器式,在实际应用中,未必会比栈式更高效,而且如果是通用的Java虚拟机,需要运行在各种不同平台上,寄存器式实现还有着天生的缺陷。
比如说性能,我们也看到在Dalvik的这种伪指令体系里,使用16位的opcode用于实现更多支持,没有栈访问,则不得不靠增加opcode来弥补,再加需要进行虚拟寄存器的换算,这时解析器(Interpreter)在优化时就远比简单的8位解析器要复杂得多,复杂则优化起来更困难。
从上面的函数与宏的对比里,我们也可以看到寄存器实现上的毛病,代码的重复量会变大,原来不停操作栈的8bit代码会变成更长的加寄存器操作的代码,理论上这种代码会使代码体系变大。
之所以在前面我们看到.dex代码反而更精减,只不过是Dalvik虚拟机进行了牺牲通用性代码固化,这解决了问题,但会影响到伪代码的可移植性。
在栈式虚拟机里,都使用8bit指令反复操作栈,于是理论上16bit、32bit、64bit,都可以有针对性的优化,而寄存器式则不可能,像我们的Dalvik虚拟机,我们可以看到它的伪代码会使用32位里每个bit,这样的方式不可能通用,16bit、32bit、64bit的处理器体系里,都需要重新设计一整套新的指令体系,完全没有通用性。
最后,所有的操作都不再经过栈,则在运行态要得到正确的运算操作的历史就很难,于是JIT则几乎成了不可能完成的任务,于是Dalvik虚拟机刚开始则宣称JIT是没有必要,虽然从2.2开始加入了JIT,但这种JIT也只是统计意义上的,并不是完整意义上的JIT运算加速。
所有这些因素,都导致在标准Java虚拟机里,很难做出像Dalvik这样的寄存器式的虚拟机。
幸运的是,我们上面说的这些限制条件,对Android来说,都不存在。
嵌入式环境里的CPU与内存都是有限的资源,我们不太可能通过全面的JIT提升性能,而嵌入式环境以寄存器访问基础的RISC构架为主,从理论上来说,寄存器式的虚拟机将拥有更高的性能。
如果是嵌入式平台,则基本上都是32位的处理器,而出于功耗上的限制,这种状况将持续很长一段时间,于是代码通用性的需求不是那么高。
如果我们放弃全面支持Java执行环境的兼容性,进一步通过固化设计来提升性,这时我们就可以得到一个有商用价值的寄存器式的虚拟机,于是,我们就得到了Dalvik。
Dalvik虚拟机的性能上是不是比传统的栈式实现有更高性能,一直是一个有争议的话题,特别是后面当Dalvik也从2.2之后也不得不开始进行JIT尝试之后。
我们可以想像,基于前面提到的寄存器式的虚拟机的实现原理,Dalvik虚拟机通过JIT进行性能提升会遇到困难。
在Dalvik引入JIT后,性能得到了好几倍的提升,但Dalvik上的这种JIT,并非完整的JIT,如果是栈式的虚拟机实现,这方面的提升会要更强大。
但Dalvik实现本身对于Android来讲是意义非凡的,在Java授权上绕开了限制,更何况在Android诞生时,嵌入式上的硬件条件极度受限,是不太可能通过栈式虚拟机方式来实现出一个性能足够的嵌入式产品的。
而当Android系统通过Dalvik虚拟机成功杀出一条血路,让大家都认可这套系统之后,围绕Android来进行虚拟机提速也就变得更现实了,比如现在也有使用更好的虚拟机来改进Android的尝试,比如标准栈式虚拟机,使用改进版的Java语言的变种,像Scalar、Groovy等。
我们再看看,在寄存器式的虚拟机之外,Android在性能设计上的其他一些特点。
以AndroidAPI为基础,不再以Java标准为基础
当我们对外提供的是一个系统,一种平台之时,就必须要考虑到系统的可持续升级的能力,同时又需要保持这种升级之后的向后兼容性。
使用Java语言作为编程基础,使我们的Android环境,得到了另一项好处,那就是可兼容性的提升。
在传统的嵌入式Linux方案里,受限于有限的CPU,有限的内存,大家还没有能力去实施一套像Android这样使用中间语言的操作系统。
使用C语言还需要加各种各样的加速技巧才能让系统可以运行,基至有时还需要通过硬件来进行加速,再在这种平台上运行虚拟机环境,则很不靠谱。
这样的开发,当然没有升级性可言,连二次开发的能力都很有限,更不用说对话接口了,所谓的升级,仅仅是增加点功能,修改掉一些Bug,再加入再多Bug。
而使用机器可以直接执行的代码,就算是能够提供升级和二次开发的能力,也会有严重问题。
这样的写出来的代码,在不同体系架构的机器(比如ARM、X86、MIPS、PowerPC)上,都需要重新编译一次。
更严重的是,我们的C或者C++,都是通过参数压栈,再进行指令跳转来进行函数调用的,如果升级造成了函数参数变动,则还必须修改所开发的源代码,不然会直接崩溃掉。
而比较幸运的是,所有的嵌入式Linux方案,在Android之前,都没有流行开,比较成功的最多也不过自己陪自己玩,份额很小,大部分则都是红颜薄命,出生时是demo,消亡时也是demo。
不然,这样的产品,将来维护起来也会是个很吐血的过程。
而Android所使用的Java,从一开始就是被定位于“一次编写,到处运行”的,不用说它极强大的跨平台能力,就是其升级性与兼容性,也都是Java语言的制胜法宝之一。
Java编译生成的结果,是.class的伪代码,是需要由虚拟器来解析执行的,我们可以提供不同体系构架里实现的Java虚拟机,甚至可以是不同产商设计生产的Java虚拟机,而这些不同虚拟机,都可以执行已经编译过的.class文件,完全不需要重新编译。
Java是一种高级语言,具有极大的重用性,除非是极端的无法兼容接口变动,都可以通过重载来获得更高可升级能力。
最后,Java在历史曾应用于多种用途的运行环境,于是定义针对不同场合的API标准,这些标准一般被称为JSR(JavaSpecificationRequest),特别是嵌入式平台,针对带不带屏幕、屏幕大小,运算能力,都定义了详细而复杂的标准,符合了这些标准的虚拟机应该会提供某种能力,从而保证符合同一标准的应用程序得以正常执行。
我们的JAVAME,就是这样的产物,在比较长周期内,因为没有可选编程方案,JAVAME在诸多领域里都成为了工业标准,但性能不佳,实用性较差。
JAVAME之所以性能会不佳的一个重要原因,是它只是一种规范,作为规范的东西,则需要考虑到不同平台资源上的不同,不容易追求极致,而且在长期的开发与使用的历史里,一些历史上的接口,也成为了进一步提升的负担。
Android则不一样,它是一个新生事物,它不需要遵守任何标准,即使它能够提供JAVAME兼容,它能得到资源回报也不会大,而且会带来JAVAME的授权费用。
于是,Android在设计上就采取了另一次的反Java设计,不兼容任何Java标准,而只以Android的API作为其兼容性的基础,这样就没有了历史包袱,可以轻装上阵进行开发,也大大减小了维护的工作量。
作为一个Java写的操作系统,但又不兼容任何Java标准,这貌似是比较讽刺的,但我们在整个行业内四顾一下,大家应该都会发现这样一种特色,所有不支持JAVAME标准的系统,都发展得很好,而支持JAVAME标准,则多被时代所淘汰。
这不能说JAVAME有多大的缺陷,或是晦气太重,只不过靠支持JAVAME来提供有限开发能力的系统,的确也会受限于可开发能力,无法走得太远罢了。
这样会不会导致Java写的代码在Android环境里运行不起来呢?
理论上来说不会。
如果是一些Java写的通用算法,因为只涉及语言本身,不存在问题。
如果是代码里涉及一些基本的IO、网络等通用操作,Android也使用了Apache组织的Harmony的JavaIO库实现,也不会有不兼容性。
唯一不能兼容的是一些Java规范里的特殊代码,像图形接口、窗口、Swing等方面的代码。
而我们在Android系统里编程,最好也可以把算法与界面层代码分离,这样可以增加代码复用性,也可以保证在UI编程上,保持跟Android系统的兼容性。
Android的版本有两层作用,一是描述系统某一个阶段性的软硬件功能,另外就是用于界定API的规范。
描述功能的作用,更多地用于宣传,用于说该版本的Android是个什么东西,也就是我们常见的食物版本号,像éclair(2.0,2.1),Froyo(2.2),Gingerbread(2.3),IcecreamSandswich(4.0),JellyBean(4.1),大家都可以通过这些美味的版本号,了解Android这个版本有什么功能,有趣而易于宣传。
而对于这样的版本号,实际上也意味着API接口上的升级,会增加或是改变一些接口。
所谓的API版本,是位于应用程序层与Framework层之间的一层接口层,如下所示:
应用程序只通过AndroidAPI来对下进行访问,而我们每一个版本的Android系统,都会通过Framework来对上实现一套完整的API接口,提供给应用程序访问。
只要上下两层这种调用与被调用的需求能够在一定范围内合拍,应用程序所需要的最低API版本低于Framework所提供的版本,这时应用程序就可以正常执行。
从这个意义来说,API的版本,更大程度算是AndroidFramework实现上的版本,而Android系统,我们也可以看成就是Android的Framework层。
这种机制在Android发展过程中一直实施得很好,直到Android2.3,都保持了向前发展,同时也保持向后兼容。
Android2.3也是Android历史上的一个里程碑,一台智能手机所应该实现的功能,Android2.3都基本上完成了。
但这时又出现了平板(pad)的系统需求,从2.3又发展出一个跟手机平台不兼容的3.0,然后这两个版本再到4.0进行融合。
在2.3到4..0,因为运行机制都有大的变动,于是这样的兼容性遇到了一定的挑战,现在还是无法实现100%的从4.0到2.3的兼容。
只兼容自己API,是Android系统自信的一种体现,同时,也给它带来另一个好处,那就是可以大量使用JNI加速。
大量使用JNI加速
JNI,全称是Java本地化接口层(JavaNativeInterface),就是通过给Java虚拟机加动态链接库插件的方式,将一些Java环境原本不支持功能加入到系统中。
我们前面说到过Dalvik虚拟机在JIT实现上有缺陷,这点在Android设计人员的演示说明里,被狡猾地掩盖了。
他们说,在Android编程里,JIT不是必须的,Android在2.2之前都不提供JIT支持,理由是应用程序不可能太复杂,同时Android本身是没有必要使用JIT,因为系统里大部分功能是通过JNI来调用机器代码(Native代码)来实现的。
这点也体现了Android设计人员,作为技术狂热者的可爱之处,类似这样的错误还不少,比如Android刚开始的设计初衷是要改变大家编程的习惯,要通过Android应用程序在概念上的封装,去除掉进程、线程这样底层的概念;甚至他们还定义了一套工具,希望大家可以像玩积木一样,在图形界面里拖拉一下,在完全没有编程背景的情况下也可以编程;等等。
这些错误当然也被Android强大的开源社区所改正了。
但对于Android系统性能是由大量JNI来推进的,这点诊断倒没有错,Android发展也一直顺着这个方向在走。
Android系统实现里,大量使用JNI进行优化,这也是一个很大的反Java举动,在Java的世界里,为了保持在各个环境的兼容性,除了Java虚拟机这个必须与底层操作系统打交道的执行实体,以及一些无法绕开底层限制的IO接口,Java环境的所有代码,都尽可能使用Java语言来编写。
通过这种方式,可以有效减小平台间差异所引发的不兼容。
在Java虚拟机的开发文档里,有详尽的JNI编程说明,同时也强烈建议,这样的编程接口是需要避免使用的,使用了JNI,则会跟底层打上交道,这时就需要每个体系构架,每个不同操作系统都提供实现,并每种情况下都需要编译一次,维护的代价会过高。
但Android则是另一个情况,它只是借用Java编程语言,应用程序使用的只是AndroidAPI接口,不存在平台差异性问题。
比如我们要把一个Android环境运行在不那么流行的MIPS平台之上,我们需要的JNI,是Android源代码在MIPS构架上实现并编译出来的结果,只要API兼容,则就不存在平台差异性。
对于系统构架层次来说,使用JNI造成的差异性,在Framework层里,就已经被屏蔽掉了:
如上图所示,Android应用程序,只知道有Framework层,通过API接口与Framework通信。
而我们底层,在Library层里,我们就可以使用大量的JNI,我们只需要在Framework向上的部分保持统一的接口就可以了。
虽然我们的Library层在每个平台上都需要重新编译(有些部分可能还需要重新实现),但这是平台产商或是硬件产商的工作,应用程序开发者只需要针对API版本写代码,而不需要关心这种差异性。
于是,我们即得到高效的性能(与纯用机器实现的软件系统没有多少性能差异),以使用到Java的一些高级特性。
单进程虚拟机
使用单进程虚拟机,是Android整个设计方案里反Java的又一表现。
我们前面提到了,如果在Android系统里,要构建一种安全无忧的应用程序加载环境,这时,我们需要的是一种以进程为单位的“沙盒(Sandbox)”模型。
在实现这种模型时,我们可以有多种选择,如果是遵循Java原则的话,我们的设计应该是这个样子的:
我们运行起Java环境,然后再在这个环境里构建应用程序的基于进程的“小牢房”。
按照传统的计算机理论,或是从资源有效的角度考虑,特别是如果需要使用栈式的标准Java虚拟机,这些都是没话说的,只有这种构建方式才最优。
Java虚拟机,之所以被称为虚拟机,是因为它真通过一个用户态的虚拟机进程虚拟出了一个虚拟的计算机环境,是真正意义上的虚拟机。
在这个环境里,执行.class写出来的伪代码,这个世界里的一切都是由对象构成的,支持进程,信号,Stream形式访问的文件等一切本该是实际操作系统所支持的功能。
这样就抽象出来一个跟任何平台无关的Java世界。
如果在这个Java虚拟世界时打造“
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Android 应用 开发 以及 设计 思想 深度 剖析