1. 从一部手机说起:Android 系统架构到底在解决什么问题
很多人第一次接触 Android 系统架构,是从刷机、改 Framework、或者移植系统开始的。我也不例外。当年拿着一台卡顿的旧机器,想搞明白为什么同样一颗芯片,不同厂商的系统流畅度差这么多,才一步步从 App 层往下挖,挖到 Framework,再挖到 HAL,最后挖到 Linux 内核。挖完之后最大的感受是:Android 系统架构不是一张画出来好看的框图,而是一套为了解决"硬件千差万别、应用却要统一运行"这个核心矛盾而设计的分层协作机制。
先把结论摆在前面:Android 系统架构的本质,是用一套分层 + 抽象的结构,把"硬件差异"和"应用逻辑"彻底隔离开。上层应用只认统一的 API,下层硬件各自实现自己的驱动,中间靠 HAL 和 Framework 做翻译和调度。理解了这条主线,后面所有的模块、进程、通信机制,都是围绕它展开的。
这套架构大致分成五层,从下往上依次是:
- Linux 内核层:负责进程调度、内存管理、设备驱动、网络协议栈、电源管理等最底层能力。
- HAL 层(硬件抽象层):把具体硬件的操作封装成标准接口,让上层不用关心用的是哪家芯片。
- Native 层与运行时:包括 C/C++ 库、ART 虚拟机、核心系统库。
- Framework 层(Java API 框架):开发者日常打交道的 ActivityManager、WindowManager、PackageManager 都在这一层。
- 应用层:系统应用和第三方 App。
这五层不是简单的堆叠,而是通过Binder IPC、JNI、HIDL/AIDL等机制互相通信。很多人学架构只记住了分层图,却忽略了层与层之间"怎么说话",结果一到实际改代码就懵。所以这篇我会把重点放在"层与层之间的接口和通信"上,而不是复述那张人人都见过的框图。
适合谁看:如果你正在做 Android 系统开发、Framework 定制、HAL 驱动适配、系统移植,或者准备系统架构设计师这类考试,这篇内容能帮你把零散的知识点串成一条线。如果你只是 App 开发者,看完也能明白为什么有些 API 在不同机型上行为不一致。
2. Linux 内核层:Android 不是一套全新的系统
2.1 为什么 Android 要基于 Linux 内核而不是从零写
这是很多人没想清楚的问题。Android 完全可以像某些嵌入式系统一样自己写一套内核,但它选择了 Linux。原因很实际:Linux 内核经过几十年发展,在进程调度、内存管理、文件系统、网络协议栈、电源管理这些基础能力上已经非常成熟,而且有庞大的驱动生态。Android 团队要做的不是重造轮子,而是在 Linux 之上做针对移动场景的裁剪和增强。
具体来说,Android 在标准 Linux 内核基础上加了几个关键的东西:
- Binder 驱动:这是 Android 独有的,标准 Linux 没有。它是整个 Android IPC 的基石,后面会详细讲。
- Ashmem(匿名共享内存):用于进程间高效共享大块内存,比如图形缓冲区。
- Low Memory Killer(LMK):移动设备内存有限,需要在内存紧张时主动杀掉后台进程,这套机制是 Android 特有的。
- Wakelock:电源管理相关,控制设备何时可以进入休眠。
- Logger:日志系统。
所以严格来说,Android 用的不是"原版 Linux",而是"打了 Android 补丁的 Linux"。你在做系统移植时,第一步往往就是把 Android 的这些补丁合入到目标芯片的内核源码里。
2.2 内核层到底管哪些事
从架构角度看,内核层主要承担四类职责:
| 职责类别 | 具体内容 | 对上层的影响 |
|---|---|---|
| 进程与线程调度 | CFS 调度器、进程优先级 | 决定 App 响应速度 |
| 内存管理 | 虚拟内存、页缓存、LMK | 决定后台能留多少 App |
| 设备驱动 | 显示、触摸、摄像头、音频、传感器 | 决定硬件能否被识别 |
| 通信与同步 | Binder、Socket、信号量、互斥锁 | 决定进程间能否协作 |
这里有个容易被忽略的点:内核层的驱动质量直接决定了整机的稳定性。我见过太多机器,Framework 层代码写得没问题,但一跑压力测试就重启,最后定位到是某个传感器驱动在高频采样时死锁。所以做系统架构,不能只盯着上层,内核这层的地基必须打牢。
2.3 内核与用户空间的通信方式
内核和上层应用怎么交换数据?常见的有这么几种:
- 系统调用(syscall):最基础的方式,应用通过 open、read、write、ioctl 等进入内核。
- procfs / sysfs:内核把一些状态和配置暴露成文件,用户空间读写文件就等于和内核通信。
- netlink:用于内核主动向用户空间推送事件,比如网络状态变化。
- Binder:Android 特有的高频 IPC 通道。
实际做系统开发时,如果你要新增一个内核功能并让上层能控制它,最省事的做法往往是走 sysfs 或 ioctl,而不是去改 Binder。因为 Binder 的接口改动会牵动 Framework,成本高得多。这是我在实际项目里踩过坑之后总结的经验:能用文件接口解决的,就别动 IPC 协议。
3. HAL 层:让 Android 摆脱"一机一系统"的泥潭
3.1 HAL 存在的根本理由
假设没有 HAL。那么 Framework 层要调用摄像头,就得直接写针对某颗摄像头芯片的代码。换一颗芯片,Framework 就得改。这在手机行业是不可接受的——一个 Android 版本要适配几十上百款机型,每款硬件都不同。
HAL 就是来解决这个问题的。它在 Framework 和内核驱动之间插了一层,定义了一套标准接口。Framework 只调用接口,具体实现由各芯片厂商提供。厂商只要按接口实现自己的 HAL,Framework 就不用改。
用生活化的类比:HAL 就像电源插座标准。你的电器(Framework)只认插头形状,不管电是从火电、水电还是核电来的。发电厂(硬件厂商)只要按标准做插座(HAL),电器就能用。
3.2 HAL 的三种形态与演进
HAL 不是一成不变的,它经历了几个阶段:
- 传统 HAL(Legacy HAL):以
.so动态库形式存在,通过hw_get_module加载。接口用 C 语言定义,结构体里塞满函数指针。 - HIDL(HAL Interface Definition Language):Android 8.0 引入,用于 Treble 项目。把 HAL 拆成独立进程,通过 Binder 化通信,实现 Framework 和 HAL 的解耦。
- AIDL HAL:Android 11 之后主推,用 AIDL 统一描述 HAL 接口,进一步简化。
为什么要从传统 HAL 演进到 HIDL/AIDL?核心目的是让系统可以独立升级。传统 HAL 是库,和 Framework 编译在一起,升级 Framework 就得重新编译整个系统。Treble 之后,HAL 变成独立进程,Framework 升级时 HAL 不用动,厂商适配成本大幅降低。
3.3 一个 HAL 模块的典型结构
以传统 HAL 为例,一个模块通常包含:
// 定义模块结构 struct hw_module_t { uint32_t tag; uint16_t module_api_version; uint16_t hal_api_version; const char *id; const char *name; const char *author; struct hw_module_methods_t* methods; void* dso; // ... }; // 定义设备结构 struct hw_device_t { uint32_t tag; uint32_t version; struct hw_module_t* module; uint32_t reserved[12]; int (*close)(struct hw_device_t* device); };厂商实现时,就是填充这些结构体,把具体的 open、read、write 逻辑挂上去。Framework 通过hw_get_module("camera")拿到模块,再open拿到设备,然后调用设备的方法。
这里有个实操细节:HAL 模块的命名和路径是有约定的。模块 id 要和.so文件名对应,放在/vendor/lib/hw/或/system/lib/hw/下,文件名格式是<id>.<board>.so。如果放错位置或命名不对,hw_get_module会直接返回失败,而且日志里不一定有明显提示,这是新手最容易卡住的地方。
3.4 HAL 开发中的常见坑
我在适配 HAL 时踩过的坑,挑几个典型的说:
- 版本号不匹配:
module_api_version和 Framework 期望的不一致,会导致加载失败。改 HAL 时一定要确认版本号。 - 线程安全:HAL 可能被多个进程同时调用,实现时必须考虑加锁。我见过因为没加锁导致摄像头偶发花屏的案例。
- 资源释放:
close函数里没释放干净,反复打开关闭会内存泄漏,跑久了系统就崩。 - 日志缺失:HAL 层日志默认很少,出问题时建议临时加
ALOGD,定位完再删。
提示:调试 HAL 时,
logcat里过滤HAL、hw_get_module、dlopen这些关键字,能快速定位是加载失败还是调用失败。
4. Framework 层:开发者最熟悉也最容易误解的一层
4.1 Framework 到底包含什么
Framework 层是 Java 世界和 Native 世界的交汇处。它向上给 App 提供 API,向下通过 JNI 调用 Native 库,再通过 Binder 和系统服务通信。核心组件包括:
- ActivityManagerService(AMS):管理 Activity 生命周期、任务栈、进程。
- WindowManagerService(WMS):管理窗口、层级、输入分发。
- PackageManagerService(PMS):管理应用安装、权限、组件信息。
- ContentProvider、BroadcastReceiver、Service的底层支撑。
- View 系统:从 Measure、Layout 到 Draw 的完整流程。
很多人以为 Framework 就是"系统 API 的集合",其实它更重要的角色是系统服务的调度中心。App 调用的每一个 API,背后往往是一次跨进程调用,最终落到某个 SystemService 上执行。
4.2 Binder:Framework 的血管
如果只能记住一个 Framework 相关的机制,那一定是 Binder。它是 Android 最高频的 IPC 方式,AMS、WMS、PMS 全都靠它通信。
Binder 的核心设计是一次拷贝。传统 IPC(如管道、Socket)需要两次数据拷贝:用户空间到内核,内核再到目标用户空间。Binder 通过内存映射,让内核和目标进程共享一块内存,只需要一次拷贝。这在移动设备上意义重大,因为 IPC 极其频繁。
Binder 通信的基本角色:
- Client:发起调用的进程。
- Server:提供服务的进程(如 system_server)。
- ServiceManager:服务注册与查询的中心。
- Binder 驱动:内核里的中转站。
一个典型的调用链是:App 通过getSystemService拿到 AMS 的代理对象(BpBinder),调用方法时数据打包成 Parcel,通过 Binder 驱动传到 system_server,由 BnBinder 解包并调用真正的 AMS 实现。
这里有个关键理解:App 拿到的 AMS 不是真正的 AMS,而是一个代理。真正的 AMS 跑在 system_server 进程里。这个"代理-实体"模式是理解 Framework 的钥匙。
4.3 系统启动流程:从按下电源到桌面出现
理解 Framework 最好的方式之一是跟一遍启动流程。简化版如下:
- Bootloader加载内核。
- 内核启动,挂载根文件系统,启动第一个用户进程
init。 - init 进程解析
init.rc,启动各种 native 服务,其中最关键的是zygote。 - Zygote预加载常用类和资源,然后 fork 出
system_server。 - system_server启动 AMS、WMS、PMS 等核心服务。
- AMS启动 Launcher,桌面出现。
这条链路里,Zygote 的设计非常巧妙。它预加载了大量类和资源,fork 出来的进程直接共享这些内存,避免了每个 App 启动都重新加载,大幅提升启动速度。这也是为什么 Android 应用启动比某些系统快的原因之一。
4.4 改 Framework 的正确姿势
做系统定制时,改 Framework 是家常便饭。但直接改 AOSP 源码风险很高,我的经验是:
- 优先用 overlay 机制:AOSP 支持 RRO(Runtime Resource Overlay),可以在不改源码的情况下替换资源。
- 新增 API 要谨慎:一旦新增公开 API,就要考虑兼容性和 CTS 测试。
- 改系统服务要评估影响面:AMS、WMS 的改动可能影响所有 App,必须做充分回归。
- 善用
dumpsys:dumpsys activity、dumpsys window能直接看到系统服务内部状态,调试神器。
注意:改 Framework 后一定要跑 CTS。很多定制 ROM 过不了 CTS,就是因为 Framework 改动引入了不兼容行为。
5. 应用层与运行时:ART、JNI 与跨语言协作
5.1 ART 虚拟机做了什么
Android 的应用代码跑在 ART(Android Runtime)上。ART 的核心职责是执行 DEX 字节码。和早期的 Dalvik 相比,ART 最大的变化是提前编译(AOT)加即时编译(JIT)的混合模式。
- AOT:安装时把 DEX 编译成机器码,启动快,但安装慢、占空间。
- JIT:运行时编译热点代码,安装快,但启动初期慢。
- 混合模式:结合两者,安装时只做部分编译,运行时根据热度再编译。
理解 ART 对性能优化很关键。比如你知道某个方法被频繁调用,就可以通过减少反射、避免频繁创建对象来降低 JIT 压力。
5.2 JNI:Java 和 Native 的桥
Framework 层大量使用 JNI 调用 Native 代码。JNI 的规则不复杂,但坑不少:
- 局部引用和全局引用:JNI 里的局部引用在方法返回后失效,跨线程使用必须转成全局引用。
- 线程附着:Native 线程要调用 Java 方法,必须先
AttachCurrentThread。 - 异常处理:JNI 调用 Java 方法后要检查异常,否则会带着异常继续执行,行为不可预期。
我见过最典型的 JNI 崩溃是:Native 线程里直接用了 Java 传过来的局部引用,结果引用失效导致野指针。这类问题日志往往指向不明,排查起来很痛苦。所以 JNI 代码一定要严格管理引用生命周期。
5.3 应用层的沙箱机制
每个 App 跑在独立的进程里,有独立的 UID,独立的文件目录。这套沙箱机制保证了应用之间互不干扰。但沙箱也带来限制,比如访问其他应用数据需要通过 ContentProvider。
热词里出现的content://com.xxx.fileprovider/...就是这套机制的体现。FileProvider 是应用间共享文件的官方方式,通过 URI 授权,避免直接暴露文件路径。理解沙箱,才能理解为什么 Android 的文件访问这么"绕"——绕是为了安全。
6. 层与层之间:真正决定架构质量的是接口设计
6.1 纵向通信:从 App 到内核的完整链路
把前面几层串起来,一次典型的"点击屏幕打开相机"操作,链路是这样的:
- 触摸事件由内核输入子系统捕获,通过
/dev/input上报。 - Framework 的 InputManagerService 读取事件,分发给对应窗口。
- App 收到点击,调用 Camera API。
- Camera API 通过 Binder 调用 CameraService。
- CameraService 通过 HIDL/AIDL 调用 Camera HAL。
- HAL 调用内核 V4L2 驱动操作摄像头硬件。
这条链路里任何一环出问题,表现都是"相机打不开"。所以排查系统问题时,从下往上或从上往下逐层验证,比盲目改代码高效得多。
6.2 横向通信:系统服务之间的协作
系统服务之间也大量通信。比如 AMS 启动 Activity 时,要和 WMS 协调窗口,和 PMS 确认组件信息,和 PowerManager 确认屏幕状态。这些协作都通过 Binder 完成。
理解横向通信,才能理解为什么改一个服务会影响另一个服务。我做过一个需求,只是改 AMS 的进程优先级策略,结果影响了 WMS 的窗口动画,因为两者共享了进程状态。这种耦合是架构设计时必须警惕的。
6.3 接口稳定性:Treble 带来的改变
Treble 项目是 Android 架构演进的一个分水岭。它把 HAL 从 Framework 中彻底剥离,定义了稳定的接口。带来的好处是:
- 系统升级更快:Framework 升级不用等厂商适配 HAL。
- 厂商适配更省力:只要 HAL 接口不变,底层不用动。
- 测试更聚焦:VTS 测试专门验证 HAL 接口。
但 Treble 也带来新问题:HAL 变成独立进程后,IPC 开销增加,某些高频调用场景性能下降。所以架构演进永远是权衡,没有银弹。
7. 系统架构的实操排查思路与经验总结
7.1 分层排查法:定位问题的通用套路
遇到系统问题,我习惯按这个顺序排查:
| 层级 | 排查手段 | 典型问题 |
|---|---|---|
| 应用层 | logcat、StrictMode | 崩溃、ANR |
| Framework | dumpsys、systrace | 服务异常、卡顿 |
| Native | tombstone、gdb | 段错误、内存泄漏 |
| HAL | HAL 日志、VTS | 硬件不工作 |
| 内核 | dmesg、ftrace | 驱动报错、死锁 |
关键是先确定问题在哪一层,再深入。很多人一上来就改 Framework,结果问题在内核,白忙一场。
7.2 几个我踩过的真实坑
- Binder 事务过大导致崩溃:Binder 单次传输有大小限制(约 1MB),传大图或大列表时会抛
TransactionTooLargeException。解决办法是分片或改用共享内存。 - HAL 加载顺序问题:某些 HAL 依赖其他 HAL 先启动,
init.rc里的启动顺序写错会导致服务起不来。用on property触发可以解决。 - SELinux 权限拦截:Android 5.0 之后 SELinux 强制模式,HAL 访问某些资源会被拒。日志里搜
avc: denied能看到,需要补te规则。 - Framework 改动导致 CTS 失败:新增 API 没加
@SystemApi或@TestApi注解,CTS 直接挂。
这些坑的共同点是:日志里不一定有直接答案,需要结合架构知识推断。这也是为什么我一直强调要理解层与层的关系,而不是死记 API。
7.3 学习路径建议
如果你想系统掌握 Android 系统架构,我的建议是:
- 先跑通 AOSP 编译,理解构建系统。
- 跟一遍启动流程,从 init 到 Launcher。
- 挑一个系统服务(比如 AMS)深入读源码。
- 自己写一个 HAL 模块,跑通加载和调用。
- 做一次系统定制,比如改开机动画或加系统服务。
这个过程很慢,但走完一遍,你对架构的理解会从"知道有这几层"变成"知道每层怎么协作"。这种理解,是看多少篇架构图都换不来的。
最后分享一个我个人的习惯:每次遇到系统问题,我都会在笔记本上画一遍涉及到的调用链路,标出每一层的接口和通信方式。画着画着,很多想不通的问题就通了。架构这东西,看是看不会的,得动手画、动手改、动手调。