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-apim <package>-freeze-api 命令,来解冻、新增/修改、冻结接口(详情参考 官网)。

audiocontrol 为例,这两个命令如下:

m android.hardware.automotive.audiocontrol-update-api
m android.hardware.automotive.audiocontrol-freeze-api

这一系列操作完成后,可以看到 Android.bpversions_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 中的 aidlVerifyHashRuleversionForHashGen 方法(详情参考 官网),代码截取如下:

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_infofrozen: true 字段,并增加 unstable:true 字段;然后删除 *.aidl 文件中的 @VintfStability 描述符,即可。


4. 总结

简言之,在不禁用 Stable AIDL 检查的情况下,修改 AIDL 文件后,既想避免生成新 AIDL API 版本,又想绕过编译时版本检查,无论采用上述何种办法,均按如下流程操作即可:

修改AIDL文件 --> 覆盖旧AIDL文件 --> 生成新hash --> 覆盖旧hash --> 编译
posted @ 2025-05-29 18:25  Qidi_Huang  阅读(514)  评论(0)    收藏  举报