本文内容,主题是透过应用程序来分析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环境不要说在嵌入式平台上,就是在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
这时会发现系统还是会受到影响,但Linux的健壮性表现在,虽然系统会暂时因为资源不足而变得响应迟缓,但还是可以保证系统不会崩溃。为了进程数过多而影响系统运行,Linux内核里有一种OOM Killer(Out Of Memory
Killer)机制,系统里通过一种叫notifier的机制(顾名思义,跟我们的Listener设计模式类似的实现)监听目前系统里内存使用率,当内存使用达到比率时,就开始杀掉一些进程,回收内存,这里系统就可以回到正常执行。当然,在真正发生Out Of Memory错误也会提前触发这种杀死进程的操作。 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环境,几乎都会遵从一种约定俗成的传统,就是专注于如何将开源软件精减,然后尽可能将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 我要回帖更多关于 保护宝宝隐私 的文章随机推荐
|