不知道怎么回事隐私访问空间所有应用都禁了。访问限制没开过一直是允许变更的。现在相机、麦应用里都用不了

本文内容,主题是透过应用程序来分析Android系统的设计原理与构架。我们先会简单介绍一下Android里的应用程序编程,然后以这些应用程 序在运行环境上的需求来分析出,为什么我们的Android系统需要今天这样的设计方案,这样的设计会有怎样的意义, Android究竟是基于怎样的考虑才变成今天的这个样子,所以本文更多的分析Android应用程序设计背后的思想,品味良好架构设计的魅力。分五次连 载完成,第一部分是最简单的部分,解析Android应用程序的开发流程。

Android应用程序开发以及背后的设计思想深度剖析 1

在这个图形界面的示例里,我们可以看到,这样的图形编程方式,比如传统的方式里要学习大量的API要方便得多。

<LinearLayout>,这一标签会决定应用程序如何在界面里摆放相应的控件

使用这种XML构成的UI界面,是MVC设计的附属产品,但更大的好处是,有了标准化的XML结构,就可以创建可以用来画界面的IDE工具。一流的系统提供工具,让设计师来设计界面、工程师来逻辑,这样生产出来的软件产品显示效果与用户体验会更佳,比如iOS;二流的系统,界面与逻辑都由工程师来完成,在这种系统上开发出来的软件,不光界面不好看,用户体验也会不好。我们比如在Eclipse里的工程里查看,我们会发现,我们打开res/layout//training/basics/supporting-devices/查看具体的使用方式。

于是,透过资源文件,我们进一步验证了我们对于Android MVC的猜想,在Android应用程序设计里,也跟iOS类似,可以实现界面与逻辑完全分离。而另一点,就是Android应用程序天然具备屏幕自适应的能力,这一方面带来的影响是Android应用程序天生具备很强的适应性,另一方面的影响是Android里实现像素精度显示的应用程序是比较困难的,维护的代价很高。

我们可以再通过应用程序的代码部分来看看应用程序是如何将显示与逻辑进行绑定的。

package=",Java等都有这样的加强的健壮性。

  • 性能。我们并不总是需要再编译执行的,上次翻译出来的代码,我们也可以缓冲起来,下次支持调用机器代码,这样,Java的执行效率跟实际机器代码的效率相关不大。因为虚拟机是软件,我们可以在虚拟机环境里追踪代码的执行历史,在这种基础上,可以更容易进行虚拟机里面代码的执行状况分析,甚至加入自动化优化,甚至可以超过真实机器的执行效率。比如,在Java环境里,我们执行过一次都是同样的道理。比如安全性,如果我们上面描述的执行模型是纯Java的,则其安全性得到进一步提升。

    但是,纯Java环境不要说在嵌入式平台上,就是在PC环境里,也是以缓慢淡定著称的,Java的自优化能力与处理器能力、内存大小成正比。使用纯Java写嵌入式方案,最终达到的结果也就只会是奇慢无比的JAVA ME。另外,Java是使用商业授权的语言,无论是在以前它的创建者Sun公司,还是现在已经收购Sun公司的Oracle,对Java一贯会收取不低的商业授权费用,一旦基于纯粹Java环境来构建系统,最后肯定会造成Android系统不再是免费的午餐。既然Android所需要的环境只是Java语言本身,原始的Java虚拟机的授权又难以免费,这就迫使Android的开发者,开发出来另一套Java虚拟机环境,也就是我们的Dalvik虚拟机。

    于是,我们基于多进程模型的系统构架,出于跨平台、安全、编程的简易性等多方面的原因,使我们得到的Android的设计方案成为下面的这个新的样子:核心进程这部分的实现我们还没分析到,但应用程序此时在引入Java环境之后,都变成了通过Dalvik虚拟机所管理起来的更受限的环境,于是更安全。

    而在Java环境里,一个Java程序的主入口实际上还是传统的main()入口的方式,而以main()方法作为主入口,则意味着编程时,特别是图形界面编程,需要用户更多地考虑如何实现,如何进行交互。整个Java环境,全都是由一个个的有一定生存周期的对象组合而成,任何一个对象里存在的static属性的main()方法,则可以在Java环境里作为一个程序的主入口得到执行。如果使用标准Java编程,则我们的图形界面编程将复杂得多,比如我们下面的使用Swing编程写出来的,跟我们前面的Helloworld类似的例子:

    交叉编译器、Bionic C库与Dalvik虚拟机的移植(如果不是ARM、X86和MIPS这三种基本构架) 提供板卡支持所需要的配置 实现所需要使用的HAL Android产品化,完成界面或是功能上的定制

    这些移植过程的步骤如下图所示:

    对于我们做Android移植与系统级开发而言,可能我们所需要花的代码并不是那么大。像Bootloader与Linux内核的移植,这一般都在进行Android系统移植时早就会就绪的,比如我们去选购某个产商的主芯片时(Application Processor,术语为AP),这些Android之前的支持大都已经就绪。而作为产业的霸主,我们除非极其特殊的情况,我们也不需要接触交叉编译器和Dalvik虚拟机的移植。所以一般情况下,我们的Android移植是从建立repo源代码管理环境开始,然后再进行板卡相关的配置,然后实现HAL。而Android的产品化这个步骤,Framework的细微调整与编写自己平台上特殊的应用程序,严格意义上来说,Framework也不属于Android移植工作范围内的,我们一般把它定位于Android产品化或是Android定制化这个步骤里。Android移植相对来说非常简单,而真正完成Android产品化则会是一个比较耗时耗人力的过程。

    所谓的板卡的配置文件,一般是放在一个专门的目录里,在2.3以前,是发在vendor目录下,从2.3开始,vendor目录只存放二进制代码,配置文件移到了device目录。在这一目录里,会以“产商名/设备名”的命名方式来规范配置的目录结构。比如是产商名是ti,设备名是panda,则会以“device/ti/panda”来存放这些配置文件,再在这个目录里放置平台相关的配置项。配置文件,则是会几个关键文件构成:

    } AndroidProducts.mk,这是会被编译系统扫描的文件,通过在这一文件里再导入具体的编译配置文件,比如ti_panda.mk

    } ti_panda.mk,在这一文件里定义具体的产品名,设备名这些关键变量,这些变量是在Android编译过程里起关键配置作用的变量。一般说来,这个文件不会很复杂,主要依赖导入一些其他的配置文件来完成所有的配置,比如语言配置等。而设备特殊的设置,则一般是使用同目录下的device.mk文件来进行定制化的设置。

    } device.mk,在这一文件会使用一些更加复杂一些配置,包含一些需要编译的子工程,设置某些特殊的编译参数,以及进行系统某些特性的定制化,比如需要自定义怎样的显示效果、配置文件等

    } BoardConfig.mk,在这一文件则是板子相关的一些定制项,以宏的方式传入到编译过程里,比如BOARD_SYSTEMIMAGE_PARTITION_SIZE来控制system分区的大小, TARGET_CPU_SMP来控制是否需要使用SMP(对称多处理器)支持等。一般,对于同一个板卡环境,这些参数可以照抄,勿须修改。

    所有的这些配置文件,并不是必须的,只不过是建议性的,在这一点上也常会透露出产商在开源文化的素质。毕竟是开源的方案,如果都使用约定俗成的解决方案,则大家都会不用看也知道怎么改。但一些在开源做得不好的厂商,对这些的配置环境都喜欢自己搞一套东西出来,要显得自己与众不同,所以对于配置文件的写法与移植过程,也需要具体情况具体对待。

    当我们完成了这些配置上的工作后,可以先将这些配置上传到repo的服务器管理起来,剩下的移植工作就是实现所需要的HAL了。在Android移植过程里,很多HAL的实现,是可以大量复用的,可以找一个类似的配置复制过来,然后再进行细微调整,比如使用ALSA ASoC框架的音频支持,基本功能都是通用的,只需要在Audio Path和HiJack功能上进行微调即可。

    /tech/dalvik/dalvik-bytecode.html
    像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在性能设计上的其他一些特点。

    当我们对外提供的是一个系统,一种平台之时,就必须要考虑到系统的可持续升级的能力,同时又需要保持这种升级之后的向后兼容性。使用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(Java Specification Request),特别是嵌入式平台,针对带不带屏幕、屏幕大小,运算能力,都定义了详细而复杂的标准,符合了这些标准的虚拟机应该会提供某种能力,从而保证符合同一标准的应用程序得以正常执行。我们的JAVA ME,就是这样的产物,在比较长周期内,因为没有可选编程方案,JAVA ME在诸多领域里都成为了工业标准,但性能不佳,实用性较差。
    JAVA ME之所以性能会不佳的一个重要原因,是它只是一种规范,作为规范的东西,则需要考虑到不同平台资源上的不同,不容易追求极致,而且在长期的开发与使用的历史里,一些历史上的接口,也成为了进一步提升的负担。Android则不一样,它是一个新生事物,它不需要遵守任何标准,即使它能够提供JAVA ME兼容,它能得到资源回报也不会大,而且会带来JAVA ME的授权费用。于是,Android在设计上就采取了另一次的反Java设计,不兼容任何Java标准,而只以Android的API作为其兼容性的基础,这样就没有了历史包袱,可以轻装上阵进行开发,也大大减小了维护的工作量。作为一个Java写的操作系统,但又不兼容任何Java标准,这貌似是比较讽刺的,但我们在整个行业内四顾一下,大家应该都会发现这样一种特色,所有不支持JAVA ME标准的系统,都发展得很好,而支持JAVA ME标准,则多被时代所淘汰。这不能说JAVA ME有多大的缺陷,或是晦气太重,只不过靠支持JAVA ME来提供有限开发能力的系统,的确也会受限于可开发能力,无法走得太远罢了。
    这样会不会导致Java写的代码在Android环境里运行不起来呢?理论上来说不会。如果是一些Java写的通用算法,因为只涉及语言本身,不存在问题。如果是代码里涉及一些基本的IO、网络等通用操作,Android也使用了Apache组织的Harmony的Java IO库实现,也不会有不兼容性。唯一不能兼容的是一些Java规范里的特殊代码,像图形接口、窗口、Swing等方面的代码。而我们在Android系统里编程,最好也可以把算法与界面层代码分离,这样可以增加代码复用性,也可以保证在UI编程上,保持跟Android系统的兼容性。
    Android的版本有两层作用,一是描述系统某一个阶段性的软硬件功能,另外就是用于界定API的规范。描述功能的作用,更多地用于宣传,用于说该版本的Android是个什么东西,也就是我们常见的食物版本号,像éclair(2.0,2.1),Froyo(2.2), Gingerbread(2.3),Icecream Sandswich(4.0),Jelly Bean(4.1),大家都可以通过这些美味的版本号,了解Android这个版本有什么功能,有趣而易于宣传。而对于这样的版本号,实际上也意味着API接口上的升级,会增加或是改变一些接口。
    所谓的API版本,是位于应用程序层与Framework层之间的一层接口层,如下所示:

    应用程序只通过AndroidAPI来对下进行访问,而我们每一个版本的Android系统,都会通过Framework来对上实现一套完整的API接口,提供给应用程序访问。只要上下两层这种调用与被调用的需求能够在一定范围内合拍,应用程序所需要的最低API版本低于Framework所提供的版本,这时应用程序就可以正常执行。从这个意义来说,API的版本,更大程度算是Android
    这种机制在Android发展过程中一直实施得很好,直到Android 2.3,都保持了向前发展,同时也保持向后兼容。Android 2.3也是Android历史上的一个里程碑,一台智能手机所应该实现的功能,Android2.3都基本上完成了。但这时又出现了平板(pad)的系统需求,从2.3又发展出一个跟手机平台不兼容的3.0,然后这两个版本再到4.0进行融合。在2.3到4..0,因为运行机制都有大的变动,于是这样的兼容性遇到了一定的挑战,现在还是无法实现100%的从4.0到2.3的兼容。
    只兼容自己API,是Android系统自信的一种体现,同时,也给它带来另一个好处,那就是可以大量使用JNI加速。

    JNI,全称是Java本地化接口层(Java Native Interface),就是通过给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编程语言,应用程序使用的只是Android API接口,不存在平台差异性问题。比如我们要把一个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虚拟世界时打造“沙盒”模式,则只能使用同一个Java虚拟机环境(这不光是进程,因为Java虚拟机内部还可以再创建出进程),这样就可以通过统一的垃圾收集器进行有效的对象管理,同时,多进程则内存有可能需要在多个进程空间里进行复制,在使用同一个Java虚拟机实例里,才有可能通过对象引用减小复制,不使用统一的Java虚拟机环境管理,则可复用性就会很低。
    但这种方案,存在一些不容易解决的问题。这种单Java虚拟机环境的假设,是建立在标准Java虚拟机之上的,但如前面所说,这样的选择困难重重,于是Android是使用Dalvik虚拟机。这种单虚拟机实例设计,需要一个极其强大稳定的虚拟机实现,而我们的Dalvik虚拟机未必可以实现得如此功能复杂同时又能保证稳定性(简单稳定容易,复杂稳定则难)。Android必须要使用大量的JNI开发,于是会进一步破坏虚拟机的稳定性,如果系统里只有一个虚拟机实例,则这个实例将会非常脆弱。当在Java环境里的进程,有恶意代码或是实现不当,有可能破坏虚拟机环境,这时,我们只能靠重启虚拟机来完成恢复,这时会影响到虚拟机里运行的其他进程,失去了“沙盒”的意义。最后,虚拟机必须给预足够的权限运行,才能保证核心进程可访问硬件资源,则权限控制有可能被某个恶意应用程序破坏,从而失去对系统资源的保护。
    Android既然使用是非标准的Dalvik虚拟机,我们就可以继续在反Java的道路上尝试得更远,于是,我们得到的是Android里的单进程虚拟机模型。在基于Dalvik虚拟机的方案里,虚拟机的作用退回到了解析器的阶段,并不再是一个完整的虚拟机,而只是进程中一个用于解析.dex伪代码的解析执行工具:

    在这种沙盒模式里,每个进程都会执行起一个Dalvik虚拟机实例,应用程序在编程上,只能在这个受限的,以进程为单位的虚拟机实例里执行,任何出错,也只影响到这个应用程序的宿主进程本身,对系统,对其他进程都没有严重影响。这种单进程的虚拟机,只有当这个应用程序被调用到时才予以创建,也不存在什么需要重启的问题,出了错,杀掉出错进程,再创建一个新的进程即可。基于uid/gid的权限控制,在虚拟机之外的实现,应用程序完全不可能通过Java代码来破坏这种操作系统级别的权限控制,于是保护了系统。这时,我们的系统设计上反有了更高的灵活度,我们可以放心大胆地使用JNI开发,同时核心进程也有可能是直接通过C/C++写出来的本地化代码来实现,再通过JNI提供给Dalvik环境。而由于这时,由于我们降低了虚拟机在设计上的复杂程序,这时我们的执行性能必然会更好,更容易被优化。
    当然,这种单进程虚拟机设计,在运行上也会带来一些问题,比如以进程以单位进行GC,数据必然在每个进程里都进行复制,而进程创建也是有开销的,造成程序启动缓慢,在跨进程的Intent调用时,严重影响用户体验。Java环境里的GC是标准的,这方面的开销倒是没法绕开,所以Android应用程序编程优化里的重要一招就是减小对象使用,绕开GC。但数据复制造成的冗余,以及进程创建的开销则可以进行精减,我们来看看Android如何解决这样的问题。

    在几乎所有的Unix进程管理模型里,都使用延时分配来处理代码的加载,从而达到减小内存使用的作用,Linux内核也不例外。所谓的进程,在Linux内核里只是带mm(虚存映射)的task_struct而已,而所谓的进程创建,就是通过fork()系统调用来创建一个进程,而在新创建的进程里使用execve()系列的系统调用来执行新的代码。这两个步骤是分两步进行,父进程调用fork(),子进程里调用execve():

    上图的实线代表了函数调用,虚线代码内存引用。在创建一个进程执行某些代码时,一个进程会调用fork(),这个fork()会通过libc,通过系统调用,转入内核实现的sys_fork()。然后在sys_fork()实现里,这时就会创建新的task_struct,也就是新的进程空间,形成父子进程关系。但是,这时,两个进程使用同一个进程空间。当被创建的子进程里,自己主动地调用了execve()系列的函数之后,这时才会去通过内核的sys_execve()去尝试解析和加载所要执行的文件,比如a.out文件,验证权限并加载成功之后,这时才会建立起新的虚存映射(mm),但此时虽然子进程有了自己独立的进程空间,并不会分配实际的物理内存。于是有了自己的进程空间,当下次执行到时,才会通过一次缺页中断加载a.out的代码段,数据段,而此时,libc.so因为两个进程都需要使用,于是会直接通过一次内存映射来完成。
    通过Linux的进程创建,我们可以看到,进程之间虽然有独立的空间,但进程之间会大量地通过页面映射来实现内存页的共享,从而减小内存的使用。虽然在代码执行过程中都会形成它自己的进程空间,有各自独立的内存类,但对于可执行文件、动态链接库等这些静态资源,则在进程之间会通过页面映射进行共享进行共享。于是,可以得到的解决思路,就是如何加强页面的共享。
    加强共享的简单一点的思路,就是人为地将所有可能使用到的动态链接库.so文件,dalvik虚拟机的执行文件,都通过强制读一次,于是物理内存里便有了存放这些文件内容的内存页,其他部分则可以通过mmap()来借用这些被预加载过的内存页。于是,当我们的用户态进程被执行时,虽然还是同样的执行流程,但因为内存里面有了所需要的虚拟机环境的物理页,这时缺页中断则只是进行一次页面映射,不需要读文件,非常快就返回了,同时由于页面映射只是对内存页的引用,这种共享也减小实际物理页的使用。我们将上面的fork()处理人为地改进一下,就可以使用如下的模式:

    这时,对于任一应用程序,在dalvik开始执行前,它所需要的物理页就都已经存在了,对于非系统进程的应用程序而言,它所需要使用的Framework提供的功能、动态链接库,都不会从文件系统里再次读取,而只需要通过page_fault触发一次页面映射,这时就可以大大提供加载时的性能。然后便是开始执行dalvik虚拟,解析.dex文件来执行应用程序的独特实现,当然,每个classes.dex文件的内容则是需要各自独立地进行加载。我们可以从.dex文件的解析入手,进一步加强内存使用。
    Android环境里,会使用dx工具,将.class文件翻译成.dex文件,.dex文件与.class文件,不光是伪指令不同,它们的文件格式也完全不同,从而达到加强共享的目的。标准的Java一般使用.jar文件来包装一个软件包,在这个软件里会是以目录结构组织的.class文件,比如org/lianlab/hello/Hello.class这样的形式。这种格式需要在运行时进行解压,需要进行目录结构的检索,还会因为.class文件里分散的定义,无法高效地加载。而在.dex文件里,所有.class文件里实现的内容,会合并到一个.dex文件里,然后把每个.class文件里的信息提取出来,放到同一个段位里,以便通过内存映射的方式加速文件的操作与加载。

    这时,我们的各个不同的.class文件里内容被检索并合并到同一个文件里,这里得到的.dex文件,有特定情况下会比压缩过的.jar文件还要小,因为此时可以合并不同.class文件里的重复定义。这样,在可以通过内存映射来加速的基础上,也从侧面降低了内存的使用,比如用于.class的文件系统开销得到减小,用于加载单个.class文件的开销也得以减小,于是得到了加速的目的。
    这还不是全部,需要知道,我们的dalvik不光是一个可执行的ELF文件而已,还是Java语言的一个解析器,这时势必需要一些额外的.class文件(当然,在Android环境里,因为使用了与Java虚拟机不兼容的Dalvik虚拟机,这样的.class文件也会被翻译成.dex文件)里提供的内容,这些额外的文件主要就是Framework的实现部分,还有由Harmony提供的一些Java语言的基本类。还不止于此,作为一个系统环境,一些特定的图标,UI的一些控件资源文件,也都会在执行过程里不断被用到,最好我们也能实现这部分的预先加载。出于这样的目的,我们又会面临前面的两难选择,改内核的page_fault处理,还是自己设计。出于设计上的可移植性角度考虑,还是改设计吧。这时,就可以得到Android里的第一个系统进程设计,Zygote。
    我们这时对于Zygote的需求是,能够实现动态链接库、Dalvik执行进程的共享,同时它最好能实现一些Java环境里的库文件的预加载,以及一些资源文件的加载。出于这样的目的,我们得到了Zygote实现的雏形:

    这时,Zygote基本上可以满足我们的需求,可以加载我们运行一个应用程序进程除了classes.dex之外的所有资源,而我们前面也看到.dex这种文件格式本身也被优化过,于是对于页面共享上的优化基本上得以完成了。我们之后的操作完全可以依赖于zygote进程,以后的设计里,我们就把所有的需要特权的服务都在zygote进程里实现就好了。
    有了zygote进程则我们解决掉了共享的问题,但如果把所有的功能部分都放在Zygote进程里,则过犹不及,这样的做法反而更不合适。Zygote则创建应用程序进程并共享应用程序程序所需要的页,而并非所有的内存页,我们的系统进程执行的绝大部分内容是应用程序所不需要的,所以没必要共享。共享之后还会带来潜在问题,影响应用程序的可用进程空间,另外恶意应用程序则可以取得我们系统进程的实现细节,反而使我们的辛辛苦苦构建的“沙盒”失效了。
    Zygote,英文愿意是“孵化器”的意思,既然是这种名字,我们就可以在设计上尽可能保持其简单性,只做孵化这么最简单的工作,更符合我们目前的需求。但是还有一个实现上的小细节,我们是不是期望zygote通过fork()创建进程之后,每个应用程序自己去调用exec()来加载dalvik虚拟机呢?这样实现也不合理,实现上很丑陋,还不安全,一旦恶意应用程序不停地调用到zygote创建进程,这时系统还是会由于创建进程造成的开销而耗尽内存,这时系统也还是很脆弱的。这些应该是由系统进程来完成的,这个系统进程应该也需要兼职负责Intent的分发。当有Intent发送到某个应用程序,而这个应用程序并没有被运行起来时,这时,这个系统进程应该发一个请求到Zygote创建虚拟机进程,然后再通过系统进程来驱动应用程序具体做怎么样的操作,这时,我们的Android的系统构架就基本上就绪了。在Android环境里,系统进程就是我们的System Server,它是我们系统里,通过init脚本创建的第一个Dalvik进程,也就是说Android系统,本就是构建在Dalvik虚拟机之上的。

    在SystemServer里,会实现ActivityManager,来实现对Activity、Service等应用程序执行实体的管理,分发Intent,并维护这些实体生命周期(比如Activity的栈式管理)。最终,在Android系统里,最终会有3个进程,一个只负责进程创建以提供页面共享,一个用户应用程序进程,和我们实现一些系统级权限才能完成的特殊功能的SystemServer进程。在这3种进程的交互之下,我们的系统会坚固,我们不会盲目地创建进程,因为应用程序完全不知道有进程这回事,它只会像调用函数那样,调用一个个实现具体功能的Activity,我们在完成内存页共享难题的同时,也完成Android系统设计的整体思路。
    这时对于应用程序处理上,还剩下最后一个问题,如果加快应用程序的加载。
    应用程序进程“永不退出”
    虽然我们拥有了内存页的预加载实现,但这还是无法保证Android应用程序执行上的高效性的。根据到现在为此我们分析到的Android应用程序支持,我们在这方面必将面临挑战。像Activity之间进行跳转,我们如果处理跳转出的Activity所依附的那个进程呢?直接杀死掉,这时,当我们从被调用Activity返回时怎么办?
    这也会是个比较复杂的问题。一是前一个进程的状态如何处理,二是我们又如何对待上一个已经暂时退出执行的进程。
    我们老式的应用程序是不存在这样的问题的,因为它不具备跨进程交互的能力,唯一的有可能进行跨进程交互的方式是在应用程序之间进行复制/粘贴操作。而对于进程内部的界面之间的切换,实际上只会发生在同一个While循环里面,一旦退出某一个界面,则相应的代码都不会被执行到,直到处理完成再返回原始界面:

    而这种界面模型,在Android世界里,只是一个UI线程所需要完成的工作,跟界面交互倒并不相关。我们的Android 在界面上进行交互,实际上是在Activity之间进行切换,而每个进程内部再维护一套上述的UI循环体:

    在这样的运行模式下,如果我们退出了某一个界面的执行,则没有必要再维持其运行,我们可以通过特殊的设计使其退出执行。但这种调用是无论处理完,还是中途取消,我们还是会回到上一个界面,如果要达到一体化看上去像同一个应用程序的效果,这里我们需要恢复上一个界面的状态。比如我们例子里,我们打了联系列表选择了某个联系人,然后通过Gallery设置大头贴,再返回到联系人列表时,一定要回到我们正在编译联系人的界面里。如果这时承载联系人列表的进程已经退出了话,我们将要使整个操作重做一次,很低效。
    所以综合考虑,最好的方式居然会是偷懒,对上个进程完全不处理,而需要提供一种暂停机制,可以让不处理活跃交互状态的进程进入暂停。当我们返回时则直接可以到上次调用前的那个界面,这时对用户来说很友好,在多个进程间协作在用户看来会是在同一个应用程序进行,这才是Android设计的初衷。
    因为针对需要暂停的处理,所以我们的应用程序各个实体便有了生命周期,这种生命周期会随着Android系统变得复杂而加入更多的生命周期的回调点。但对于偷懒处理,则会有后遗症,如果应用程序一直不退出,则对系统会是一个灾难。系统会因为应用程序不断增加而耗尽资源,最后会崩溃掉。
    不光Android会有这样的问题的,Linux也会有。我们一直都说Linux内核强劲安全,但这也是相对的,如果我们系统里有了一些流氓程序,也有可能通过耗尽资源的方式影响系统运行。大家可以写一些简单的例子做到这点,比如:

    这时会发现系统还是会受到影响,但Linux的健壮性表现在,虽然系统会暂时因为资源不足而变得响应迟缓,但还是可以保证系统不会崩溃。为了进程数过多而影响系统运行,Linux内核里有一种OOM Killer(Out Of Memory Killer)机制,系统里通过一种叫notifier的机制(顾名思义,跟我们的Listener设计模式类似的实现)监听目前系统里内存使用率,当内存使用达到比率时,就开始杀掉一些进程,回收内存,这里系统就可以回到正常执行。当然,在真正发生Out Of Memory错误也会提前触发这种杀死进程的操作。
    一旦发生OOM事件,这时系统会通过一定规则杀死掉某种类型的进程来回收内存,所谓枪打出头鸟,被杀的进程应该是能够提供更多内存回收机会的,比如进程空间很大、内存共享性很小的。这种机制并不完全满足Android需要,如果刚好这个“出头鸟”就是产生调用的进程,或是系统进程,这时反而会影响到Android系统的正常运行。

    Memory事件的不再是Linux内核里的Notifier,而由Android系统进程来驱动。像我们前面说明的,在Android里负责管理进程生成与Activity调用栈的会是这个系统进程,这样在遇到系统内存不够(可以直接通过查询空闲内存来得到)时,就触发Low Memory Killer驱动来杀死进程来释放内存。

    这种设计,从我们感性认识里也可以看到,用adb shell free登录到设备上查看空闲内存,这时都会发现的内存的剩余量很低。因为在Android设备里,系统里空闲内存数量不低到一定的程度,是不会去回收内存的,Android在内存使用上,是“月光族”。Android通过这种方式,让尽可能多的应用程序驻留在内存里,从而达到一个加速执行的目的。在这种模型时,内存相当于一个我们TCP协议栈里的一个窗口,尽可能多地进行缓冲,而落到窗口之外的则会被舍弃。

    理论上来说,这是一种物尽其用,勤俭执家的做法,这样使Android系统保持运行流畅,而且从侧面也刺激了Android设备使用更大内存,因为内存越多则内存池越大,可同时运行的任务越多,越流畅。唯一不足之处,一些试图缩减Android内存的厂商就显得很无辜,精减内存则有可能影响Android的使用体验。

    我们经常会见到系统间的对比,说Android是真实的多任务操作系统,而其他手机操作平台只是伪多任务的。这是实话,但这不是被Android作为优点来设计的,而只是整个系统设计迫使Android系统不得不使用这种设计,来维持系统的流畅度。至于多任务,这也是无心插柳柳成荫的运气吧。

    在Android系统里,无论我们今天可以得到的硬件平台是多么强大,我们还是有降低系统里的运算量的需求。作为一个开源的手机解决方案,我们不能假设系统具备多么强劲的运算能力,出于成本的考虑,也会有产商生产一些更廉价的低端设备。而即便是在一些高端硬件平台之上,我们也不能浪费手机上的运算能力,因为我们受限于有限的电池供电能力。就算是将来这些限制都不存在,我们最好也还是减少不必要的损耗,将计算能力花到最需要使用它们的地方。于是,我们在前面谈到的各种设计技巧之外,又增加了降低运算量的需求。

    这些技巧,貌似更高深,但实际上在Android之前的嵌入式Linux开发过程里,大家也被迫干过很多次了。主要的思路时,所有跟运行环境无关运算操作,我们都在编译时解决掉,与运行环境相关的部分,则尽可能使用固化设计,在安装时或是系统启动时做一次。

    与运算环境无关的操作,在我们以前嵌入式开发里,Codec会用到,比如一些码表,实际上每次算出来都是同样或是类似的结构,于是我们可以直接在编译时就把这张表算出来,在运行时则直接使用。在Android里,因为大量使用了XML文件,而XML在运行时解析很消耗内存,也会占用大量内存空间,于是就把它在编译时解析出来,在应用程序可能使用的内存段位里找一个空闲位置放进去,然后再将这个内存偏移地址写到R.java文件里。在执行时,就是直接将二进制的解析好的xml树形结构映射到内存R.java所指向的位置,这时应用程序的代码在执行时就可以直接使用了。

    在Android系统里使用的另一项编译态运算是prelink。我们Linux内核之睥系统环境,一般都会使用Gnu编译器的动态链接功能,从而可以让大量代码通过动态链接库的方式进行共享。在动态链接处理里,一般会先把代码编译成位置无关代码(Position Independent Code,PIC),然后在链接阶段将共用代码编译成.so动态链接库,而将可执行代码链接到这样的.so文件。而在动态链接处理里,无论是.so库文件还是可执行文件,在.text段位里会有PLT(Procedure Linkage Table),在.data段位里会有GOT(Global Offset Table)。这样,在代码执行时,这两个文件都会被映射到同一进程空间,可执行程序执行到动态链接库里的代码,会通过PLT,找到GOT里定位到的动态链接库里代码具体实现的位置,然后实现跳转。

    通过这样的方式,我们就可以实现代码的共享,如上图中,我们的可执行文件a.out,是可以与其他可执行程序共享libxxx.so里实现的func_from_dso()的。在动态链接的设计里,PLT与GOT分开是因为.text段位一般只会被映射到只读字段,避免代码被非法偷换,而.data段位映射后是可以被修改的,所以一般PLT表保持不动,而GOT会根据.so文件被映射到进程空间的偏移位置再进行转换,这样就实现了灵活的目的。同时,.so文件内部也是这样的设计,也就是动态链接库本身可以再次使用这样的代码共享技术链接到其他的动态链接库,在运行时这些库都必须被映射到同一进程空间里。所以,实际上,我们的进程空间可能使用到大量的动态链接库。

    动态链接在运行时还进行一些运行态处理,像GOT表是需要根据进程上下文换算成正确的虚拟地址上的依稀,另外,还需要验证这些动态链接代码的合法性,并且可能需要处理链接时的一些符号冲突问题。出于加快动态连接库的调用过程,PLT本身也会通过Hash表来进行索引以加快执行效率。但是动态链接库文件有可能很大,里面实现的函数很多很复杂,还有可能可执行程序使用了大量的动态链接库,所有这些情况会导致使用了动态链接的应用程序,在启动时都会很慢。在一些大型应用程序里,这样的开销有可能需要花好几秒才能完全。于是有了prelink的需求。Prelink就是用一个交叉编译的完整环境,模拟一次完整地运行过程,把参与运行的可执行程序与动态链接所需要使用的地址空间都算出来一个合理的位置,然后再就这个值写入到ELF文件里的特殊段位里。在执行时,就可以不再需要(即便需要,也只是小范围的改正)进行动态链接处理,可以更快完成加载。这样的技术一直是Linux环境里一个热门研究方向,像firefox这样的大型应用程序经过prelink之后,可以减少几乎一半的启动时间,这样的加速对于嵌入式环境来说,也就更加重要了。

    但这种技术有种致命缺陷,需要一台Linux机器,运行交叉编译环境,才能使用prelink。而Android源代码本就设计成至少在MacOS与Linux环境里执行的,它使用的交叉编译工具使用到Gnu编译的部分只完成编译,链接还是通过它自己实现的工具来完成的。有了需求,但受限于Linux环境,于是Android开发者又继续创新。在Android世界里使用的prelink,是固定段位的,在链接时会根据固定配置好地址信息来处理动态链接,比如libc.so,对于所有进程,libc.so都是固定的位置。在Android一直到2.3版本时,都会使用build/core/prelink-linux-arm.map这个文件来进行prelink操作,而这个文件也可以看到prelink处理是何其简单:

    在Android发展的初期,这种简单的prelink机制,一直是有效的,但这不是一种很合理的解决方案。首先,这种方式不通用,也不够节省资源,我们很难想像要在系统层加入firefox、openoffice这样大型软件(几十、上百个.so文件),同时虽然绝大部分的.so是应用程序不会用到的,但都被一股脑地塞了进来。最好,这些链接方式也不安全,我们虽然可以通过“沙盒”模式来打造应用程序执行环境的安全性,但应用程序完全知道一些系统进程使用的.so文件的内容,则破解起来相对比较容易,进程空间分布很固定,则还可以人为地制造一些栈溢出方式来进行攻击。

    虽然作了这方面的努力,但当Android到4.0版时,为了加强系统的安全性,开始使用新的动态链接技术,地址空间分布随机化(Address Space Layout Randomization,ASLR),将地址空间上的固定分配变成伪随机分布,这时就也取消了prelink。

    Android系统设计上,对于性能,在各方面都进行了相当成功的尝试,最后得到的效果也非常不错。大家经常批评Android整个生态环境很恶劣,高中低档的设备充斥市场,五花八门的分辨率,但抛开商业因素不谈,Android作为一套操作系统环境,可以兼容到这么多种应用情境,本就是一种设计上很成功的表现。如果说这种实现很复杂,倒还显得不那么神奇,问题是Android在解决一些很难的工程问题的时候,用的技巧还是很简单的,这就非常不容易了。我们写过代码的人都会知道,把代码写得极度让人看不懂,逻辑复杂,其实并不需要太高智商,反而是编程能力不行所致。逻辑清晰,简单明了,又能解决问题,才真正是大神级的代码,业界成功的项目,linux、git、apache,都是这方面的典范。

    Android所有这些提升性能的设计,都会导致另一个间接收益,就是所需使用的电量也相应大大降低。同样的运算,如果节省了运算上的时间,变相地也减少了电量上的损失。但这不够,我们的手机使用的电池非常有限,如果不使用一些特殊的省电技术,也是不行的。于是,我们可以再来透过应用程序,看看Android的功耗管理。

    Android应用程序开发以及背后的设计思想深度剖析(5)

    在嵌入式领域,功耗与运算量几乎成正比。操作系统里所需要的功能越来越复杂、安全性需求越来越高,则会需要更强大的处理能力支持。像在老式的实时操作系统里,没有进程概念,不需要虚拟内存支持,这时即便是写一些简单应用,所需要的运算量、内存都非常小,而一旦换用支持虚拟内存的系统,则所需要的硬件处理能力、电量都会成倍上涨,像一些功能性手机平台,可以成为一台不错的手机,但运行起一个Linux操作系统都很困难。而随着操作系统能力增强,则所能支持的硬件又得以提升,可以使用更大的屏幕、使用更大量内存、支持更多的无线芯片,这些功能增强的同时,也进一步加剧了电量的消耗。虽然现在芯片技术不断提高生产工艺降低制程(就是芯片内部烧写逻辑时的门电路尺寸),几乎都已经接近了物理上的极限(40纳米、28纳米、22纳米),但是出于设计更复杂芯片为目的的,随着双核、四核、以及越来越高的工作频率,事实上,功耗问题不但没有降低,反而进一步被加剧了。

    面对这样越来越大的功耗上的挑战,Android在设计上,必须在考虑其他设计因素之前,更关注功耗控制问题。Android在设计上的一些特点,使系统所需要的功耗要高于传统设计:Android是使用Java语言执行环境的,所有在虚拟机之上运行的代码都需要更大的运算量,使用机器代码中需要一条指令的地方,在虚拟机环境下执行则可能需要十几条指令;与其他伪多任务不同,Android是真实多任务的,多任务则意味着在同一时刻会有更多任务在运行;Android是构建上Linux内核之上的系统,Linux内核在性能上表现奇佳,在功耗处理上则是短板,就拿PC环境来说,Linux的桌面环境在功耗控制上从来不如其他操作系统,MacOS或是Windows。

    当然,有时没有历史包袱,也未必就是坏事,比如Linux内核在功耗管理上做得还不够好,于是就不会在Linux内核环境里死磕,Android可以通过新的设计来进行功耗控制上的提升。出于跟前面我们所说过的可减小对Linux内核依赖性、加强系统可移植性的设计需求,于是不可避免的,功耗控制将会尽可能多地被推到系统的上层。在我们前面对于安全性的分层中可以看到,Android相当于把整个操作系统都在用户态重新设计了一次,SystemServer这个系统级进程相当于用户态的一个Linux内核,于是将功耗控制更多地抽到用户态来执行,也没有什么不合理的。

    在Android的整体系统设计里,功耗控制会先从应用程序着手,通过多任务并行时减小不必要的开销开始;在整个系统构架里,唯一知道当前系统对功耗需求的是SystemServer,于是可以通过相应的安全接口,将功耗的控制提取出来,可由SystemServer来进行后续的处理。Android系统所面临的运行环境需求里,电源是极度有限的资源,于是功耗控制应该是暴力型的,尽可能有能力关闭不需要使用的电源输出。当然暴力关电,则可能引起某些外设芯片不正常工作,于是在芯片驱动里需要做小范围修改。与其他功能部分的设计不同,既然我们功耗控制是通过与驱动打交道来实现,可能无法避免地需要驱动,但要让修改尽可能小,以提供可移植性。

    在这种修改方案里,最需要解决的当然首先是多任务处理。我们可以得到的就是我们的生命周期。所谓的生命周期,是不是仅仅只是提供更多一些编程上的回调接口而已呢?不仅如此,我们的所谓生命周期是一种休眠状态点,更多地起到休眠操作时我们有机会插入代码的作用。如果仅是提供编程功能,我们可以参考JAVA ME里对于应用程序实现:

    JAVA ME框架里对待应用程序只有三个状态点,运行、暂停、关闭,对应提供三种回调接口就可以驱动起这种编程模型。但我们的Android不是这样处理的,Android在编程模型上,把带显示与不带显示的代码逻辑分别抽象成Activity与Service,每种不同逻辑实现都有其独特的生命周期,以更好地融入到系统的电源管理框架里。

    像我们的与显示相关的处理,Activity,它拥有6种不同状态:

    它的不同生命周期阶段,取决于这一Activity是否处于交互状态,是否处理可见状态。如果加入这两个限制条件,于是Activity的生命周期则是为这两种状态而设计的。onResume()与onResume()分别是进入交互与退出交互时的状态点,在onResume()执行完之后,这时系统进入了交互状态,也就是Activity的Running状态,而此时如果由于Activity发生调用或是另一个Activity主动执行,弹出一个小对话框,使原来处于Running状态的Activity被挡住,这时Activity就被视为不需要交互了,这时Activity进入不可见互状态,触发onPause()回调。onStart()与onStop()则是对应于是否可见,在onStart()回调之后,应用程序这里就可以被显示出来,但不会真正进入交互期,当Activity变得完全不可见之后,则会触发onStop()。而Android的多任务实现,还会造成进程会被杀死掉,于是也提供两个onCreate()与onDestroy()两种回调方法来提供进程被创建之后与进程被杀死之前的两种不同操作。

    这种设计的技巧在于,当Activity处于可交互状况时,这是系统里的全马力执行的周期。而再向外走一个状态期,只是处于可见但不可交互状态时,我们就可以开始通过技巧降功耗了,比如此时界面不再刷新、可以关闭一些所有与用户交互相关的硬件。当Activity再进一步退出可见状态时,可以进一步退出所有硬件设备的使用,这时就可以全关电了。编写应用程序时,当我们希望它有不一样的表现时,我们可以去通过IoC去灵活地覆盖并改进这些回调接口,而假如这种标准的模型满足我们的需求,我们就什么都不需要用,自动地被这种框架所管理起来。

    当然,这种模型也不符合所有的需求,比如对于很多应用程序来说,在后台不可见状态下,仍然需要做一些特定的操作。于是Android的应用程序模型里,又增加了一个Service。对于一些暴力派的开发者,比较喜欢使用后台线程来实现这种需求,但这种实现在Android并不科学,因为只通过Activity承载的后台线程,有可能会被杀死掉,在有状态更新需求时,后台线程需要通过Activity重绘界面,实际上这样也会破坏Android在功耗控制上的这种合理性设计。比较合适的做法,所有不带界面、需要在后台持续进行某些操作的实现,都需要使用Service来实现,而状态显示的改变应该是在onStart()里完成的,状态上的交互则需要放到onResume()方法里,这样的实现可以有效绕开进程被杀死的问题。并且在我们后面介绍AIDL的部分,还可以看到,这样实现还可以加强后台任务的可交互性,当我们进一步将Service通过AIDL转换成Remote Service之后,则我们的实现会具备强大的可复用性,多个进程都可以访问到。

    Service也会有其生存周期,但Service的生存周期相对而言要简单得多,因为它的生存周期只存在“是否正在被使用”的区别。当然,同样出于Android的多任务设计,“使用中”这个状态之外,也会有进程是否存在的状态。

    于是,我们的Service也可被纳入到这种代码活跃状态的受控环境,当是不需要与后台的Service发生交互,这时,我们可能只是通过一个startService()发出Intent,这时Service在执行完相应的处理请求则直接退出。而如果是一个AIDL方式抛出的Remote

    当我们的应用程序的各种不同执行逻辑,都是处于一个可控状态下时,这时,我们的功耗控制就可以被集中到一个系统进程的SystemServer来完成。这时,我们面临一种设计上的选择,是默认提供一种松散的电源控制,让应用程序尽可能多自由地控制电源使用,还是提供一种严格逻辑,默认情况下实施严格的电源输出管理,只允许应用程序出于特殊的需求来调高它的需求?当然,前一种方式灵活,但出于电源的有限性,这时Android系统里使用了第二次逻辑,尽可能多地严格电源输出控制。

    在默认情况下,Android会尝试让系统尽可能多地进入到休眠状态之中。在从用户开始进行了最后一次交互之后,系统则会触发一个计时器,计时器会在一定的时间间隔后超时,但每次用户的交互操作都会重置这一计时器。如果用户一直没有进行第二次交互,计时器超时则触发一些功耗控制的操作。比如第一步,会先变暗直至关闭系统的屏幕,如果在后续的一定时间内用户继续没有任何操作,这时系统则会进一步尝试将整个系统变成休眠状态。

    休眠部分的操作,基本上是Linux内核的功耗控制逻辑了。休眠操作的最后,会将内存控制器设成自刷新模式,关掉CPU。到这种低功耗运行模式之下,这时系统功耗会降到最低,如果是不带3G模组的芯片,待机电流应该处于1mA以下。但我们的系统是手机,一般2G、3G、或是4G是必须存在的,而且待机状态时关掉这种不同网络制式下的Modem,也失去了手机存在的意义,于是,一般功耗上会加上一个移动Modem,专业术语是基带(Baseband)的功耗,这时一般要控制在10 – 30mA的待机电流,100mW左右的待机功耗。如果这时,用户按些某些用于唤醒的按键、或是基带芯片上过来了一些短信或是电话之类的信息,则系统会通过唤醒操作,回到休眠之前的状态。

    但是Linux内核的Suspend与Resume方案,是针对ACPI里通用计算环境(我们的PC、笔记本、服务器)的功耗控制方案,并不完全与手机的使用需求相符合。而Linux内核所缺失的,主要是UI控制上功耗管理,手机平台上耗电最大的元器件,是屏幕与背光,是无法通过Linux内核的suspend/resume两级模型来实现高效的电源管理。于是,Android系统,在原始的suspend与resume接口之外,再增加了两级early_suspend与late_resume,用于UI交互时的提前休眠。

    我们的Android系统,在出现用户操作超时的情况下,会先进入early_suspend状态点,关闭一些UI交互相关的硬件设备,比如屏幕、背光、触摸屏、Sensor、摄像头等。然后,在进一步没有相应唤醒操作时,会进入suspend关闭系统里的其他类的硬件。最后系统进入到内存自刷新、CPU关电的状态。如果在系统完全休眠的情况下,发生了某种唤醒事件,比如电话打进来、短信、或是用户按了电源键,这时就会先进resume,将与UI交互不相关的硬件唤醒,再进入late_resume唤醒与UI交互相关的硬件。但如果设备在进入early_suspend状态但还没有开始suspend操作之前发生了唤醒事件,这时就直接会走到late_resume,唤醒UI交互的硬件驱动,从而用户又可以看到屏幕上的显示,并且可以进行交互操作。

    经过了这样的修改,在没有用户操作的情况下,系统会不断进入休眠模式省电,而用户并不会感受到这种变化,在频繁操作时,实际上休眠与唤醒只是快进快出的UI相关硬件的休眠与唤醒。但完全暴力型的休眠也会存在问题,比如我们有些应用程序,QQ需要保持登录,下载需要一直在后台下载,这些都不符合Android的需求的,于是,我们还需要一种机制,让某些特殊的应用程序,在万不得已的情况下,我们还是可以得这些应用程序足够的供电运行得下去。

    于是Android在设计上,又提出了一套创新框架,wake_lock,在多加了early_suspend与late_resume之外,再加上可以提供功耗上的特殊控制。Wake_lock这套机制,跟我们C++里使用的智能指针(Smart pointer),借用智能指针的思想来设计电源的使用和分配。我们也知道Smart Pointer都是引用,则它的引用计数会自动加1,取消引用则引用计数减1,使用了智能指针的对象,当它的引用计数为0时,则该对象会被回收掉。同样,我们的wake_lock也保持使用计数,只不过这种“智能指针”的所使用的资源不再是内存,而是电量。应用程序会通过特定的WakeLock去访问硬件,然后硬件会根据引用计数是否为0来决定是不是需要关闭这一硬件的供电。

    Suspend与wake_lock这两种新加入的机制,最后也是需要加放SystemServer这个进程里,因为这是属于系统级的服务,需要特权才能保证“沙盒”机制。于是,我们得到了Android里的电源管理框架:

    当然,这里唯一不太好的地方,就是Android系统设计必须对Linux内核原有的电源管理机制进行改动,需要加入wake_lock机制的处理,也需要在原始的内核驱动之上加入新的early_suspend与late_resume两个新的电源管理级别与wake_lock相配套。这部分的代码,则会造成Android系统所需要的驱动,与标准Linux内核的驱动并不完全匹配,同时这种简单粗暴的方式,也会破坏掉内核原有的清晰简要的风格。这方面也造成了Linux社区与Android社区之间曾一度吵得很凶,Linux内核拒绝Android提交的修改,而Android源代码则不再使用标准的Linux内核源代码,使用自己特殊的分支进行开发。

    我们再来看Android系统对于功能接口的设计。

    我们实现一个系统,必须尽可能多地提供给应用程序尽可能多的开发接口,作为一个开源系统更应该如此。虽然我们前面提到了,我们需要有权限控制机制来限制应用程序可访问系统功能与硬件功能,但是这是权限控制的角度,如果应用程序得到了授权,应该有理由来使用这一功能,一个能够获得所有权限的应用程序,则理所当然应该享受系统里所提供的一切功能。

    对于一个标准的Java系统,无论是桌面环境里使用的Java SE还是嵌入式环境里使用的Java ME,都不存在任何问题,因为这时Java本就只是系统的一层“皮”,每个Java写成的应用程序,只是一层底层系统上的二次封装,实际上都是借用底层操作系统来完成访问请求的。对于传统的应用程序,一个main()进入死循环处理UI,也不存在这个问题,通过链接到系统里的动态链接库或是直接访问设备文件,也可以实现。但这样的方式,到了Android系统里,就会面临一个功能接口的插分问题。因为我们的Android,不再是一层操作系统之上的Java虚拟机封装,而是抽象出来的在用户态运转的操作系统,同时还会有“沙盒”模式,应用程序并不见得拥有所有权限来访问系统资源,则又不能影响它的正常运行。

    于是,对于Android在功能接口设计上,会被划分成两个层次的,一种是以“受托管”环境下通过一个系统进程SystemServer来执行,另一种是被映射到应用程序的进程空间内来完成。而我们前面分析的使用Java编程语言,而Framework层功能只以API方式向上提供访问接口,就变得非常有远见。使用了Java语言,则我们更容易实现代码结构上的重构,如果我们的功耗接口有变动,则可以通过访问接口的重构来隐藏掉这样的差异性;只以Framework的API版本为标准来支持应用程序,则进一步提供封装,在绝大部分情况下,虽然我们底层结构已经发生了巨大变动,应用程序却完全不受影响,也不会知道有这样的变化。

    从这种设计思路,我们再去看Android的进程模型,我们就可以看到,通常意义上的Framework,实际上被拆分成两部分:一部分被应用程序用Java实现的classes.dex所引用,这部分用来提供应用程序运行所必须的功能;另一部分,则是由我们的SystemServer进程来提供。

    在应用程序只需要完成基本的功能,比如只是使用Activity来处理图形交互时,通过Activity来构建方便用户使用的一些功能时,这时会通过自己进程空间内映射的功能来完成。而如果要使用一些特殊功能,像打电话、发短信,则需要通过一种跨进程通讯,将请求提交到SystemServer来完成。

    这种由于特殊设计而得到的运行模型很重要,也是Android系统有别于其他系统很重要的一个区别。这样的框架设计,使Android与传统Linux上所面临的易用性问题在设计角度就更容易解决。

    比如显示处理。我们传统的嵌入式环境里,要不就是简单的Framebuffer直接运行,要么会针对通用性使用一个DirectFB的显示处理方案,但这种方案通用性很低,安全性极差。为了达到安全性,同时又能尽可能兼容传统桌面环境下的应用程序,大都会传承桌面环境里的一个Xorg的显示系统,比如Meego,以及Meego的前身Maemo,都是使用了Xorg用来处理图形。但Xorg有个很严重的性能问题:

    使用Xorg处理显示的,所有的应用程序实际上只是一个客户端,通过Unix Socket,使用一种与传统兼容的X11的网络协议。用户交互,应用程序会在自己的交互循环里,通过X11发起创建窗口的请求,之后的交互,则会通过输入设备读取输入事件,再通过Xorg服务器,转回客户端,而应用程序界面上的重绘操作,则还是会通过X11协议,走回到Xorg Server之后,再进行最后的绘制与输出。虽然现在我们使用的经过模块化重新设计的XorgR7.7,已经尽可能通过硬件加速来完成这种操作,Xorg服务器还是有可能会成为整个图形交互的瓶颈,更重要的是复杂度太高,在这种构架里修改一个bug都有点困难,更不要说改进。在嵌入式平台上更是如此,性能本就不够的系统环境,Xorg的缺陷暴露无移,比如使用Xorg的Meego更新过程远比Android要困难,用户交互体验也比较差。

    在Android里,处理模型则跟传统的Xorg构架很不一样。从设计角度来讲,绘制图形界面与获取输入设备过来的输入事件,本来不需要像Xorg那样的中控服务器,尤其像Android运行环境这样,并不存在多窗口问题(多窗口的系统需要有个服务器决定哪个窗口处于前台,哪个窗口处于交互状态中)。而从实现的角度,如果能够提供一种设计,将图形处理与最终输出分开,则更容易实现优化处理。基于图形界面的交互,实际上将由三个不同的功能实体来完成:应用程序、负责将图层进行叠加渲染的SurfaceFlinger、以及负责输入事件管理和选择合适的地址进行发送的SystemServer。当然,我们的上层的应用程序不会看到内部的复杂逻辑,它只知道通过android.view这个包来访问所有的图形交互功能。

    于是得到Android系统的图形处理框架:

    Service的实现,所以有原理上来说,只要有一个承载它的执行体(进程、线程皆可),就可以在系统里执行。在实现过程里,SurfaceFlinger作为一个线程在SystemServer这个进程空间里完成也是可以的,只是出于稳定性的考虑,一般将它独立成一个单独的SurfaceFlinger的独立进程。

    这种设计,可以达到一个低耦合设计的优势,这套图形处理框架将变得更简单,同时也不会将Xorg那样需要大量的特殊内核接口与其适配,如果在别的操作系统内核之上进行移植,也不会有太大的依赖性。但这时会带来严重的性能问题,因为图层的处理和输出是需要大量内存的(如果是24位真彩色输出,即使是800x480的分辩率,每秒60桢的输出频率,也需要3*800*480*60 = ,69M Byte/s),这种开销对于嵌入式方案而言,是难以承受的。在进程间传递数据时,会先需要在一个进程执行上下文环境里通过copy_from_user()把数据从用户态拷贝到内核态,然后在另一个进程执行的上下文环境里通过copy_to_user()把数据拷贝从内核态拷贝到另一个用户态环境,这样才能保证互不干扰。

    而回过头来看Linux内核,搞过Linux内核态开发的都知道,在Linux系统的进程之间减小内存拷贝的开销,最直接的手段就是通过mmap()来完成内存映射,让保存数据的内存页只会在内核态里循环,这时就没有内存拷拷贝的开销了。使用了mmap()之后,内存页是直接在内核态分配的内存,两个进程都通过mmap()把这段区域映射到自己的用户空间,然后可以一个进程直接操作内存,另一个进程就可以直接访问到。在图层处理上,最好这些在内核态申请的内存是连续内存,这时就可以直接通过LCD控制器的DMA直接输出,Android于是提供了一种新的特殊驱动pmem,用来处理连续物理内存的分配与管理。同时,这种方式很裸,最好还在上层提供一次抽象,编程时则灵活度会更高,针对这种需求,就有了我们的Gralloc的HAL接口。加入了这两种接口之后,Android在图像处理上便自成体系,不再受限于传统实现了。

    我们的图层,是由应用程序在创建是通过Gralloc来申请图层存储空间,然后被包装成上层的Surface类,在Activity实现里Surface则是按需要进行重绘(调用view的draw()方法),并在绘制完成后通过post()将绘制完成的消息发送给SurfaceComposer远程对象。而在SurfaceFlinger这段,则是将已经绘制完成的Surface通过其对应的模式,进行图层的合成并输出到屏幕。对于上层实现,貌似是一种很松散的交互,而对于底层实现,实际则是一种很高效的流水线操作。

    这里,值得一提的是Surface本身也包含了图层处理加速的另一种技巧,就是double buffer技术。一个Surface会有两个图层buffer,一桢在后台被绘制,另一桢在前台进行输出。当后台绘制完成后,会通过一次Page Flipping操作,原来的后台桢被换到前台进行输出,而绘制操作则继续在后台完成。这样用户总会看到绘制完整的图像,因为图层总是绘制完成后才能输出。而有了double buffer,使我们图形输出的性能也得到提升,我们输出绘制与输出使用独立的循环,通过流水线加快了图层处理,尤其在Android里,可能有多个绘制的逻辑部分,性能得以进一步加速。在Android 4.1里面,这种图形处理得以进一步优化,使用了triple buffer(三重缓冲),加深了图层处理的流水线操作能力。

    这种显示处理上的灵活性,在Android系统里也具备非常重要的意义,可以让整个系统在功能设计上可以变得更加灵活。我们提供了一种“零拷贝”图层处理技术之后,最终上层都可以通过一个特殊的可以跨进程的Surface对象来进行异步的绘制处理(如果我们不是直接操作控件,而是通过“打洞”方式来操作图形界面上的某个区域,则属于SurfaceView提供的,当然,这时也只是操作Surface的某一部分)。我们的Surface的绘制与post()异步进行的,于是多个执行体可以并行处理图层,而用户只会看到通过post()发送的图层绘制完成的同步事件之后的完整图层,图层质量与流畅性反而可以更佳。比如,我们的VOIP应用程序,可以会涉及多个功能实体的交互,Camera、多媒体编解码、应用程序、SurfaceFlinger。

    应用程序、多媒体编解码与Camera都只会通过一个Surface对象来在后台桢上进行交互界面的绘制,像前摄像头出来的回显,从网络解码出来的远端的视频,然后应用程序的操作控件,都将重绘后台图层。而如果这一应用程序处于Activity的可交互状态(见前面的生命周期的部分),就会通过找到同一Surface对象,将这一Surface对象的前台桢(也就是绘制完成但还没有输出的图层)输出。输出完则对这一Surface对象的前后两桢图层进行对调,于是这样的流水线则可以很完美的运行下去。

    Android并非是最高效的方案,而只是一种通过面向对象方式完全重新设计的嵌入式解决方案,高效是其设计的一部分要素。如果单从效率角度出发,无进程概念的实时操作系统最高效,调度开销也小,没有虚址切换时的开销。作为Android系统,通过目前我们看到的功能性接口的设计,至少得到了以良好的构架为基础同时又兼顾性能的一种设计。

    当然,我们前面所总结的,对于Android系统的种种特性,最终得到的一种印象是每种设计都是万能胶,同一种设计收获了多种的好处。那是不是这种方式最好,大家都应该遵循这种设计上的思路与技巧?That depends,要看情况。像Android这样要完整地实现一整套这种在嵌入式环境里运行的,面向对象式的,而且是基于沙盒模式的系统,要么会得到效率不高的解决方案,要么会兼顾性能而得到大量黑客式的接口。Android最终也就是这么一套黑客式的系统,这个系统一环套一环,作为系统核心部分的设计,都彼此过分依赖,拆都拆不开,对它进行拆分、精减或是定制,其实都很困难。但Android,其系统的核心就是Framework,而所谓的Framework,从软件工程学意义上来说,这样的构架却是可以接受的。所谓的Framework,对上提供统一接口,保持系统演进时的灵活性;对下则提供抽象,封装掉底层实现的细节。Android的整个系统层构架,则很好的完成了这样的抽象,出于这样的角度,我们来看看Android的可移植性设计。

    单纯从可移植性角度来说,Linux内核是目前世界上可移植性最强的操作系统内核,没有之一。目前,只要处理器芯片能够提供基本的运算能力(可以支撑多进程在调度上的开销),只要能够提供C语言的编译器(准确地说是Gnu C编译工具链),就可以运行Linux内核。Linux内核在设计上保持了传统Unix的特点,大部分使用了C语言开发,极少部分机器相关的代码使用汇编,这种结构使其可移植性很强。在Linux内核发展到2.6版本之后,这种强大的可移植性得到进一步提升,通过驱动模型与驱动框架的引入和不断加强,使Linux内核里绝大部分源代码几乎都没有硬件平台上的依赖性。于是,Linux内核几乎能够运行在所有的硬件平台之上,常见有的X86、ARM,不那么常见但可能也会在不知道不觉地使用到的有MIPS、PowerPC、Alpha,另外还有一些我们听都没有听过的,像Blackfin,Cris、SuperH、Xtensa,Linux内核都支持,平台支持可参考linux内核源代码的arch目录。甚至,出于Linux内核的可移植性,Linux一般也被作为芯片验证的工具,芯片从FPGA设计到最终出厂前,都会通过Linux内核来检测这一芯片是否可以运行,是否存在芯片设计上的错误。

    得益于Linux内核,构建于其上的操作系统,多多少少可继承这样的可移植性。但Android又完成应用程序运行环境的二次抽象,在用户态几乎又构造出一层新的操作系统,于是它的可移植性多多少少会受此影响,而且,像我们前面所分析出来的,Android的核心层构建本身,也因为性能上的考虑,耦合性也有点强,于是在可移植性也会面临挑战。“穷山恶水出刁民”,正因为挑战大,于是Android反倒通过各种技巧来加强系统本身的可移植性,反而做得远比其他系统要好得多。Android在可移植性上的特点有:

    • 按需要定制可移植性。与传统嵌入式Linux操作系统不同,Android在设计上有明确的设计思想与目标,不会为了使用更多开源软件而提供更高兼容性的编译环境,而是列出功能需求,按功能需求来定制所需要的开源软件。有些开源软件能够提供更复杂的功能,但在Android环境里,只会选择其验证过的必需功能,像蓝牙,BlueZ本身可以提供更复杂的蓝牙控制,但Android只选择了BlueZ的基本功能,更多功能是由Android自己来实现,于是减小了依赖性,也降低了移植时的风险性。
    • 尽可能跨平台。与以前的系统相比,Android在跨平台上得益于Java语言的使用,使其跨平台能力更强,在开发上几乎可以使用任何Java环境可以运行的操作系统里。而在源代码级别,它也能够在MacOSX与Linux环境里进行编译,这也是一个大的突破。
    • 硬件抽象层。Android在系统设计的最初,便规划了硬件抽象层,通过对硬件访问接口的抽象,使硬件的访问接口相对稳定,而具体的实现则可在底层换用不同硬件访问接口时灵活地加以实现,不要说应用程序,就是Framework都不会意识到这种变动。这是以前的嵌入式Linux操作系统所没有的一种优点。硬件抽象层的使用,使Android并不一定需要局限于Linux内核之上,如果将底层偷换成别的接口,也不会有太大的工作量。
    • 实现接口统一的规范化。Android在构架上,都是奉行一种统一化的思路,先定义好API,然后会有Framework层的实现,然后再到硬件抽象层上的变动。API可在同一版本上拓展,Framework也在逐步加强,而硬件抽象层本身可提供的能力也越来越强,但这一切都在有组织有纪律的环境下进行,变动在任何一次版本更新上来看,都是增量的小范围变动,而不会像普通的Linux环境那样时刻都在变,时刻都有不兼容的风险。从可移植性角度来说,这种规范化提供的好处,便是大幅降低了移植时的工作量。
    • 尽可能简单。简单明了是Android系统构成上的一大特色,这种特色在可移植性上也是如此。像编译环境,Android在交叉编译环境上,是通过固化编译选项来达到简编译过程的上的,最终,Android源代码的编译工程,会是一个个由Android.mk来构造的可编译环境,这当然会降低灵活性,但直接导致了这套框架在跨平台上表现非常出色。再比如硬件抽象层,同样的抽象在现代嵌入式操作系统上可能都有,但是大都会远比Android的HAL层要复杂,简单于是容易理解和开发,在跨平台性方面也会表现更好。

    我们传统的嵌入式Linux环境,几乎都会遵从一种约定俗成的传统,就是专注于如何将开源软件精减,然后尽可能将PC上的运行环境照搬到嵌入式。在这种思路引导下开发出来的系统,可移植性本身是没什么问题的,只是不是跟X86绑定的源代码,铁定是可以移植。但是,这样构建出来的系统,一般都在结构上过于复杂,会有过多的依赖性,应用程序接口并不统一,升级也困难。所有这样的系统,最后反倒是影响到了系统的可移植性。比如开源嵌入式Linux解决方案,maemo,就是一个很好的例子:

    对于Maemo的整体框架而言,我们似乎也可以看到类似于Android的层次化结构,但注意看这种系统组成时,我们就不难发现,这样的层次化结构是假的,在Maemo环境里,实际上就是一个小型化的Linux桌面环境,有Xorg,有gtk,有一大堆的依赖库,编程环境几乎与传统Linux没任何区别。这种所谓的软件上的构架,到了Maemo的后继者Meego,也是如此,只不过把gtk的图形界面换成了Qt的,然后再在Qt库环境里包装出所谓的UX,换汤不换药,这时,Meego还是拥有一颗PC的心。

    一般这种系统的交叉编译环境,还必须构建在一套比较复杂的编译环境之上,通过在编译环境里模拟出一个Linux运行环境,然后才能编译尽可能多的源代码。这样的交叉编译环境有Open Embedded,ScratchBox等。虽然有不同的交叉编译实现上的思路,但并没有解决可移植性问题,它们必须在Linux操作系统里运行,而且使用上的复杂程度,不是经验丰富的Linux工作者还没办法灵活使用。即便是比较易用的ScratchBox,也会有如下令人眼花缭乱的结构。

    针对这样的现状,Android在解决可移植性问题时的思路就要简单得多。既然原来的尝试不成功,PC被精减到嵌入式环境里效果并不好,这时就可以换一种思路,一种“返璞归真”的思路,直接从最底层来简化设计,简化交叉编译。这样做法的一个最重要前提条件,就是Android本身是完整重新设计与实现的,是一个自包含的系统,所有的编译环境,都可以从源代码里把系统编译出来。

    在系统结构上,Android在设计上便抛弃了传统的大肆搜刮开源代码的做法,由自己的设计来定位需要使用的开源代码,如果没有合适的开源代码,则会提供一个简单实现来实现这一部分的功能。于是,得到我们经常见到的Android的四层结构:

    从这样简化过的四层结构里,最底层的Linux内核层,这些都与其他嵌入式Linux解决方案是共通的特性,都是一样的。其他三层就与其他操作系统大相径庭了:应用程序层是一种基于“沙盒”模式的,以功能共享为最终目的的统一开发层,并非只是用于开发,同时还会通过API来规范这些应用程序的行为;Framework层,这是Android真正的核心层,而从编程环境上来说,这一层算是Java层,任何底层功能或硬件接口的访问,都会通过JNI访问到更低层次来实现;给Framework提供支撑的就是Library层,也就是使用的一个自己实现的,或是第三方的库环境,这一层以C/C++编写的可以直接在机器上执行的ELF文件为主。

    有了这种更简化的层次关系,使Android最后得到的源代码相对来说更加固定,应用程序这层我们只会编译Java,Framework层的编译要么是Java要么是JNI,而Library层则会是C/C++的编译。在比较固定的编译目标基础上,编译环境所需要解决的问题则会比较少,于是更容易通过一些简化过的编译环境来实现。Android使用了最基本的编译环境GnuMake,然后再在其上使用最基本的Gnu工具链(不带library与动态链接支持)来编译源代码,最后再通过简化过的链接器来完成动态链接。得到的结果是几乎不需要编译主机上的环境支持,从而可以在多种操作系统环境里运行,Android的编译工程文件还比较简单,更容易编写,And

    我要回帖

    更多关于 保护宝宝隐私 的文章

     

    随机推荐