资讯动态

AIDL到HAL完整链路:Android底层集成与构建避坑指南

发布时间:2026/9/15 3:22:57 来源:尧图企业网站定制
从 AIDL 到 HAL这条链路我自己踩了大半年。开头必须先说清楚这三个东西虽然名字都熟但它们根本不是同一层的技术拆开看都不难真正让人崩溃的是把它们串起来之后的一次次构建失败、so 加载不到、AIDL 生成不出来。这篇文章把我从零搭建这套链路的过程写出来接口怎么定义、JNI 怎么注册、HAL 模块怎么被 Native 层加载、gradle/cmake/soong 各层构建配置怎么对齐以及我在实战里碰到的高频报错和排查思路。适合正在做 Android 系统定制、设备驱动集成、硬件网关类应用的开发者参考也适合打算把 C/C 算法库接进 Android 服务的同学读一下。1. 链路拆解AIDL、NDK、HAL 到底在一条线的哪个位置先把概念摆正。网上聊 AIDL 的教程大多停留在“两个 App 之间通信”聊 NDK 的教程又往往止步于“写一个 hello world 的 JNI”而聊 HAL 的内容基本散落在 AOSP 源码解析里。极少有人告诉你这三者在一次真实的设备数据读取场景里是怎么接成一条线的。1.1 一条真实的数据流App - Binder - JNI - Hal - Kernel我把一条完整的硬件数据读取链路按层级拆开看Android App (Java/Kotlin) ↓ 通过 AIDL 定义的 Binder 接口 System Service或专用特权进程持有 HAL 访问权限 ↓ JNI 桥 Native 共享库 (.so) ↓ dlopen / hw_get_module HAL 模块厂商实现的硬件抽象层 so 库 ↓ open() / ioctl() / read() Linux 内核设备驱动(/dev/xxx)这里有几个容易被新手搞混的点。AIDL 不只是 App 与 App 通信它同样是系统服务对外暴露能力的方式JNI 也不只是“给 Java 调 C”它是把 Java 层拿到的请求转交到 Native 层的必经管道HAL 更不是驱动驱动在内核态HAL 只是用户态的一个封装壳。1.2 为什么要用 AIDL而不是直接 JNI这是一个非常常见的问题。如果只是 App 内部读一个传感器数据哪怕用 JNI 直接调底层也说得通但一旦涉及跨进程比如 App 要访问一个运行在独立进程里的系统服务就必须走 Binder。AIDL 的价值就在这里它定义的是 IPC 接口契约编译器会帮你生成 Proxy 和 Stub一个负责跨进程调用一个负责接收并分发调用。那为什么还要 NDK因为 AIDL 的 Java 层只能拿到 Java 类型真正的硬件控制、算法库、串口读取、设备节点操作绝大多数厂商代码是用 C/C 写的这些代码不可能用 Java 重写一遍。所以链路中必须有一个 JNI 层把 Java 调用翻译成 native 函数调用再由 native 函数去加载 HAL。1.3 HAL 到底“抽象”了什么说句大白话HAL 就是 Android 为了让上层不用关心厂商差异而设的一道“适配层”。比如摄像头、传感器、音频这些硬件每家芯片方案都不同但 Google 希望 framework 层只需要面对统一的接口至于你是用高通方案还是别的方案那是 HAL 内部的事。我在做自定义硬件接入时HAL 模块其实就是一个特殊的动态库导出固定符号HAL_MODULE_INFO_SYM里面放一个hw_module_t结构体再提供 open 方法让你拿到具体的设备句柄。上层不用管这个库是谁编译的、里面怎么操作寄存器只要知道调用open之后能拿到一个设备对象就行。2. 环境与构建配置把 Android Studio、NDK、CMake 一次配齐很多项目卡在“代码写好了但构建不过”八成是环境问题。先说结论Android 生态构建链路是 SDK、build-tools、NDK、CMake、AGP、Gradle 共同决定的版本匹配比什么都重要。2.1 版本如何选型我自己使用比较稳的组合给出表格参考组件版本说明Android SDK Platform34编译目标版本Android Build-Tools34.0.0打包工具NDK25.2.9519653必须显式指定别让 AGP 猜CMake3.22.1在 SDK Manager 里单独装Android Gradle Plugin8.2.x和 Gradle 8.x 配套Gradle8.6从 gradle-wrapper.properties 控制有一个基本规律AGP 越新要求的 Gradle 版本越高而 NDK 版本需要与 AGP 兼容。去 Android 官方文档查 AGP 与 NDK 的默认匹配表是最靠谱的方式但更省事的是在build.gradle里显式写死ndkVersion不要依赖 AGP 自动挑。2.2 build.gradle 关键配置AIDL、NDK、CMake 三者的配置全部集中在 app 模块的build.gradle里android { compileSdk 34 namespace com.example.sensorbridge defaultConfig { applicationId com.example.sensorbridge minSdk 27 targetSdk 34 versionCode 1 versionName 1.0 ndk { abiFilters armeabi-v7a, arm64-v8a, x86_64 } externalNativeBuild { cmake { cppFlags -stdc17 arguments -DANDROID_STLc_shared } } } externalNativeBuild { cmake { path src/main/cpp/CMakeLists.txt version 3.22.1 } } ndkVersion 25.2.9519653 buildFeatures { aidl true } }abiFilters一定要写清楚否则 AGP 默认把armeabi-v7a,arm64-v8a,x86,x86_64全部打进去编译时间成倍增加不说某些 NDK 函数在不同 ABI 下的表现还不一样调试时容易遇到“真机能跑模拟器挂”的怪现象。2.3 “NDK not configured”到底错在哪搜索热词里有个高频报错NDK not configured. Download it with SDK Manager. Preferred NDK version is xxxxx。这个错误的本质是AGP 根据项目的 ndkVersion 去找 NDK但本机 SDK 目录里没有安装对应版本。处理办法很简单Android Studio 里打开 SDK Manager。切到 SDK Tools 标签页。勾选 NDK (Side by side) 和 CMake。安装弹窗里提示的那个首选版本。如果你在命令行环境下工作也可以用 sdkmanagersdkmanager --install ndk;25.2.9519653 cmake;3.22.1这里我踩过一个坑装了 NDK 但没装 CMake 的话第一次跑externalNativeBuild依然会报 CMake 找不到。SDK 目录下的 CMake 和系统 CMake 不是一回事前者在$ANDROID_HOME/cmake下。3. AIDL 接口设计与代码生成这一步挂了后面全白搭AIDL 是整条链路的入口也是最容易在早期翻车的地方。很多人遇到的“aidl 文件生成失败”绝大多数是文件目录、包名、import 这三者之间任何一个对不上。3.1 目录结构和 AIDL 基础语法AIDL 文件不能随手乱放。Android Studio 项目里AIDL 文件默认放在src/main/aidl/包名路径/注意这个包名路径要和文件内的package声明保持一致。以我的示例工程com.example.sensorbridge为例完整的 AIDL 文件内容如下。先定义一个自定义数据类型SensorData.aidlpackage com.example.sensorbridge; parcelable SensorData;然后是接口文件ISensorBridge.aidlpackage com.example.sensorbridge; import com.example.sensorbridge.SensorData; interface ISensorBridge { SensorData readSensor(); void registerCallback(ISensorCallback callback); void unregisterCallback(ISensorCallback callback); }回调接口ISensorCallback.aidlpackage com.example.sensorbridge; import com.example.sensorbridge.SensorData; interface ISensorCallback { void onSensorData(inout SensorData data); }3.2 自定义 Parcelable 类型的完整实现凡是 AIDL 方法里用到的自定义类必须满足三个条件实现Parcelable接口、提供CREATOR静态字段、有对应的.aidl声明文件。SensorData.java的核心实现package com.example.sensorbridge; import android.os.Parcel; import android.os.Parcelable; public class SensorData implements Parcelable { public float temperature; public float humidity; public long timestamp; public SensorData(float temperature, float humidity, long timestamp) { this.temperature temperature; this.humidity humidity; this.timestamp timestamp; } protected SensorData(Parcel in) { temperature in.readFloat(); humidity in.readFloat(); timestamp in.readLong(); } Override public void writeToParcel(Parcel dest, int flags) { dest.writeFloat(temperature); dest.writeFloat(humidity); dest.writeLong(timestamp); } Override public int describeContents() { return 0; } public static final CreatorSensorData CREATOR new CreatorSensorData() { Override public SensorData createFromParcel(Parcel in) { return new SensorData(in); } Override public SensorData[] newArray(int size) { return new SensorData[size]; } }; }这里有个约定writeToParcel里字段的写入顺序必须和Parcel构造里的读出顺序完全一致。AIDL 生成的 Stub 在跨进程传输时会自动进行 Parcel 序列化和反序列化字段顺序错乱会直接导致另一端拿到脏数据。3.3 服务端实现与代码生成检查接口定义好了就可以写一个实现类然后在 Service 的onBind里返回。public class SensorBridgeService extends Service { private final ISensorBridge.Stub binder new ISensorBridge.Stub() { Override public SensorData readSensor() { return new SensorData(25.6f, 48.2f, System.currentTimeMillis()); } Override public void registerCallback(ISensorCallback callback) { // 保存到 RemoteCallbackList } Override public void unregisterCallback(ISensorCallback callback) { // 从 RemoteCallbackList 移除 } }; Override public IBinder onBind(Intent intent) { return binder; } }编译项目后自动生成的 Java 代码会出现在app/build/generated/aidl_source_output_dir/variant/aidl/...这个目录。官方文档举例build/generated/aidl_source_output_dir/debug/out/com/example/sensorbridge/ISensorBridge.java当你发现“AIDL 文件生成失败”第一步一定是去这个目录看有没有生成对应的 Java 文件。如果没有再回头检查包名、import、语法。我遇到过一次非常诡异的情况IDE 里 AIDL 没报错但构建时仍然失败最后发现是SensorData.aidl与SensorData.java的包名不一致一个在com.example.sensorbridge另一个在com.example.sensorbridge.model。系统在生成 Stub 时会尝试 import 这个 parcelable包名对不上就会编译失败。4. JNI 与 NDK 实现把 AIDL 服务接到 Native 世界AIDL 跑通只是第一步Service 里返回的数据目前还是 Java 层写死的。现在要做的是让 Java 层通过 JNI 调用 native 方法native 方法再去加载 HAL 模块拿真实数据。4.1 CMakeLists.txt 的编写src/main/cpp/CMakeLists.txt是 NDK 构建的入口。我习惯把代码组织清晰一点cmake_minimum_required(VERSION 3.18.1) project(sensor_bridge) add_library( sensor_bridge SHARED native_sensor_bridge.cpp hal_loader.cpp ) find_library(log-lib log) find_library(android-lib android) target_link_libraries( sensor_bridge ${log-lib} ${android-lib} ) target_include_directories( sensor_bridge PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include )这里必须说明一个容易踩的点find_library(log-lib log)的 log 库在 NDK 里对应的是 Android 的 logcat 输出。如果没有链接它.cpp里__android_log_print会出现链接错误。android库则提供ANativeWindow等接口纯计算场景可以不链但链上没有任何坏处。4.2 JNI 函数命名与 C 实现JNI 有两种注册方式。最简单的是按命名规则来Java_包名_类名_方法名。包名里的点号替换成下划线这也是大多数示例代码的做法。假设我在com.example.sensorbridge.SensorBridge类里声明了一个 native 方法readRawData()#include jni.h #include android/log.h #include cstring #define LOG_TAG SensorBridgeNative #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) extern C JNIEXPORT jbyteArray JNICALL Java_com_example_sensorbridge_SensorBridge_readRawData( JNIEnv *env, jobject /* this */) { LOGI(readRawData called); // 假装从 HAL 层读到了字节数据 const char *raw sensor-raw-bytes; size_t len strlen(raw); jbyteArray result env-NewByteArray(static_castjsize(len)); if (result nullptr) { return nullptr; } env-SetByteArrayRegion(result, 0, static_castjsize(len), reinterpret_castconst jbyte *(raw)); return result; }Java/Kotlin 侧对应object SensorBridge { init { System.loadLibrary(sensor_bridge) } external fun readRawData(): ByteArray }需要特别提醒extern C是必须的C 的函数签名和 C 不一样不加就会导致符号找不到运行时抛UnsatisfiedLinkError。4.3 RegisterNatives 方式为什么更值得用长函数名的 JNI 方式在小项目里挺好但真实工程里类名一深函数名就会变得很长而且如果代码混淆开了Java 侧类名变了JNI 查找也会出问题。更推荐的方式是在JNI_OnLoad里用RegisterNatives注册。static JNINativeMethod gMethods[] { {readRawData, ()[B, (void *) readRawData}, }; JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv *env nullptr; if (vm-GetEnv((void **) env, JNI_VERSION_1_6) ! JNI_OK) { return JNI_ERR; } jclass clazz env-FindClass(com/example/sensorbridge/SensorBridge); if (clazz nullptr) { return JNI_ERR; } if (env-RegisterNatives(clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0])) 0) { return JNI_ERR; } return JNI_VERSION_1_6; }gMethods里的字符串()[B是 JNI 方法签名。这个签名体系很容易把人绕晕记住几个常见的就够了Java 类型签名intIfloatFlongJbooleanZStringLjava/lang/String;byte[][Bint[][IvoidV返回值类型写在括号后面比如(ILjava/lang/String;)V表示参数是 int 和 String返回值是 void。写错这个方法签名是UnsatisfiedLinkError的头号原因而且这个错误在 Java 层拿到的是很笼统的提示基本只能靠日志多打点来定位。5. HAL 层接入Native 代码怎么加载硬件模块前面铺垫了这么多真正“从零搭建”的重头戏是这个部分。AIDL 和 JNI 解决的是上层调用问题HAL 解决的是底层硬件适配问题。这里我要再强调一次本文的 HAL 是 Android Hardware Abstraction Layer不是嵌入式里 STM32 的 HAL 库搜索的时候别搞混。5.1 传统 HAL 模块的核心结构一个传统 HAL 模块本质上是动态库固定导出名为HAL_MODULE_INFO_SYM即HMI的符号符号类型是hw_module_t。我用一个自定义传感器 HAL 举例。sensor_hal.h#ifndef SENSOR_HAL_H #define SENSOR_HAL_H #include hardware/hardware.h #define SENSOR_HARDWARE_MODULE_ID sensor typedef struct sensor_device_t { struct hw_device_t common; int (*get_raw_data)(struct sensor_device_t *dev, char *buffer, int buffer_len); } sensor_device_t; #endifhardware.h是 AOSP 里libhardware的头文件。如果在纯 NDK 环境里没有这个头文件也可以自己定义一份最小结构体或者直接跳过标准 HAL 框架用dlopendlsym的方式加载自定义动态库。我在自己可控的原型项目里很多时候直接走后者下面会细说。5.2 用 dlopen 加载 HAL 模块的实战写法hw_get_module是传统 libhardware 提供的统一加载入口但它强依赖系统源码里的 module 搜索路径和权限模型。对于从零搭建的原型验证用dlopen最直接#include dlfcn.h #include android/log.h #define LOG_TAG HalLoader int load_hal_module(const char *module_path, void **handle) { *handle dlopen(module_path, RTLD_NOW); if (*handle nullptr) { __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, dlopen failed: %s, dlerror()); return -1; } return 0; } int call_hal_get_raw_data(void *handle, char *buffer, int buffer_len) { using GetRawDataFunc int (*)(char *, int); void *symbol dlsym(handle, sensor_get_raw_data); if (symbol nullptr) { __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, dlsym failed: %s, dlerror()); return -1; } auto func reinterpret_castGetRawDataFunc(symbol); return func(buffer, buffer_len); }这里有两个注意点。一是dlsym找到的符号名必须和 HAL 库里实际导出的保持一致C 文件里导出sensor_get_raw_data就要用这个名字去取如果 HAL 模块是 C 写的记得把函数声明为extern C否则会被 name mangling。二是dlopen的路径要小心在 Android 8.0 之后访问/vendor/lib64下的库受 selinux 限制普通 App 进程根本没有权限打开这些路径。所以在我的实践里一般只把这种dlopen模式放在系统级进程或自定义 HAL 进程中验证。5.3 AOSP 里的 HAL 构建Android.bp如果你的项目本身就处于 AOSP 环境构建 HAL 模块不能再用 gradle而是用 Soong。一个最小化的 HAL 库在Android.bp中cc_library_shared { name: sensor.default, vendor: true, relative_install_path: hw, srcs: [sensor_hal.cpp], shared_libs: [ libhardware, liblog, ], cflags: [-Wall, -Werror], }vendor: true表示该模块会安装在 vendor 分区relative_install_path: hw表示安装到hw/目录最终路径类似/vendor/lib64/hw/sensor.default.so。传统 HAL 库的命名规则就是模块名.厂商名.sohw_get_module会根据该规则去搜索。5.4 新版系统 HALAIDL for HALGoogle 在推进 Treble 架构的过程中把 HAL 接口从 HIDL 逐步迁移到 AIDL这就是所谓的“AIDL for HAL”。在 AOSP 里定义一个 HAL 服务接口可以直接用我们前面熟悉的 AIDL// packages/modules/DroidSdk/foo/... 或 device/.../aidl/ aidl_interface { name: vendor.acme.sensor, versions: [1, 2], srcs: [aidl/vendor/acme/sensor/*.aidl], }Soong 会自动根据.aidl文件生成 C、Java 和 NDK 三种语言的接口代码。编译出来的服务进程以 Binder 方式注册到servicemanager上层模块通过AServiceManager或 Java 的ServiceManager获取代理对象。这一块对于刚从标准 App 开发转过来的同学比较抽象其实可以理解成“把 HAL 服务做成一个系统级的 AIDL 服务”只不过这个服务运行在 vendor 进程里拥有访问硬件设备的权限。6. 构建坑速查表从 Gradle 到 Soong 的实战避坑手册最后这部分是全文最值钱的内容我把实战中遇到的高频问题、报错原文和解决方案整理成了表格。针对“构建”这个关键词这一节是我踩坑最多的地方。报错/现象根因解决方案aidl file generation failedAIDL 包名与文件路径不一致、自定义 parcelable 没有对应的 aidl 声明、import 缺失统一包名按src/main/aidl/包名/放置文件检查自定义类型是否有parcelable XXX.aidlNDK not configured. Download it with SDK Manager. Preferred NDK version is ...本机缺少项目指定的 NDK 版本用 SDK Manager 安装对应版本或用命令sdkmanager --install ndk;版本号Gradle 构建报zip END header not foundGradle 缓存里的 zip 包损坏删除~/.gradle/caches下的对应模块目录重新构建CMake was unable to find a build program没有安装 CMake或 CMake 文件不完整SDK Manager 安装 CMake指定externalNativeBuild.cmake.versionUnsatisfiedLinkError: dlopen failed: ... is not foundso 没有打进 APK 或 ABI 不匹配检查abiFilters是否覆盖目标设备确认 CMake 输出的 so 被sourceSets正确打包JNI native 方法找不到函数签名错误、忘记extern C、RegisterNatives 注册时机不对核对javap -s生成的方法签名JNI_OnLoad 里先 FindClass 再注册hw_get_module返回错误模块路径不对、selinux 权限限制先用dlopen写日志验证确认 HAL 库在/vendor/lib64/hw/下且权限 SEPolicy 放通soong 构建提示missing dependenciesAndroid.bp 里的 shared_libs 少了libhardware、liblog等检查 HAL 模块依赖按编译错误逐个补全6.1 AIDL 生成失败的三个隐藏原因我见过不止一次“AIDL 文件明明没问题就是生成失败”的情况。除了明显错误还有三个隐藏原因。第一AIDL 接口方法里使用了in和out方向标记但自定义 Parcelable 类型没有正确声明方向。非基本类型默认方向是in如果要修改需要显式写inout或out。这里的细节是out类型在跨进程传输时不会把对象内容传过去只会在返回时填充如果你在服务端想读取对象里的字段就会拿到底层晦涩难懂的空数据。第二在同一模块里同时使用 Java AIDL 和 Kotlin生成的代码路径虽然不受影响但如果你在 Kotlin 里直接用ISensorBridge.Stub某些混合语言项目里会因为 Java/Kotlin 编译顺序问题报“找不到类”。解决办法是把接口类也用 Java 编写或者在 Kotlin 中使用file:JvmName确定类名。第三Android Studio 预览里 AIDL 文件不会即时报错。很多时候要等完整构建结束后才看到日志日志在Build/Output面板里并不显眼容易被忽略。真正有用的做法是关注build/generated/aidl_source_output_dir/目录如果目录空着说明生成环节就已经失败和后续编译无关。6.2 NDK 构建中“最贵的十分钟”CMake 首次构建非常慢因为要配置整个 toolchain。如果第一次构建因为abiFilters没写默认四个 ABI 全部编译时间会翻两到三倍。对此我的做法是def localAbi project.hasProperty(abi) ? project.property(abi) : arm64-v8a android { defaultConfig { ndk { abiFilters localAbi } } }然后命令行构建时指定./gradlew assembleDebug -Pabix86_64本地调试用 x86_64最终出包再用./gradlew assembleRelease全 ABI 编译。能把“一次构建五分钟”压缩到“一次构建一分钟”。6.3 权限与 SELinux从零搭建最常见的隐藏炸弹如果只是纯 NDK 项目不存在 SELinux 问题。但一旦链路接入了 HAL并且运行在系统级进程SELinux 就是绕不过去的坎。典型现象是 dmesg 或 logcat 里出现avc: denied { open } for ... scontext... tcontext...然后dlopen或open(/dev/xxx)静默失败。在 AOSP 里需要在device/.../sepolicy/vendor/下写对应的 te 文件比如给处理 HAL 调用的域添加权限allow hal_sensor_default vendor_sensor_dev:chr_file rw_file_perms; allow hal_sensor_default sysfs_sensor:file r_file_perms;具体的 te 配置和平台强相关我在这里不展开。但原理值得说透Android 各分区和进程都有独立的 SELinux 标签从高权限域到低权限域访问规则没放行就会被拒。调试原型的捷径是先用adb root adb shell setenforce 0临时验证但正式产品必须补齐规则。从一次完整调试验证整个链路我最后再分享一次完整的调试验证路径也是我每次接入新硬件时都会走一遍的流程。第一步先把 HAL 相关代码注释掉只留 AIDL 服务返回模拟数据。用同一个进程内的 Service 绑定测试跑通 Binder 的读调用和回调。第二步把 JNI 加进来。写一个简单的 native 函数返回固定字节数组从 Kotlin 里调用确认.so能被加载、方法能执行。这一步能快速暴露 ABI 和签名问题。第三步才把dlopen加进来先加载一个空 so 库确认路径和权限没问题。然后逐层加真实硬件读取逻辑。这种“层层递进每层都有验证点”的方式可以让你在出问题时精确知道是哪一层的问题。相比一次性写完再构建省下的是几个星期的联调时间。有一点我觉得很关键这个对于刚开始接触 Android 底层开发的朋友尤其适用不要迷信自动生成AIDL 也好JNI 注册也好HAL 加载也好都要学会看生成的中间文件和运行日志。Android 自动化的确很完善但自动化的另一半是构建系统的“黑盒子”黑盒子出问题的时候能把你从坑里拉出来的只有对底层机制的理解以及一个一个的日志点。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价