你好,我是黄俊彬。
上节课,我们了解了Android系统开发的基础框架、编译环境、开发工具等基础知识。这节课,让我们聚焦在架构设计层面,看看定制系统里最容易出现哪些架构耦合问题,这些问题又会给整机产品埋下哪些隐患。
想要分析架构层面的耦合问题,自然要先弄清楚规范的Android系统架构长什么样,就让我们从这里开始今天的内容吧!
Android系统架构
做过Android系统定制开发的同学,一定会接触到AOSP。AOSP,全称是Android Open Source Project,中文译为“Android 开放源代码项目”。厂商每年会基于Google开放的最新代码进行适配定制,开发属于自己的OS版本。
首先,我们根据Android的架构图来看看Android系统架构的设计。
对照架构图,我们从上到下来看。在应用框架层上面应该还有一层,就是诸多的应用。
这些应用可以分为2类:一类是系统应用,拥有高的系统权限,可以调用系统提供的高权限接口,例如打电话、短信、设置等应用;另外一类就是非系统应用,与第三方应用一样,例如定制一些便签、运动健康、视频播放等应用。
接下来的第一层就是应用框架层,**应用框架最常被应用开发者使用,对应用提供标准的API来调用系统的能力,从而实现相关的业务功能。**我们在代码编译时,通常会依赖Android SDK的android.jar空包,保证能通过编译。但需要注意的是android.jar具体的实现都在框架层中,实际运行时调用的都是系统中的类。
第二层是Binder IPC。有了 **Binder 进程间通信 (IPC) 机制,应用框架就能跨越进程边界并调用 Android 系统服务代码。**由于系统的很多服务都是运行在System Server进程,但是集成到应用的SDK代码是运行在应用的进程,所以需要通过Binder的方式来实现跨进程间的通信。
第三层是系统服务层。系统服务专注于特定功能的模块化组件,例如窗口管理器、搜索服务或通知管理器。例如我们熟悉的AMS、WMS、PMS等,都运行在系统服务层。
第四层是硬件抽象层 (HAL)。Google 在Android 8.0 里一个名为“Treble”的项目中设计了 HAL层,目的是让制造商能够以更低成本、更轻松快速地将设备更新到新版 Android 系统。在这种新架构中,HAL 接口定义语言指定了 HAL 和其用户之间的接口,让用户无需重新构建 HAL,就能替换 Android 框架。
最后一层是Linux内核层。Google在官网介绍的开发Android设备驱动程序与开发典型的 Linux 设备驱动程序类似。但Android 使用的 Linux 内核版本包含一些特殊的补充功能,例如低内存终止守护进程、唤醒锁定、Binder IPC 驱动程序等,对于移动嵌入式平台,这些是非常重要的功能。
对Anroid系统的“魔改”
既然Android系统已经有了规范的架构设计,为什么定制Android系统还会产生耦合的问题呢?
由于手机产品涉及软硬结合,所以一般会采用 IPD 产品开发流程,研发一款手机的时间通常需要3 - 12个月的时间,并按照每款手机项目单独立项跟踪。迫于交付压力,再加上缺少有效的架构设计及守护等问题,开发人员会对系统做各种各样“花式的魔改”,最典型的是后面这3种耦合场景。
场景1:应用之间的耦合
理论上来说,应用之间都是相对独立的。但是在定制系统中,有一些应用在运行时存在相互依赖,例如桌面与负一屏(基于桌面向右滑动后的快捷入口)。这里应用A在运行时可能需要调用应用B提供的某些方法,才能保证功能正常运行,如下图所示。
这里看起来似乎合理,编译上没有依赖,运行时也是通过标准的API调用。但关键的问题是不同项目上的功能有差异,依赖的API会有变化,并且应用之间并没有做好兼容性的处理,这样导致应用B不存在时,应用A无法正常运行。你可以结合下图来理解。
场景2:应用与框架之间的耦合
接下来,我们来看第二种典型的耦合场景,应用与框架之间的耦合。
做过应用开发的同学应该知道,我们需要依赖Android的SDK来开发。因为Google会保持SDK接口的稳定及兼容,所以基于标准SDK开发的应用,才能运行在各个大版本的Android系统中。
但是在框架里面还有一些类被标识了@hide,或者有些类属于com.android.internal中的类,这些都是标准的SDK不会提供的。
但是,厂商可以编译生成完整的android.jar包,这样应用就可以调用到这些非公开的接口,以便实现更加丰富的功能。当然还有一些应用采用另外一种方法,就是使用反射的形式。你可以结合后面的示意图来理解。
由于这部分API,Google在大版本的迭代中并不一定保证兼容,所以这也意味着一旦使用这个特殊的Jar包的应用,就与特定的大版本绑定了。应用需要针对每一个大版本都维护一个特定的APK。
我还遇到过一种更“反直觉”的操作——框架依赖应用。比如在框架层增加代码,会去调用APP中相关的资源及代码。这种反向的耦合更容易出现问题,因为一旦底层框架代码的兼容性没有处理好,就很容易导致无法开机、黑屏等严重后果。
###场景3:框架之间的耦合
第三种典型的耦合场景是框架之间的耦合,这里的框架耦合指的是厂商扩展的代码与框架之间的耦合。
为了扩展系统的功能,定制Android系统可以在框架中添加一些代码,例如可以在AMS里面的Activity生命周期回调增加一些统计代码,就能统计到应用界面的一些执行情况。这些能力是三方应用无法实现的功能,是厂商定制应用的优势。你可以参考后面的示意图来理解。
但是缺少规范化的管理及灵活的插桩设计,也会产生耦合问题。我们都知道Google每年都会更新AOSP基线代码,框架之间的耦合会导致扩展的代码与框架代码强关联。一方面这些代码只能跟随着框架代码一起维护,无法做到独立维护;另外一方面当代码有更新时,维护成本也非常高。
耦合带来的问题
除了前面提到的定制Android系统的耦合问题,耦合也会影响到团队效率以及产品质量,接下来我们就重点探讨三个常见问题。
问题1:大量重复的代码合并工作
前面提到Google每年都会升级一个Android的大版本,对于厂商来说,他们其实拿到的是第三手代码。前面还有一个上游——芯片平台。为了帮助你理解,我画了后面这张示意图。
因为是第三手代码,为了保证本地代码能及时同步上游的最新代码,厂商需要定期去同步上游的代码,大版本可能是每年一次,补丁Patch可能是2周一次。由于侵入性的修改,容易导致代码冲突的出现,特别是每次的大版本更新。
另外,由于各种耦合的问题,通常最后量产版本时需要拉去独立的MP分支。这样,并行的项目越多时,合并代码的工作量就会呈指数级爆发。
问题2:并行维护多个版本
由于应用于架构和耦合问题,会让不同项目集成难度升高。因为应用无法做到一个apk适配多个项目,这样对于应用来说往往需要同时维护3-5个版本,并且通常也是采用拉取分支的形式,一个分支出一个项目版本的APK。
同时,维护多个版本带来了大量重复性的工作。例如当修复一个Bug时,需要同步到若干个分支中,并且带给测试同学的压力也非常大。由于每个分支的代码都不是完全一致的,需要做回归测试时,工作量也会翻倍。
问题3:“未知”的产品质量
由于代码的耦合问题,非常容易导致修改代码出现连带问题。所以开发同学会选择尽可能少修改代码,更别谈去做一些中大型的代码重构。在机型数量越来越多的状态下,技术复杂度越来越高。两种压力的共同作用下,代码修改越多,代码重构就变得越来越难,代码质量完全无法把控。
另外,对于产品的质量也带来了非常大的挑战。前面提到的多项目、多版本的问题,导致在最后集成阶段需要大量的回归测试,然而在缺乏高质量的自动化测试覆盖下,仅靠人工很难进行全面的验证,这样就非常容易导致问题流到线上用户手中。
总结
今天我们一起了解了Android的系统架构,与前面讲过的应用分层架构类似,Android系统也是采用模块化的分层架构。各个分层职责清晰,从上到下依次为应用框架层、Binder IPC、系统服务层、硬件抽象层以及Linux内核层。
但是当厂商开始定制Android系统时,由于项目管理、交付压力等等情况,非常容易出现不按规矩出牌的情况,我总结了3种最常见的耦合场景,你可以通过后面的表格回顾。
由于耦合的问题,团队需要完成大量重复、机械性的代码合并工作,也需要同时维护多个并行的版本。开发同学淹没在数不尽的分支合并任务里,测试同学淹没在数不尽的黑盒测试中,团队无法把精力投入到代码优化和更多产品质量优化工作上,时间一久,就会给系统埋下诸多隐患。
下节课,我们就聊聊应对这些耦合问题的具体思路,敬请期待。
思考题
感谢你学完了今天的内容,今天的思考题是这样的:你觉得Android系统的耦合与应用的耦合有哪些相同点和差异点呢?