Android系统的耦合与解耦
从应用到系统开发,代码量从几十万行增长到几千万行,开发框架以及编译环境等与应用开发也不一样。所以如果要学习Android系统开发,我们需要先了解对应的开发框架及工具链。
1.Android系统架构
AOSP全称是Android Open Source Project,中文译为“Android 开放源代码项目”。手机厂商每年会基于Google开放的最新代码进行适配定制,开发属于自己的OS版本。
我们根据Android的架构图来看看Android系统架构的设计:
应用
对照架构图,我们从上到下来看。在应用框架层上面应该还有一层,就是诸多的应用。这些应用可以分为2类:一类是系统应用,拥有高的系统权限,可以调用系统提供的高权限接口,例如打电话、短信、设置等应用;另外一类就是非系统应用,与第三方应用一样,例如定制一些便签、运动健康、视频播放等应用。
框架层(framework)
接下来的第一层就是应用框架层, 应用框架最常被应用开发者使用,对应用提供标准的API来调用系统的能力,从而实现相关的业务功能。 我们在代码编译时,通常会依赖Android SDK的android.jar空包,保证能通过编译。但需要注意的是android.jar具体的实现都在框架层中,实际运行时调用的都是系统中的类。
Binder (IPC)
第二层是Binder IPC。有了 Binder 进程间通信 (IPC) 机制,应用框架就能跨越进程边界并调用 Android 系统服务代码。 由于系统的很多服务都是运行在System Server进程,但是集成到应用的SDK代码是运行在应用的进程,所以需要通过Binder的方式来实现跨进程间的通信。
系统服务层 (system services)
第三层是系统服务层。 系统服务专注于特定功能的模块化组件,例如窗口管理器、搜索服务或通知管理器。例如我们熟悉的AMS、WMS、PMS等,都运行在系统服务层。
硬件抽象层 (HAL)
第四层是硬件抽象层。Google 在Android 8.0 里一个名为“Treble”的项目中设计了 HAL层,目的是让制造商能够以更低成本、更轻松快速地将设备更新到新版 Android 系统。在这种新架构中, HAL 接口定义语言指定了 HAL 和其用户之间的接口,让用户无需重新构建 HAL,就能替换 Android 框架。
Linux内核层 (KERNEL)
最后一层是Linux内核层。 Google在官网介绍的开发Android设备驱动程序与开发典型的 Linux 设备驱动程序类似。但Android 使用的 Linux 内核版本包含一些特殊的补充功能,例如低内存终止守护进程、唤醒锁定、Binder IPC 驱动程序等,对于移动嵌入式平台,这些是非常重要的功能。
2.Android系统的耦合问题
既然Android系统已经有了规范的架构设计,为什么定制Android系统还会产生耦合的问题呢?
由于手机产品涉及软硬结合,所以一般会采用 IPD 产品开发流程,研发一款手机的时间通常需要3 - 12个月的时间,并按照每款手机项目单独立项跟踪。迫于交付压力,再加上缺少有效的架构设计及守护等问题,开发人员会对系统做各种各样“花式的魔改”,最典型的是后面这3种耦合场景。
场景1:应用之间的耦合
理论上来说,应用之间都是相对独立的。但是在定制系统中,有一些应用在运行时存在相互依赖,例如桌面与负一屏(基于桌面向右滑动后的快捷入口)。这里应用A在运行时可能需要调用应用B提供的某些方法,才能保证功能正常运行,如下图所示。
.PNG)
这里看起来似乎合理,编译上没有依赖,运行时也是通过标准的API调用。但关键的问题是不同项目上的功能有差异,依赖的API会有变化,并且应用之间并没有做好兼容性的处理,这样导致应用B不存在时,应用A无法正常运行。你可以结合下图来理解。
.PNG)
场景2:应用与框架之间的耦合
接下来,我们来看第二种典型的耦合场景,应用与框架之间的耦合。
我们需要依赖Android的SDK来开发。因为Google会保持SDK接口的稳定及兼容,所以基于标准SDK开发的应用,才能运行在各个大版本的Android系统中。
但是在框架里面还有一些类被标识了@hide,或者有些类属于com.android.internal中的类,这些都是标准的SDK不会提供的。
但是,厂商可以编译生成完整的android.jar包,这样应用就可以调用到这些非公开的接口,以便实现更加丰富的功能。当然还有一些应用采用另外一种方法,就是使用反射的形式。你可以结合后面的示意图来理解。
.PNG)
由于这部分API,Google在大版本的迭代中并不一定保证兼容,所以这也意味着 一旦使用这个特殊的Jar包的应用,就与特定的大版本绑定了。应用需要针对每一个大版本都维护一个特定的APK。
场景3:框架之间的耦合
第三种典型的耦合场景是框架之间的耦合,这里的框架耦合指的是厂商扩展的代码与框架之间的耦合。
为了扩展系统的功能,定制Android系统可以在框架中添加一些代码,例如可以在AMS里面的Activity生命周期回调增加一些统计代码,就能统计到应用界面的一些执行情况。这些能力是三方应用无法实现的功能,是厂商定制应用的优势。你可以参考后面的示意图来理解。
.PNG)
但是缺少规范化的管理及灵活的插桩设计,也会产生耦合问题。我们都知道Google每年都会更新AOSP基线代码,框架之间的耦合会导致扩展的代码与框架代码强关联。一方面这些代码只能跟随着框架代码一起维护,无法做到独立维护;另外一方面当代码有更新时,维护成本也非常高。
总结
.PNG)
3.耦合带来的问题
除了前面提到的定制Android系统的耦合问题,耦合也会影响到团队效率以及产品质量,接下来重点探讨三个常见问题。
问题1:大量重复的代码合并工作
前面提到Google每年都会升级一个Android的大版本,对于厂商来说,他们其实拿到的是第三手代码。前面还有一个上游——芯片平台。为了帮助你理解,我画了后面这张示意图。
.PNG)
因为是第三手代码,为了保证本地代码能及时同步上游的最新代码,厂商需要定期去同步上游的代码,大版本可能是每年一次,补丁Patch可能是2周一次。由于侵入性的修改,容易导致代码冲突的出现,特别是每次的大版本更新。
另外,由于各种耦合的问题,通常最后量产版本时需要拉去独立的MP分支。这样,并行的项目越多时,合并代码的工作量就会呈指数级爆发。
问题2:并行维护多个版本
由于应用与架构的耦合问题,会让不同项目集成难度升高。因为应用无法做到一个apk适配多个项目,这样对于应用来说往往需要同时维护3-5个版本,并且通常也是采用拉取分支的形式,一个分支出一个项目版本的APK。
.PNG)
同时,维护多个版本带来了大量重复性的工作。例如当修复一个Bug时,需要同步到若干个分支中,并且带给测试同学的压力也非常大。由于每个分支的代码都不是完全一致的,需要做回归测试时,工作量也会翻倍。
问题3:“未知”的产品质量
由于代码的耦合问题,非常容易导致修改代码出现连带问题。所以开发同学会选择尽可能少修改代码,更别谈去做一些中大型的代码重构。在机型数量越来越多的状态下,技术复杂度越来越高。两种压力的共同作用下,代码修改越多,代码重构就变得越来越难,代码质量完全无法把控。
另外,对于产品的质量也带来了非常大的挑战。前面提到的多项目、多版本的问题,导致在最后集成阶段需要大量的回归测试,然而在缺乏高质量的自动化测试覆盖下,仅靠人工很难进行全面的验证,这样就非常容易导致问题流到线上用户手中。
总结
由于耦合的问题,团队需要完成大量重复、机械性的代码合并工作,也需要同时维护多个并行的版本。开发同学淹没在数不尽的分支合并任务里,测试同学淹没在数不尽的黑盒测试中,团队无法把精力投入到代码优化和更多产品质量优化工作上,时间一久,就会给系统埋下诸多隐患。
4.Android系统解耦
下面针对这些耦合问题,探讨具体有哪些针对性的解耦策略。
整机组件化
我们知道Android的架构设计本身就是采用了分层组件化的方式,只是由于厂商在定制的过程中,没有遵循架构设计来扩展,才出现了腐化。典型的问题包括应用间的耦合、应用与框架的耦合以及框架与框架间的耦合,如下图所示。
但是定制就意味着增加代码,所以关键问题是如何管理扩展的代码,才能把对原生代码的侵入降到最低,同时又要让系统更独立地演进。
其实解决方法也很简单,就是沿用 Android系统的架构,让我们自己扩展的代码也形成组件,能够在Android原生系统之上灵活插拔,你可以对照下图来理解。
.png)
可以看出,厂商主要定制的代码分布在应用、框架与内核层。我们分别从这几层来看看组件化改造的思路。
应用层
第一个是应用层。应用层其实分2种类型,第一种是完全自己定制的应用,第二种是扩展AOSP中原有的应用(例如桌面、电话本、短信等)。
第一种类型,应用本来的构建就是独立的二进制组件(APK),但就像上节课我们提到的,这样组件无法复用,效果大打折扣。所以我们需要解耦,不让应用与特定的系统版本耦合。
特别需要注意的是第二种类型,扩展AOSP的应用。
由于Google在大版本升级的时候,也可能会同步升级这些应用的代码,所以如果想让同步的代码是最新的,就需要保证对原生代码的扩展是相对独立的,否则也会有不少同步代码的工作量。
而很多厂商的处理方法是基于某个Android版本扩展以后就独立演进,不再同步上游的代码。这种方式有得有失,好处是减少了同步的工作量(Google有些应用可能在某个大版本变化比较大,例如短信从MMS升级到Message,基本变成2个应用),坏处是不能及时同步到一些新的代码。
总的来说,应用本身天然就是以二进制组件的形式与整机集成。所以 针对应用层组件化关键的举措就是解耦,让组件能够复用。
框架与内核层
框架与内核层的修改与应用层的修改恰恰相反,大多数的扩展修改都比较零散,而且都是直接在AOSP原生的文件中直接修改的。有些比较独立的扩展,会采用新增文件或者新增方法放到AOSP文件尾部的形式,但是有些修改就不得不在原生代码方法中间增加一些代码,这也就是我们上一节课说的框架与框架之间的耦合。
那么,针对框架的修改要如何做组件化的设计呢?我推荐的方式就是 减少对原生代码的修改,将扩展的代码独立成二进制的文件(Jar、So、Apex)。
要满足这个设计,需要先满足两个前置条件。
第一个条件,能从散落在各个源码修改的文件中的扩展代码梳理出逻辑内聚的组件。举个例子,假如扩展一个应用保活的特性,需要在AOSP中的AMS、PMS等类中插入一些代码,如果我们要抽取一个应用保活的组件,就得先把这个特性涉及到修改的地方都分析出来。
第二个条件是需要设法将更多的修改独立出来,尽量减少对源码的修改,我们在后面的解耦设计中,还会详细讨论这个问题。
对于框架代码扩展来说,由于修改比较零散,而且对AOSP源文件的侵入性修改,相比应用来说,它的组件化改造挑战更大。
组件化解耦
前面我们讨论了整机组件化整体的设计思路,接下来我们再一起看看具体的解耦方法。
应用与应用解耦
首先来看应用与应用间的解耦。 应用间的解耦重点是约定好双方的交互协议以及保证API的稳定及兼容性,你可以参考后面的示意图来理解。
.png)
首先需要保证API的稳定,尽可能保证扩展但不修改,避免在多个版本出现不兼容问题。另外,当依赖的应用不存在时,调用方需要保证功能的兼容性,可以将相关的特性屏蔽。
特别需要注意的是,封装API的时候最好是 基于数据的封装,而不是基于界面的封装。例如应用B需要展示应用A的一些内容,如果应用B直接通过API获取应用A的View来展示,就可能导致当应用A与应用B之间需要统一视觉风格时,版本之间会存在强依赖。但如果只是数据的依赖,则会更加稳定,应用B的视觉风格变化不依赖应用A的修改。
应用与框架解耦
对于应用与框架的耦合,解耦重点是 封装统一的扩展SDK,避免应用调用系统的非公开接口,导致与系统大版本产生耦合。 你可以对照后面的示意图来加深理解。
.png)
对照图片可以看出,我们的策略是通过扩展的SDK封装原先对系统的非公开接口和扩展的API,并且做好大版本的兼容性处理,应用会统一使用扩展的SDK来增强系统的能力。这样对于应用来说,就不用做大量的版本判断代码,同时也能有统一的入口来管理版本的API。
需要注意的是,扩展SDK有些方法仍然没有办法做到百分之百的兼容。比方说在T版本上有A接口,但是在S版本上没有等价的实现,那么我们同样需要在应用侧进行兼容性判断处理。但是使用统一的扩展SDK还是有好处的,这么做能帮助应用简化90%以上的兼容性维护工作,并且能统一规范。
框架与框架解耦
最后我们来看框架间的耦合,这也是最重要的和我们的工作息息相关, 框架与框架的解耦重点是减少对原生代码的修改,只保留稳定的插桩接口,将具体的实现独立成单独的组件。
.png)
首先我们需要定义桩点接口,为了减少编译时的耦合,可以采用反射的机制去查找接口的实现。
我来举个例子,AOSP中有一个类为AMS,其中有一个方法为onCreate(),原先在这个方法里面插入了代码,此时我们可以定义一个IAMS的接口,抽象一个onCreate()的桩点接口,然后将实现代码移动至IAMS的实现类中。
伪代码如下:
//解耦前:
class AMS{
public void onCreate(){
//原生代码... ...
// MIUI ADD:
xxx
yyy
zzz
// END
//原生代码... ...
}
}
//解耦后:
interface IAMS{
onCreate();
}
//AOSP代码
class AMS{
public void onCreate(){
//原生代码... ...
// MIUI ADD:
IAMS.instance().onCreate();
// END
//原生代码... ...
}
}
//扩展组件
class AMSImpl{
public void onCreate(){
// MIUI ADD:
xxx
yyy
zzz
// END
}
}
我们在解耦框架的时候,同样也需要注意兼容性的问题。当桩点接口找不到具体的实现时,需要注意不能破坏原有框架的逻辑执行。另外,我们应该保证桩点接口的稳定性,并且控制其数量,避免出现大量重复以及缺少抽象设计的桩点,不然就违背了前面提到的 最少侵入修改 原则。
组件化收益
我们提到了代码耦合带来的诸多隐患,下面我们来分析一下,组件化解耦对我们解决这些耦合问题有哪些帮助。
1.减少大量代码合并工作
当大量的组件被抽离成独立的组件化后,对于AOSP源码的修改其实只保留了最小的桩点接口,这样当有代码合并时,就能有效减少合并的工作量。
另外,由于抽离出来的系统组件也能以独立的二进制交付,后续对项目相当于就是AOSP源码+桩点接口+扩展组件集成(应用组件、系统组件等)。如果我们能保证好组件的质量,就能避免各个项目都拉取独立的分支问题,减少分支的数量。
2.减少并行的版本维护工作
组件化的设计要求各个组件必须同时做好兼容性处理,当组件能够满足这个要求时,就能做到一个版本兼容多个系统版本,那么就可以减少版本的维护工作。例如,当修改一个bug时无须在多个分支上同步、当增加一些特性时也无须在多个分支上同步等,团队能够更聚焦在组件自身的功能开发和质量提升上。
3.减少质量隐患,缩短问题定位时间
组件化最明显的好处就是能够将关联的逻辑内聚到一个组件中,同时组件之间能够有更清晰的边界及职责。这样,我们修改扩展多组件的代码时,产生的影响范围通常就会控制在当前组件中,这不容易产生连带的问题。
另外,由于组件的边界更加清晰,发现问题时也更加方便我们去定位排查,有效缩短定位问题的时间。
4.提高产品的响应力
由于组件间已完成解耦,应用组件就能够独立实现升级和发布,不与系统版本耦合。出现问题的时候,能够快速发布,不必跟随整机系统一同发布。对于框架组件也一样,Google官方提供了Apex格式也支持独立的更新,这样系统内组件有Bug时,同样也能够独立地发布更新。这将大大提高各个组件的响应力,无须跟随整机一起进行OTA的升级。
总结
.png)