Android Stable AIDL 编译时绕过 API 版本检查的方法
Android Stable AIDL 编译时绕过 API 版本检查的方法
注意:本文基于 Android 14/U 撰写
Qidi 2025.05.29
0. 背景
随着 Android 版本迭代,越来越多的 HAL 接口描述从 HIDL 转向 AIDL。
从 Android 10 开始,Stable AIDL
概念被引入,用于在开发时验证接口稳定性(详情参考 官网)。对于 AOSP Frameworks 依赖的 HAL 接口,Stable AIDL 检查默认是开启的。比如 audiocontrol
,在其 Android.bp
中可以看到:
aidl_interface {
name: "android.hardware.automotive.audiocontrol",
vendor_available: true,
srcs: ["android/hardware/automotive/audiocontrol/*.aidl"],
imports: [
"android.hardware.audio.common-V1",
"android.media.audio.common.types-V2",
],
stability: "vintf",
backend: {
java: {
sdk_version: "module_current",
min_sdk_version: "31",
apex_available: [
"//apex_available:platform",
"com.android.car.framework",
],
},
},
versions_with_info: [
{
version: "1",
imports: [
"android.hardware.audio.common-V1",
"android.media.audio.common.types-V2",
],
},
{
version: "2",
imports: [
"android.hardware.audio.common-V1",
"android.media.audio.common.types-V2",
],
},
{
version: "3",
imports: [
"android.hardware.audio.common-V1",
"android.media.audio.common.types-V2",
],
},
],
frozen: true,
}
audiocontrol
启用了 AIDL 接口稳定性检查,而且 AOSP 已经开发并冻结了 3 个版本的接口。
1. 编译错误
在 Android 系统工程师的工作中,去修改 HAL 接口几乎是无法避免的。对于采用 AIDL 的 HAL 而言,这意味着要去修改 *.aidl
文件。
然而由于 Stable AIDL 的作用,如果不做任何准备工作,直接修改 *.aidl
文件并编译,一定会编不过。会看到类似下方的编译错误:
ERROR: out/soong/.intermediates/hardware/interfaces/audio/aidl/android.hardware.audio.core-api/dump/android/hardware/audio/core/IModule.aidl:36.1-10: Expected equality of these values:
hardware/interfaces/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/IModule.aidl
Which is: "@VintfStability\ninterface IModule
...
if [ $(cd 'hardware/interfaces/automotive/audiocontrol/aidl/aidl_api/android.hardware.automotive.audiocontrol/3' && { find ./ -name "*.aidl" -print0 | LC_ALL=C sort -z | xargs -0 sha1sum && echo 2; } | sha1sum | cut -d " " -f 1) = $(tail -1 'hardware/interfaces/automotive/audiocontrol/aidl/aidl_api/android.hardware.automotive.audiocontrol/3/.hash') ]; then touch out/soong/.intermediates/hardware/interfaces/automotive/audiocontrol/aidl/android.hardware.automotive.audiocontrol-api/checkhash_3.timestamp; else cat 'system/tools/aidl/build/message_check_integrity.txt' && exit 1; fi
###############################################################################
# ERROR: Modification detected of stable AIDL API file #
###############################################################################
Above AIDL file(s) has changed, resulting in a different hash. Hash values may
be checked at runtime to verify interface stability. If a device is shipped
with this change by ignoring this message, it has a high risk of breaking later
when a module using the interface is updated, e.g., Mainline modules.
由于被修改的 *.aidl
文件、包括其描述的接口,处于冻结状态,所以编译失败了。
2. 绕过 AIDL API 版本检查进行编译
对于这种修改冻结 *.aidl
文件的行为,AOSP 认为必须通过创建新版本的接口描述文件来进行,以实现不同接口之间的版本管理和兼容。具体来说,就是使用 m <package>-update-api
和 m <package>-freeze-api
命令,来解冻、新增/修改、冻结接口(详情参考 官网)。
以 audiocontrol
为例,这两个命令如下:
m android.hardware.automotive.audiocontrol-update-api
m android.hardware.automotive.audiocontrol-freeze-api
这一系列操作完成后,可以看到 Android.bp
的 versions_with_info
字段自动添加了一个版本号,并且在 aidl_api/<package>/
目录下也生成了一个新版本号对应的目录。
尽管按照 AOSP 建议的操作可以解决编译问题,但弊端在于,项目处于开发阶段时,工程师对接口的修改极其频繁,每做一次修改,就会生成一个新版本号,反而不便于代码维护和理解。
我们需要一种在开发阶段轻松修改 AIDL 接口的方法。
分析编译错误日志,可知 Stable AIDL 是通过比对 hash值 来判断接口变动。回忆在老 Android 版本上开发 HIDL 接口时,可以通过手动修改 hash值 来绕过检查,所以现在我们也尝试这样做来绕过 Stable AIDL 的编译检查。
基于这个思路,还是以 audiocontrol
为例。我们先在 AIDL API 开发目录下完成接口开发,也就是在 hardware/interfaces/automotive/audiocontrol/aidl/android/*
目录下完成对 *.aidl
文件的新增/修改;然后将变更后的 *.aidl
文件拷贝/替换到现有最新的 AIDL API 目录下,也就是 hardware/interfaces/automotive/audiocontrol/aidl/aidl_api/android.hardware.automotive.audiocontrol/3/*
目录下;最后重新生成 hash值,并覆盖 hardware/interfaces/automotive/audiocontrol/aidl/aidl_api/android.hardware.automotive.audiocontrol/3/.hash
中的原值就行了。
生成 hash值 的命令参见 system/tools/aidl/build/aidl_api.go
中的 aidlVerifyHashRule
和 versionForHashGen
方法(详情参考 官网),代码截取如下:
aidlVerifyHashRule = pctx.StaticRule("aidlVerifyHashRule", blueprint.RuleParams{
Command: `if [ $$(cd '${apiDir}' && { find ./ -name "*.aidl" -print0 | LC_ALL=C sort -z | xargs -0 sha1sum && echo ${version}; } | sha1sum | cut -d " " -f 1) = $$(tail -1 '${hashFile}') ]; then ` +
`touch ${out}; else cat '${messageFile}' && exit 1; fi`,
Description: "Verify ${apiDir} files have not been modified",
}, "apiDir", "version", "messageFile", "hashFile")
func versionForHashGen(ver string) string {
// aidlHashGen uses the version before current version. If it has never been frozen, return 'latest-version'.
verInt, _ := strconv.Atoi(ver)
if verInt > 1 {
return strconv.Itoa(verInt - 1)
}
return "latest-version"
}
这个过程,可以手动完成,也可以使用我编写的脚本 update_aidl_apis.sh
完成。脚本内容如下:
#!/bin/bash
#############################################################
#
# This script enables you to modify and compile *.aidl files
# without executing stable-aidl commands like
# "m <package>-update-api" and "m <package>-freeze-api", thus
# to avoid too many aidl versions generated at development
# phase.
#
# Details about stable-aidl, refer to
# https://source.android.google.cn/docs/core/architecture/aidl/stable-aidl?hl=en
#
# This script must be placed under aidl HAL directory, i.e.,
# hardware/interfaces/automotive/audiocontrol/aidl/
# hardware/interfaces/audio/aidl/
#
# Author : huang_qi_di@hotmail.com
# Date : 05/28/2025
#
#############################################################
COLOR_RED='\033[0;31m'
COLOR_YELLOW='\033[1;33m'
COLOR_GREEN='\033[0;32m'
COLOR_CLEAR='\033[0m'
checkRunningContext() {
PREREQUISITE_DIR_EXPECTED=2
PREREQUISITE_DIR_FOUND=0
#echo Checking if prerequisite dirctories exist...
if [ -d ./aidl_api ]; then
#echo aidl_api found.
PREREQUISITE_DIR_FOUND=$(($PREREQUISITE_DIR_FOUND+1))
else
echo aidl_api dir not found!
fi
if [ -d ./android ]; then
#echo android found.
PREREQUISITE_DIR_FOUND=$(($PREREQUISITE_DIR_FOUND+1))
else
echo android dir not found!
fi
if [ $PREREQUISITE_DIR_EXPECTED -eq $PREREQUISITE_DIR_FOUND ]; then
echo All prerequisites found. Good to go!
echo ------------------------------------
echo
else
echo -e ${COLOR_RED}Error: Prerequisites not met! Please put this script under correct path then retry.${COLOR_CLEAR}
echo
exit 1
fi
}
SELECTD_API_IDX=1
selectApiToUpdate() {
echo Select an API number to be updated from below list:
echo -e ${COLOR_YELLOW}
ls -1 aidl_api | awk '{print NR,$0}'
echo -e ${COLOR_CLEAR}
echo ------------------------------------
read -p "Waiting for input: " SELECTED_API_IDX
SELECTED_API_IDX=$(echo $SELECTED_API_IDX | cut -d " " -f 1)
SELECTED_API_NAME=$(ls -1 aidl_api | awk 'NR==api_idx' api_idx=$SELECTED_API_IDX)
#echo Selected idx=$SELECTED_API_IDX, name=$SELECTED_API_NAME
}
DIR_DEV_AIDL=""
MAX_API_VER=1
DIR_MAX_API_AIDL=""
DIR_CURRENT_API_AIDL=""
composeAidlFileDirectory() {
API_DIR_COUNT=$(ls aidl_api/$SELECTED_API_NAME | wc -w)
MAX_API_VER=$(($API_DIR_COUNT-1))
DIR_MAX_API="./aidl_api/$SELECTED_API_NAME/"$MAX_API_VER
DIR_CURRENT_API="./aidl_api/$SELECTED_API_NAME/current"
DIR_SUB_AIDL_FILES=$(echo $SELECTED_API_NAME | sed 's#\.#/#g')
DIR_MAX_API_AIDL=$DIR_MAX_API/$DIR_SUB_AIDL_FILES
DIR_CURRENT_API_AIDL=$DIR_CURRENT_API/$DIR_SUB_AIDL_FILES
DIR_DEV_AIDL=$DIR_SUB_AIDL_FILES
echo
echo Below AIDL files will be used for this update:
echo ./$DIR_DEV_AIDL
echo
ls $DIR_DEV_AIDL
echo
echo Targets:
echo
echo $DIR_MAX_API_AIDL
echo $DIR_CURRENT_API_AIDL
echo
}
copyAidlFiles() {
cp $DIR_DEV_AIDL/*.aidl $DIR_MAX_API_AIDL/
cp $DIR_DEV_AIDL/*.aidl $DIR_CURRENT_API_AIDL/
}
DIR_ANDROID_ROOT=""
RETRY_COUNT=20
findAndroidRootDir() {
RETRY_COUNT=$(($RETRY_COUNT-1))
if [ $RETRY_COUNT -eq 0 ]; then
echo -e ${COLOR_RED}Error: Unable to locate Android root directory! Please put this script under correct path then retry.${COLOR_CLEAR}
echo
exit 1
fi
#echo Searching for Android root dir: $(pwd)
DIR_HW_FOUND=$(
pwd | awk -F "/" '{
if($NF=="interfaces" && $(NF-1)=="hardware"){
print "1"
}else{
print "0"
}
}'
)
if [ $DIR_HW_FOUND -eq 1 ]; then
if [ ! -e ../../build/envsetup.sh ]; then
DIR_HW_FOUND=0
fi
fi
#echo DIR_HW_FOUND=$DIR_HW_FOUND
if [ $DIR_HW_FOUND -eq 1 ]; then
cd ../..
DIR_ANDROID_ROOT=$(pwd)
echo Detected DIR_ANDROID_ROOT=$DIR_ANDROID_ROOT
echo
else
cd ..
findAndroidRootDir
fi
}
updateHashFile() {
DIR_SCRIPT_ROOT=$(pwd)
#echo $DIR_SCRIPT_ROOT
findAndroidRootDir
OUT_HASH_FILE=$DIR_ANDROID_ROOT/out/soong/.intermediates/hardware/interfaces/automotive/audiocontrol/aidl/$SELECTED_API_NAME-api/checkhash_$MAX_API_VER.timestamp
echo OUT_HASH_FILE=$OUT_HASH_FILE
if [ -e $OUT_HASH_FILE ]; then
rm $OUT_HASH_FILE
echo Hash cache file cleared.
fi
cd $DIR_SCRIPT_ROOT/$DIR_MAX_API
OLD_HASH=$(cat ./.hash)
# API hash value is calculated per aidlVerifyHashRule at https://android.googlesource.com/platform/system/tools/aidl/+/refs/tags/aml_med_340922010/build/aidl_api.go#47
HASH_OBFUSCATOR="latest-version"
if [ $MAX_API_VER -gt 1 ]; then
HASH_OBFUSCATOR=$(($MAX_API_VER-1))
fi
echo HASH_OBFUSCATOR=$HASH_OBFUSCATOR
NEW_HASH=$({ find ./ -name "*.aidl" -print0 | LC_ALL=C sort -z | xargs -0 sha1sum && echo $HASH_OBFUSCATOR; } | sha1sum | cut -d " " -f 1)
echo
echo OLD_HASH=$OLD_HASH
echo NEW_HASH=$NEW_HASH
echo $NEW_HASH > ./.hash
}
main() {
echo
checkRunningContext
selectApiToUpdate
composeAidlFileDirectory
copyAidlFiles
updateHashFile
echo
echo -e ${COLOR_GREEN}=== Update Done ===${COLOR_CLEAR}
echo
}
main
该脚本必须放到 hardware/interfaces/<packageName>/aidl/
目录下运行,比如 hardware/interfaces/automotive/audiocontrol/aidl/
。
经测试,该方法有效,可以实现在不增加 AIDL API 版本的情况下随意修改 AIDL 接口的目的。
3. 禁用 Stable AIDL 检查
对于 AOSP Frameworks 所不依赖的 AIDL 接口,比如我们自己开发的 Vendor 服务,可以禁用 Stable AIDL 检查,从而避免编译问题。
操作很简单,只需要在 Vendor 服务对应的 Android.bp
中去掉 stability: "vintf"
、versions_with_info
和 frozen: true
字段,并增加 unstable:true
字段;然后删除 *.aidl
文件中的 @VintfStability
描述符,即可。
4. 总结
简言之,在不禁用 Stable AIDL 检查的情况下,修改 AIDL 文件后,既想避免生成新 AIDL API 版本,又想绕过编译时版本检查,无论采用上述何种办法,均按如下流程操作即可:
修改AIDL文件 --> 覆盖旧AIDL文件 --> 生成新hash --> 覆盖旧hash --> 编译