【全栈第12课】App→Framework→HAL 全竖线(阶段二收官)
Android 全栈工程师进阶教程 第12课。App→Framework→HAL 全竖线(阶段二收官)。基于公开 AOSP 知识整理,含原理、可编译示例、常见问题定位。
> 本文是《Android 全栈工程师进阶教程》系列第 12 课。完整 26 课见 GitHub 仓库。
【全栈第12课】App→Framework→HAL 全竖线(阶段二收官)
0. 这节课你将做出什么
把 L1-L11 串成一条完整竖线:App 调 framework 服务 → framework 服务转手调 HAL → HAL(将来再下到驱动)。理解真实产线的分层调用模式。 学完你能回答:App 为什么不直接调 HAL,非要经过 framework 中转?一个完整的"App 点一下 → 硬件响应"链路上,每一层各干什么?
1. 背景:为什么 App 不能直接调 HAL
你可能想:App 想用硬件,直接调 HAL 不就行了?不行,原因:
- 权限:HAL 在 vendor 进程,App 是普通进程,SELinux 不允许 App 直接调 vendor HAL(也不应该)。
- 稳定性:HAL 接口是给 framework 用的,直接暴露给千万个 App 很危险。
- 抽象:framework 提供统一、稳定的 SDK API,屏蔽底层 HAL 的差异(不同厂商 HAL 不一样,App 不该感知)。
所以真实产线一律是:App → framework 服务(中转/权限校验/抽象)→ HAL → 驱动。
2. 全局图:完整竖线(串起 L1-L11)
App
│ getSystemService(HELLO_SERVICE) ← L2/L3
▼ HelloManager(App 进程,BinderProxy)
│ sayHelloViaHal(name) ── Binder ──▶
▼ HelloManagerService(system_server) ← L2 实现,L6 sepolicy
│ 转手调 HAL:IHelloHal::fromBinder(waitForService(...)) ← L10 姿势
▼ ── Binder(system→vendor)──▶ HelloHal(vendor 进程) ← L9,L11 sepolicy
│ ::sayHello 实现
│ (真实场景这里再 open /dev 调 kernel driver,见阶段三 L14-L20)
▼ 返回值一路带回 App
每一层职责:
| 层 | 职责 | 对应课 |
|---|---|---|
| App | 调 SDK API | - |
| HelloManager | App 侧代理,转 Binder | L2 |
| HelloManagerService | 权限校验(getCallingUid)、转调 HAL、缓存降级 | L2/L3/L6 |
| HelloHal | 操作硬件(将来下到驱动) | L9/L11 |
| 驱动 | 真正读写硬件寄存器 | 阶段三 |
3. 某真实机型 真实源码印证
真实例子:App 调 Vibrator.vibrate() → VibratorManagerService(system_server)→ VibratorHalController(L10 那套 fromBinder)→ vibrator HAL → 驱动 → 马达震动。
你写的 hello 竖线就是这个模式的简化版。framework 服务作为"中间人",是这条链的枢纽。
4. 前置准备
- 依赖 L2(HelloManagerService)、L9(HelloHal)、L10(framework 调 HAL 姿势)、L11(HAL sepolicy)。
- 本课主要是把前面的串起来 + 在 HelloManagerService 里加一个"转调 HAL"的方法。
5. 动手:让 framework 服务转调 HAL
在 L2 的 HelloManagerService 里加一个 sayHelloViaHal 方法(示意 framework→HAL 中转):
// HelloManagerService(system_server)新增方法
public String sayHelloViaHal(String name) {
// 权限校验:先确认调用方有资格(L1 的 getCallingUid)
final int uid = Binder.getCallingUid();
// ... 这里可做权限判断 ...
// 拿 HAL(若 HAL 接口生成了 java 后端,system_server 可直接用;
// 否则经 JNI 下到 native 用 ndk 接口,见 L5)
IHelloHal hal = IHelloHal.Stub.asInterface(
ServiceManager.waitForService("vendor.example.demo.IHelloHal/default"));
if (hal == null) return "HAL 不可用"; // 降级:HAL 没起时的兜底
try {
return hal.sayHello(name); // framework 转手调 HAL,把结果带回 App
} catch (RemoteException e) {
return "HAL 调用失败"; // HAL 崩了的兜底
}
}
逐步讲为什么:
- 权限校验在 framework 做:App 调进来,framework 用
getCallingUid判断这个 App 有没有资格用硬件,挡住没权限的。这是 App 不能直接调 HAL 的核心价值之一。 - HAL 不可用时降级:HAL 没起/崩了,framework 返回兜底值,而不是让 App 崩。
- App 全程不碰 HAL:App 只认
HelloManager.sayHelloViaHal,完全不知道底下有没有 HAL、HAL 在哪。这就是抽象。
> 注:Java framework 调 ndk AIDL HAL 有两条路:① HAL 接口同时生成 java 后端,system_server 直接用;② 经 JNI(L5)下到 native 用 ndk 接口。真实硬件打通在阶段三(配合驱动)更完整,本课讲清中转模式。
6. 看底层:这条竖线上的三次 Binder
完整一次 App 调用,跨了两次 Binder:
- App → HelloManagerService:第一次 Binder(App 进程 → system_server)
- HelloManagerService → HelloHal:第二次 Binder(system_server → vendor 进程)
每次 Binder 都各自盖调用方 uid 戳(L1)。所以 HAL 里
getCallingUid拿到的是 system_server 的 uid,不是原始 App 的——这点要注意,权限校验要在 framework 那一层做(它能拿到真正的 App uid)。
7. 编译 & 运行(验证状态如实)
make framework services
# 刷机后 App 调用:
adb logcat | grep -iE "HelloManagerService|HelloHal"
预期(完整链路日志):
HelloManagerService: sayHelloViaHal name=zhoubenliang, caller uid=10xxx(App)
HelloHal: sayHello called (caller uid=1000 system_server) ← 注意 HAL 看到的是 system uid
HelloManagerService: HAL 返回 Hello from HAL, zhoubenliang
App: 收到 Hello from HAL, zhoubenliang
> 验证状态(诚实):本课为串联/集成说明课,展示真实产线中转模式;Java 调 ndk HAL 的完整打通 + 真机验证在阶段三结合驱动更有意义,本课未实跑。
8. 踩坑提醒
- 以为 HAL 能拿到 App 的 uid:错!HAL 经 framework 中转,getCallingUid 拿到的是 system_server 的 uid。权限校验必须在 framework 层(能拿到 App uid)。
- App 直接调 HAL:SELinux 会拒(也不该这么设计)。一律经 framework。
- framework 没做降级:HAL 没起就 NPE/抛异常给 App。要兜底。
- 同步调用链太长卡主线程:App→framework→HAL 两次 Binder 都同步,任一层慢都卡。考虑异步/oneway。
- Java 调 ndk HAL 没生成 java 后端:aidl_interface 没开 java 后端,system_server 直接用不了,要么开 java 后端要么走 JNI。
9. 常见问题分析与定位(全竖线实战)
9.1 常见问题清单
- App 调用返回兜底值("HAL 不可用")
- App 调用 SecurityException(权限被拒)
- 链路某处卡住,App 无响应
- HAL 拿到的 uid 不对导致权限逻辑错
- 数据在某一层"丢了"或变了
9.2 分析与定位
分层定位法:沿竖线从上往下,确认每一层"收到了没"。
# 第1层 App:有没有发出调用
adb logcat | grep "<App tag>"
# 第2层 framework 服务:收到了吗
adb logcat | grep HelloManagerService
# 第3层 HAL:收到了吗
adb logcat | grep HelloHal
adb shell lshal | grep hello # HAL 起着吗
① 返回"HAL 不可用" — framework 拿不到 HAL → HAL 没起(回 L9/L11)或服务名不一致(L10)。
② SecurityException — framework 权限校验拒了,或 SELinux 拦。看 framework 服务里的校验逻辑 + logcat avc。
③ 卡住 — 哪一层日志停了就卡在那层之后。framework 日志有、HAL 日志没有 → 卡在 framework→HAL 这次 Binder(HAL 慢/死锁,看 HAL 进程 trace)。
④ uid 不对 — 见踩坑1,权限校验放错层。framework 层用 App 的 uid,别在 HAL 层判 App 权限。
⑤ 数据丢/变 — 在每层日志打印参数/返回值,看哪层进去对、出来错。
9.3 定位决策树
全竖线出问题
├─ 沿竖线看日志:App→HelloManagerService→HelloHal,哪层没收到=卡在上一层之后
├─ "HAL 不可用" → lshal HAL 起没(L9/L11)+ 服务名(L10)
├─ SecurityException → framework 校验逻辑 + logcat avc
├─ 卡住 → 哪层日志停 → 那层之后(framework→HAL 卡看 HAL trace)
├─ uid 不对 → 权限校验放 framework 层(用 App uid),别在 HAL 判 App
└─ 数据丢/变 → 每层打印参数/返回值,定位哪层出错
10. 小结(阶段二收官)
- App 不直接调 HAL:经 framework 中转,为了权限/稳定/抽象。
- 完整竖线:App→HelloManager→HelloManagerService(权限校验+转调+降级)→HAL→(驱动)。
- 跨两次 Binder,HAL 拿到的是 system_server uid,权限校验要在 framework 层做。
- framework 是枢纽:校验、转调、缓存、降级都在这层。
- 排查口诀:沿竖线看每层日志,哪层没收到就卡在上层之后;HAL 不可用查 lshal。
🎓 阶段二(HAL+Treble,L7-L12)收官:你已掌握写一个 HAL、让它起来、被 framework 调、最终服务 App 的完整能力。
下节预告
L13:内核编译 + menuconfig + GKI —— 进入阶段三(Kernel/驱动)。HAL 下面就是 kernel driver,先搞懂现代 Android 内核(GKI)怎么编。