尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Android底层开发实战:AIDL+NDK+HAL全链路集成指南

Android底层开发实战:AIDL+NDK+HAL全链路集成指南 做 Android 底层开发的人迟早会碰到 AIDL、NDK、HAL 这三个词凑到一块儿的场景。通常是你刚拿到一块新板子或者要接一颗新外设、新传感器的时候需求长得很固定App 要能控制硬件Java/Kotlin 这层得有人去调 C/C最终还得有东西能跟内核驱动对上话。这条链路看着不复杂真从零开始搭一堆构建问题能把人磨到没脾气。这篇我就按自己实际踩过的路径把 AIDL 接口、NDK 本地库、HAL 层实现从无到有过一遍重点放在 Android Studio 工程里怎么组织代码、CMake 怎么配、AIDL 为什么生成失败、NDK 版本到底怎么选以及最后把 HAL 这一层用模拟实现串起来。不是教科书式讲概念就是一套能直接拿去改的实战流程适合刚接触 Android 底层开发、或者接了板子不知道怎么下手的朋友参考。1. 先看清套路AIDL、NDK、HAL 在一条链上各自干什么1.1 三个概念的分工用一句话就能说清AIDLAndroid Interface Definition Language解决的是跨进程接口定义问题。Android 里的 Binder 机制让不同进程能像调用本地方法一样互相调但跨进程传参需要定义好接口语言AIDL 就是干这个的。你写一个 .aidl 文件构建系统会帮你生成 Java 或 C 的 Stub/Proxy 代码通信细节全部封装好。NDKNative Development Kit是让你用 C/C 写 Android 代码的工具链。它提供了交叉编译环境、一系列 native API 和 JNI 绑定机制。JVM 跑在虚拟机里C/C 跑在真实 CPU 上中间靠 JNI 这座桥连通。NDK 干的事情就是帮你把 .c/.cpp 编译成 .so并在 Java 层能 load 进去调用。HALHardware Abstraction Layer是硬件抽象层。它定义了一套稳定的 C 结构体接口让上层的 Android 框架不直接面对千奇百怪的内核驱动。驱动是仓库内部怎么码货都行HAL 是仓库对外开的统一窗口上层只需要知道窗口长什么样不用管仓库里怎么摆。1.2 为什么这三个东西经常一起出现实际项目里你很少单独用 AIDL也很少单独写一个孤立 NDK 库。最常见的组合是App 希望控制一个 Linux 驱动设备但出于权限和安全考虑App 不能直接 open(/dev/xxx)于是中间要隔好几层。完整链路通常长这样App 通过 bindService 绑定系统服务服务端实现 AIDL 接口接口实现里调用 JNIJNI 进入 native 层native 层再通过 HAL 的 hw_get_module 或直接 dlsym 加载厂商库最终下发 ioctl 或 sysfs 操作到内核。每一层都是边界每一层都有自己的一套规范和坑。AIDL 和 HAL 的关系在 Android 11 之后也有变化。Android 8 引入 Treble 结构时主要推 HIDL后来 Android 11 开始允许用 AIDL 作为稳定的 HAL 接口语言AIDL Stable。现在新项目我更推荐直接用 AIDL少学一套语法构建也更统一Google 官方也在往这个方向收拢。1.3 开发形态决定你后面踩的坑先分辨你是在哪种环境里干活这比写代码还重要。第一种是 AOSP 源码树内开发用 Android.bp 或 Android.mk 编译HAL 模块、系统服务基本都走这种好处是能直接进系统镜像坏处是编译一次系统要等很久调试链路长。第二种是 Android Studio 独立工程App 模块加本地 C/C 库用 Gradle 和 CMake 构建适合做系统应用、厂商调试工具、或者先在用户态把逻辑验证通。我下面的实战以 Android Studio 工程为主线因为大多数学习场景和中等规模项目都在这里HAL 层用模拟实现替代。你要真上了 AOSP原理完全一样只是编译脚本换一下。2. 从零搭工程骨架模块怎么切、构建体系怎么选2.1 模块划分思路避免一锅炖不要把所有东西塞进一个 app 模块。至少分成三层app 层纯 Java/Kotlin只管 UI、权限申请、bindService不碰任何 native 细节。aidl 接口层定义跨进程契约包括主接口和回调接口。native 层实现 JNI 逻辑编译成 .so内部再分业务逻辑和 HAL 模拟实现。如果以后要复用 native 代码可以把 native 抽成独立模块也可以把 aidl 单独做成 library module 给多个 app 用。项目初期如果就我一个人写我会先保证包结构清晰不做过度拆分但 aidl 目录和 cpp 目录一定要分开不要让 Java 和 C 文件混在一起。一个能直接参考的目录结构长这样app/ ├── src/main/ │ ├── aidl/ │ │ └── com/example/led/ │ │ ├── ILedControl.aidl │ │ └── ILedStatusCallback.aidl │ ├── java/com/example/led/ │ │ ├── LedService.java │ │ └── MainActivity.java │ ├── cpp/ │ │ ├── CMakeLists.txt │ │ ├── led_jni.cpp │ │ ├── hal_led.h │ │ └── hal_led_stub.c │ └── AndroidManifest.xml └── build.gradle2.2 构建方式选型直接决定坑的类型同一个 native 功能你有三种构建路径预编译 .so厂商已经编好 HAL 和 JNI 库你只负责放 jniLibs。省事但架构不匹配时会报 UnsatisfiedLinkError或者在别的 ABI 设备上直接闪退。CMake 源码编译库和 app 一起构建能自己控制编译器参数、链接选项是本篇的重点。缺点是要处理 CMake 和 NDK 的兼容性。AOSP 源码编译Android.bp / Android.mk能编进系统但没人想为了验证一个接口去刷一整天系统。我建议学习阶段全部走 CMake 源码编译路线对构建过程有完全掌控力出现符号丢失、链接顺序问题也容易复现和调试。后面真要进系统再把 CMake 里的编译命令翻译成 Android.bp 即可。2.3 Gradle 与 SDK/NDK 版本匹配是最容易忽略的坑Android Studio 工程里最烦的不是代码是 Gradle、SDK、NDK、CMake 四者之间的版本匹配。Gradle 插件版本和 AGPAndroid Gradle Plugin版本要匹配AGP 和 Build Tools 版本要匹配NDK 又对 minSdkVersion 有要求。这些版本链条只要断一环报错信息就千奇百怪。我自己现在的基线配置比较保守AGP 8.x 配 Gradle 8.xNDK 用 26.x 或 27.xCMake 用 3.22.1 或 AS 内置版本minSdk 24 以上。这套组合在近两年新项目里基本不会遇到版本兼容的恶心问题。老项目迁移时尽量先按原项目的版本曲线走不要一上来就升 AGP否则副作用比收益大得多。3. AIDL 接口从编写到生成文件放错位置报错到怀疑人生3.1 AIDL 文件目录规则和语法要点AIDL 文件放置目录是硬性规定必须放在src/main/aidl/包名路径/下。这里的包名路径要和文件里声明的 package 完全一致不一致就会导致 aidl 文件生成失败或者生成的 Java 类找不到。AIDL 支持的语法并不复杂基本类型、String、Parcelable、List、Map接口方法支持 in/out/inout 定向 tag。初学者最容易漏掉 import比如回调接口写在另一个文件里主接口里使用它却没有 importide 有时候不报红编译时才炸锅。一个实际的 LED 控制接口我建议这样写// ILedControl.aidl package com.example.led; import com.example.led.ILedStatusCallback; interface ILedControl { void setOn(boolean on); void setBrightness(int level); // 0-255 int getBrightness(); void registerCallback(ILedStatusCallback callback); void unregisterCallback(ILedStatusCallback callback); }// ILedStatusCallback.aidl package com.example.led; interface ILedStatusCallback { void onStatusChanged(boolean on, int level); }这里有个细节AIDL 里定义的接口方法参数默认是 in如果只读可以不写如果服务端要改这个参数再返回给调用方就用 out要双向就用 inout。跨进程的开销和 inout 的复杂度成正比能用 in 就别用 out。3.2 aidl 生成失败基本逃不过这几个原因我统计过自己项目里遇到过的 AIDL 生成失败原因基本集中在这几类首先是路径和包名不匹配。文件在src/main/aidl/com/example/led/下package 必须是com.example.led哪怕少一层目录也会失败。其次是 import 缺失两个 aidl 文件互相引用时最容易犯。第三是定向 tag 写错比如 oneway 接口方法里用了 out/inout 参数编译器直接报错。第四是使用了不支持的泛型或者嵌套类型AIDL 的泛型支持非常有限ListMapString, Parcelable 这种组合经常出问题。还有一类隐藏很深文件编码。AIDL 文件必须是 UTF-8不要带 BOM。Windows 上记事本编辑过的文件经常带着 BOM 头Build 时会把 BOM 当作包名的一部分报一个让你完全摸不着头脑的错。3.3 如何验证 AIDL 是否成功生成在 Android Studio 里执行 Build 后生成代码在app/build/generated/aidl_source_output_dir/debug/目录下。能看到类似ILedControl.java的文件就说明生成成功然后 app 代码里就可以直接ILedControl.Stub和ILedControl.Proxy了。如果生成目录里找不到文件先从 Build 输出面板搜索 aidl 关键字通常会有具体的错误行号。注意 Android Studio 有时候不会实时刷新生成目录我在 Windows 上遇到过一次文件已经生成了编辑器里还是报红执行一次File - Sync Project with Gradle Files就正常了。自己从 eclipse 时代项目迁移过来的人要特别小心老项目经常把 aidl 放在src/根目录下Android Studio 默认不认这个路径需要你在 build.gradle 里手动指定 sourceSets。这也是很多“移植 Android Studio 项目”之后 AIDL 报错的根源。3.4 顺带说一句 AIDL 和 HIDL 的关系我见过不少人把 AIDL 和 HIDL 混在一起问其实核心区别在于服务对象。HIDL 是 Treble 时代为 vendor 和 system 分区解耦而设计的接口语言AIDL Stable 在 Android 11 开始支持就是把 AIDL 语法用于系统组件和 HAL 之间的稳定接口。如果你现在新写一个软件层面的跨进程接口我建议直接用 AIDL不需要再学 HIDL 那套半自动的构建方式。除非你是要维护老的 vendor 实现否则 AIDL 是更省心的一条路。4. NDK 配置与 C/C 本地库构建这章全是干货4.1 ndk not configured 这类报错其实是 NDK 没装对“ndk not configured. download it with sdk manager. preferred ndk version is ...” 这个报错十个人里有九个都见过。字面意思是说你没装 NDK或者 NDK 版本和项目要求的不一致。处理方式很简单打开 SDK Manager切到 SDK Tools 标签页勾选 NDK 和 CMake 装上。然后到 build.gradle 的 android 块里显式声明ndkVersion 26.1.10909125。有的老教程让你在 local.properties 里写ndk.dir...AGP 4.2 之后已经不建议这么干优先用 ndkVersion 属性。NDK 版本选择上我的建议是不要盲目追最新。r23 开始移除了 GCC只留 clangr26 之后对 minSdkVersion 有更高要求。如果项目里要集成某些老的第三方库比如老版本 OpenSSL、FFmpeg它们的编译脚本可能只适配特定 NDK强行升级会导致一堆编译错误。先确认依赖库的要求再选 NDK而不是反过来。4.2 build.gradle 里 native 构建配置模板app 模块的 build.gradle 里需要配两处CMake 路径和 ABI 过滤。android { ndkVersion 26.1.10909125 defaultConfig { externalNativeBuild { cmake { cppFlags -stdc17 arguments -DANDROID_STLc_shared } } ndk { abiFilters arm64-v8a, x86_64 } } externalNativeBuild { cmake { path src/main/cpp/CMakeLists.txt } } }ABI 过滤器一定要加。我早期图省事不限制 ABI结果构建时间翻倍还经常编出 x86 版本在某些 ARM 设备上闹笑话。开发调试阶段只留 arm64-v8a 和 x86_64 基本够用真机覆盖也主要是 arm64。4.3 CMakeLists.txt 里怎么链接 HAL 库如果你手里的 HAL 是厂商已经编好的 .so正确做法是用 IMPORTED 目标链接而不是直接把 so 扔到 jniLibs 里让 app 加载。cmake_minimum_required(VERSION 3.22.1) project(led_driver) add_library(led_jni SHARED led_jni.cpp hal_led_stub.c ) add_library(hal_led SHARED IMPORTED) set_target_properties(hal_led PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libhal_led.so ) find_library(log-lib log) target_link_libraries(led_jni hal_led ${log-lib} )为啥用 IMPORTED 而不是直接链接因为 import 目标能把“这个库是外部提供的”和“我的代码怎么编译”解耦开构建系统不会尝试去重新编译它也方便你后面替换不同版本的 hal 库。直接把 so 放 jniLibs 的缺点是它会被当作 app 的 native 依赖打进 APK但 HAL 本来就该由系统/vendor 分区提供不应该出现在 app 里逻辑上就错了。4.4 JNI 层绑定 HAL推荐 RegisterNatives 方式JNI 函数有两种注册方式。第一种是动态命名函数名写成Java_com_example_led_LedNative_setOnJVM 按约定自动找到第二种是用RegisterNatives在JNI_OnLoad里手动注册。我强烈推荐RegisterNatives。它避免了一长串名字效率更高而且能让你在加载时做统一初始化检查。代码长这样#include jni.h #include cstring #include hal_led.h static led_device_t* g_led nullptr; extern C JNIEXPORT jboolean JNICALL nativeInit(JNIEnv* env, jobject thiz) { if (g_led ! nullptr) return JNI_TRUE; g_led hal_led_open(); return g_led ! nullptr; } extern C JNIEXPORT void JNICALL nativeSetOn(JNIEnv* env, jobject thiz, jboolean on) { if (g_led ! nullptr g_led-set_on ! nullptr) { g_led-set_on(g_led, on ? 1 : 0); } } static const JNINativeMethod methods[] { {nativeInit, ()Z, (void*) nativeInit}, {nativeSetOn, (Z)V, (void*) nativeSetOn}, }; extern C JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env nullptr; if (vm-GetEnv(reinterpret_castvoid**(env), JNI_VERSION_1_6) ! JNI_OK) { return JNI_ERR; } jclass clazz env-FindClass(com/example/led/LedNative); if (clazz nullptr) return JNI_ERR; if (env-RegisterNatives(clazz, methods, sizeof(methods) / sizeof(methods[0])) ! JNI_OK) { return JNI_ERR; } return JNI_VERSION_1_6; }JNI 方法签名要特别小心()Z表示返回 boolean 无参(Z)V表示接收 boolean 返回 void类型表记错了RegisterNatives会静默失败Java 层调用时直接报UnsatisfiedLinkError。4.5 构建本地依赖时的高频坑构建本地依赖时最常见的报错是 Gradle 下载 zip 或依赖包中途失败报 “Zip file ... is corrupt” 之类。多数情况是缓存损坏清理~/.gradle/caches下对应目录重新构建就能解决。如果反复失败可以切换到网络更稳的镜像仓库。这里多说一句公司内网环境容易出现代理导致的证书问题报错里会带 “PKIX path building failed”这种情况优先检查镜像源的 HTTPS 证书别一开始就怀疑代码。另一个高频坑是 C 运行时库不匹配。你 CMake 里选了c_shared但 APK 里没打包libc_shared.so安装到设备上运行时就报library libc_shared.so not found。处理方式要么把ANDROID_STL改成c_static要么在构建配置里显式打包。我的习惯是用c_shared加jniLibs手动放入对应 so因为多个 so 共享同一个 STL 可以减少包体积和符号冲突。还有 CMake 找不到构建程序这个错CMake was unable to find a build program corresponding to Ninja基本是 CMake 版本和 AGP 默认版本不匹配。去 SDK Manager 里装一个 3.22.1 的 CMake再指定cmake { version 3.22.1 }即可。5. HAL 层怎么接用模拟实现把整条链路走通5.1 HAL 到底长什么样先看传统结构传统 Android HAL 本质是一个 C 接口库核心是hw_module_t和hw_device_t两个结构体。hw_get_module根据模块 ID 和厂商配置去/vendor/lib64/hw/下加载对应的.so加载完后你从 module 的方法列表里拿到设备结构体再调用结构体里的函数指针。要注意的是现在很多搜索里的“hal 库”其实指的是 STM32 标准外设库和 Android HAL 完全不是一回事。我在做嵌入式的时候也用过那种 hal 库那是芯片厂商封装好的寄存器操作 API。别搞混了本篇里的 HAL 都是指 Android 硬件抽象层。5.2 手写一个 HAL 模拟实现不开真机也能验证我们假设要控制一个 LED先定义 HAL 接口头文件// hal_led.h #ifndef HAL_LED_H #define HAL_LED_H #include stdint.h #ifdef __cplusplus extern C { #endif typedef struct led_device { int (*set_on)(struct led_device* dev, int on); int (*set_brightness)(struct led_device* dev, int level); int (*get_brightness)(struct led_device* dev); void* priv; } led_device_t; led_device_t* hal_led_open(void); void hal_led_close(led_device_t* dev); #ifdef __cplusplus } #endif #endif然后写一个纯内存模拟的实现。这里我特意不直接操作真实驱动就是为了让你在 Android Studio 里也能编译、运行、验证整个链路。真实环境里这个文件里的操作会变成 open(/dev/led)、ioctl、sysfs 读写等等。// hal_led_stub.c #include hal_led.h #include stdlib.h #include string.h static int stub_set_on(led_device_t* dev, int on) { // 真实环境: write sysfs or ioctl return 0; } static int stub_set_brightness(led_device_t* dev, int level) { if (level 0 || level 255) return -1; return 0; } static int stub_get_brightness(led_device_t* dev) { return 128; } led_device_t* hal_led_open(void) { led_device_t* dev (led_device_t*)calloc(1, sizeof(led_device_t)); if (dev NULL) return NULL; dev-set_on stub_set_on; dev-set_brightness stub_set_brightness; dev-get_brightness stub_get_brightness; return dev; } void hal_led_close(led_device_t* dev) { if (dev) free(dev); }这样就有了一个不依赖硬件的 HAL 实现JNI 层直接hal_led_open()就能拿到设备句柄。等真机驱动就绪替换 stub 内部实现即可上层代码完全不用动。这种“先跑通结构再替换内部逻辑”的做法是我做底层开发以来最推荐的做事方式。5.3 Java 服务层把 AIDL、JNI、HAL 串起来有了 native 层基础Java 侧就清晰了。LedService作为 AIDL 服务端继承ILedControl.Stub内部调用LedNative静态方法。LedNative声明 native 方法并用System.loadLibrary(led_jni)加载 CMake 编出来的 so。public class LedNative { static { System.loadLibrary(led_jni); } public static native boolean nativeInit(); public static native void nativeSetOn(boolean on); public static native int nativeGetBrightness(); }这里有个使用习惯要注意JNI 环境里的 native 状态要小心多进程问题。如果 LedService 跑在独立进程里nativeInit 初始化的是该进程内的全局变量另一个进程即使 bind 了同一个 Service调用的是远端进程里的 native 函数不会有数据串台。这其实是 Binder 设计的好处但也意味着你用静态变量缓存 native 状态时要以进程为边界思考而不是以 app 为边界。5.4 关于真实系统集成简化 demo 和 AOSP 的差异在真机系统里App 直接dlopenHAL 库是不合规的生产环境应该通过系统服务间接访问而且要处理 SELinux 权限、HAL 进程上下文等各种限制。我这个 demo 之所以能这么直白地打通是因为它面向的是开发调试和自用工具。如果你想把它变成一个正式系统能力标准路径是HAL 实现放 vendor 分区由独立的 HAL service 进程托管系统服务通过 AIDL 接口访问 HAL serviceApp 只能和系统服务通信。不同 SoC 平台比如 RK3576 这类还会有自己的 HAL 命名规范和权限配置定制系统镜像时还要处理 vendor 和 system 分区的依赖关系。这些都是在 AOSP 源码树里完成的不可能在 Studio 工程里一步到位。建议先在这个简化工程里把通信链路、JNI 绑定、接口设计都验证清楚再去 AOSP 里平移代码效率会高很多。6. 构建与运行常见问题速查表能救命的排错经验6.1 Android Studio 和 Gradle 层面的报错ndk not configured、CMake was unable to find a build program、SDK location not found这类属于环境问题基本都是 SDK/NDK/CMake 没装全或路径不对。处理顺序是先检查 local.properties 里的sdk.dir再确认 SDK Manager 里 NDK 和 CMake 是否安装最后看 build.gradle 里的版本声明是不是写错了或冲突了。Gradle 报 zip 损坏优先清缓存重试。不要一上来就删整个 Gradle 目录先把报错里提到的那一个文件删掉。比如报~/.gradle/wrapper/dists/gradle-8.7-bin/hash/gradle-8.7-bin.zip损坏就把对应版本目录删了让它重新下载。6.2 AIDL 生成失败重点查这四处文件路径是否在src/main/aidl/包名路径/下文件内 package 是否和路径一致引用其他 aidl 的 import 是否齐全编码是否是 UTF-8 无 BOM。还有一个容易被坑的点是文件名必须和接口名一致不能ILed.aidl里定义的接口叫LedControl编译器会直接报接口名不匹配。6.3 运行期崩溃先分清是哪一层的问题运行期崩溃最怕的是无头绪。我的排查顺序是先看 logcat 里是否报UnsatisfiedLinkError有的话说明 so 没打进 APK 或 JNI 方法名没对上没有的话看是否报JNI DETECTED ERROR这是 JNI 类型用错了再看有没有 SIGSEGV这基本就是 native 层空指针或数组越界了。HAL 相关的权限问题通常表现为open: Permission denied或者hw_get_module返回负值。真机上遇到这类问题大概率是 SELinux policy 没放行先用adb shell setenforce 0验证一下仅限 userdebug 固件能跑就说明是策略问题然后去补 sepolicy。6.4 高频问题速查表现象常见原因解决方向ndk not configuredNDK 未安装或版本不符SDK Manager 安装build.gradle 指定 ndkVersionAIDL 生成 Java 类找不到包名与路径不一致 / import 缺失修正 aidl 目录结构补 importUnsatisfiedLinkErrorso 未打包 / JNI 签名错误检查 jniLibs 和 abiFilters核对 RegisterNatives 签名libc_shared.so not foundSTL 选择 shared 但未打包改用 c_static 或手动打包 soGradle zip 损坏下载中断 / 缓存损坏删除对应缓存重新下载CMake 找不到 NinjaCMake 版本不匹配安装 3.22.1指定 cmake versionopen /dev 节点被拒SELinux 或权限问题检查 sepolicyuserdebug 固件可先 setenforce 0 验证JNI DETECTED ERRORJNI 层类型用错 / deleteLocalRef 问题检查 JNIEnv 使用确认没有跨线程用 JNIEnv最后分享一个我自己坚持的习惯每次工程新加一层我一定留一个最小可执行的测试入口。AIDL 写完先写一个纯 Java 的 fake 实现验证 binder 通信没问题JNI 写完先做 native 单元测试不经过 AppHAL 写完先写一个命令行小工具直接调 hal_led_open。这样做的好处是一旦整条链路哪个环节出问题我能快速定位到具体层而不是在 App 到内核之间来回猜。开发底层链路排查能力比编码能力更决定你的速度。
返回列表