안정적 AIDL

Android 10에는 Android 인터페이스 정의 언어(AIDL) 인터페이스에서 제공하는 애플리케이션 프로그래밍 인터페이스(API)/Application Binary Interface(ABI)를 추적할 수 있는 새로운 방법인 안정적 AIDL에 대한 지원이 추가되었습니다. 안정적 AIDL에는 AIDL에 비해 다음과 같은 주요 차이점이 있습니다.

  • 인터페이스는 aidl_interfaces를 통해 빌드 시스템에서 정의됩니다.
  • 인터페이스에는 구조화된 데이터만 포함될 수 있습니다. 원하는 유형을 나타내는 parcelable은 AIDL 정의에 따라 자동으로 생성되며 자동으로 마셜링되고 마셜링 해제됩니다.
  • 인터페이스는 안정적인 것으로 선언할 수 있습니다(이전 버전과 호환 가능). 이 경우 API는 AIDL 인터페이스에 인접한 파일을 통해 추적되고 버전이 지정됩니다.

AIDL 인터페이스 정의

aidl_interface의 정의는 다음과 같습니다.

aidl_interface {
        name: "my-aidl",
        srcs: ["srcs/aidl/**/*.aidl"],
        local_include_dir: ["srcs/aidl"],
        imports: ["other-aidl"],
        versions: ["1", "2"],
        stability: "vintf",
        backend: {
            java: {
                enabled: true,
                platform_apis: true,
            },
            cpp: {
                enabled: true,
            },
            ndk: {
                enabled: true,
            },
        },

    }
    
  • name: AIDL 인터페이스를 고유하게 식별하는 AIDL 인터페이스 모듈의 이름입니다.
  • srcs: 인터페이스를 구성하는 AIDL 소스 파일의 목록입니다. com.acme 패키지에 정의된 AIDL 유형인 Foo의 경로는 <base_path>/com/acme/Foo.aidl이어야 하며 <base_path>Android.bp가 있는 디렉터리와 관련된 모든 디렉터리가 될 수 있습니다. 위의 예에서 <base_path>srcs/aidl입니다.
  • local_include_dir: 패키지 이름이 시작하는 경로입니다. 이는 위에 설명한 <base_path>에 해당합니다.
  • imports: 이 인터페이스에서 가져오려는 다른 AIDL 인터페이스 목록입니다(선택사항). 가져온 AIDL 인터페이스에 정의된 ADIL 유형은 import 구문으로 액세스할 수 있습니다.
  • versions: api_dir 아래에 고정된 이전 버전의 인터페이스로, Android R부터 versionsaidl_api/name 아래에 고정됩니다. 고정된 버전의 인터페이스가 없는 경우 이 속성을 지정해서는 안 되며 호환성 확인도 이루어지지 않습니다.
  • stability: 이 인터페이스의 안정성 약속을 나타내는 플래그입니다(선택사항). 현재는 "vintf"만 지원합니다. 이 플래그가 설정되지 않았다면 이 플래그는 이 컴파일 컨텍스트 내에서 안정성 있는 인터페이스에 대응합니다(따라서, 여기 로딩된 인터페이스는 system.img에서 함께 컴파일된 것과 같이 함께 컴파일된 파일에만 사용될 수 있음). 이 플래그를 "vintf"로 설정하면 이는 안정성을 약속하는 것입니다. 즉, 인터페이스가 사용되는 한 이 인터페이스는 안정적으로 유지되어야 합니다.
  • backend.<type>.enabled: 이 플래그는 AIDL 컴파일러가 코드를 생성할 각 백엔드를 전환합니다. 현재 지원되는 백엔드는 java, cppndk 세 가지입니다. 이러한 백엔드는 기본적으로 모두 사용 설정되어 있습니다. 특정 백엔드가 필요하지 않은 경우 명시적으로 사용 중지해야 합니다.
  • backend.<type>.apex_available: 생성된 스터브 라이브러리를 사용할 수 있는 APEX 이름의 목록입니다.
  • backend.java.platform_apis: 자바 스터브 라이브러리를 플랫폼에서 비공개 API로 빌드할지를 제어하는 플래그입니다(선택사항). stability"vintf"이면 이 플래그는 "true"로 설정되어야 합니다.
  • backend.java.sdk_version: 자바 스터브 라이브러리가 빌드된 SDK 버전을 지정하기 위한 플래그입니다(선택사항). 기본값은 "system_current"입니다. backend.java.platform_apis가 참일 때는 이 값을 설정하면 안 됩니다.

versions와 사용 설정된 백엔드의 조합마다 스터브 라이브러리가 만들어집니다. 특정 백엔드의 특정 스터브 라이브러리 버전을 참조하는 방법은 모듈 이름 지정 규칙을 확인하세요.

AIDL 파일 작성

안정적 AIDL의 인터페이스는 구조화되지 않은 parcelable이 안정적이지 않아 허용되지 않는다는 점을 제외하면 기존 인터페이스와 유사합니다. 안정적 AIDL의 가장 큰 차이점은 parcelable이 정의되는 방식입니다. 이전에는 parcelable이 전방 선언되었습니다. 안정적 AIDL에서는 parcelable 필드와 변수가 명시적으로 정의됩니다.

// in a file like 'some/package/Thing.aidl'
    package some.package;

    parcelable SubThing {
        String a = "foo";
        int b;
    }
    

boolean, char , float, double, byte, int, long, String의 기본값이 현재 지원되지만 필수는 아닙니다.

스터브 라이브러리 사용

스터브 라이브러리를 모듈에 종속 항목으로 추가한 후 이를 파일에 포함할 수 있습니다. 다음은 빌드 시스템의 스터브 라이브러리 예시입니다. Android.mk는 레거시 모듈 정의에도 사용할 수 있습니다.

cc_... {
        name: ...,
        shared_libs: ["my-module-name-cpp"],
        ...
    }
    # or
    java_... {
        name: ...,
        // can also be shared_libs if desire is to load a library and share
        // it among multiple users or if you only need access to constants
        static_libs: ["my-module-name-java"],
        ...
    }
    

C++ 예시:

#include "some/package/IFoo.h"
    #include "some/package/Thing.h"
    ...
        // use just like traditional AIDL
    

자바 예시:

import some.package.IFoo;
    import some.package.Thing;
    ...
        // use just like traditional AIDL
    

버전 관리 인터페이스

이름이 foo인 모듈을 선언하면 빌드 시스템에 모듈의 API를 관리하는 데 사용할 수 있는 타겟도 생성됩니다. 빌드 시 foo-freeze-api는 Android 버전에 따라 다음 인터페이스 버전에서 사용하기 위해 api_dir 또는 aidl_api/name 아래에 새로운 API 정의를 추가합니다. 또한 이렇게 빌드하면 versions 속성에서 추가 버전을 반영하도록 업데이트합니다. versions 속성이 지정되면 빌드 시스템은 고정된 버전 간에 호환성 확인을 실행하며 ToT와 최신 고정 버전 간에도 실행합니다.

인터페이스의 안정성을 유지하기 위해 소유자는 다음을 새로 추가할 수 있습니다.

  • 인터페이스 끝에 메서드(또는 명시적으로 새 일련번호가 정의된 메서드) 추가
  • parcel 끝에 요소 추가(각 요소의 기본값 추가 필요)
  • 상수 값
  • Android R에 열거자

다른 작업은 허용되지 않으며 다른 누구도 인터페이스를 수정할 수 없습니다(그렇지 않으면 소유자가 변경한 사항에 충돌 위험이 있음).

런타임 시, 새 클라이언트는 이전 서버에서 새 메서드를 호출하려고 시도하면 자동으로 UNKNOWN_TRANSACTION을 받습니다. 이를 처리하는 전략은 버전 쿼리하기기본값 사용하기를 참조합니다. 새 필드가 parcelable에 추가되면 이전 클라이언트/서버에서 필드를 삭제합니다. 새 클라이언트/서버에서 이전 parcelable을 수신하면 새 필드의 기본값은 자동으로 채워집니다. 즉, parcelable의 모든 새 필드에 기본값을 지정해야 합니다. 마찬가지로, 이후에 더 추가될 수 있으므로 클라이언트/서버는 인식할 수 없는 상수 값과 열거자를 적절하게 거부하거나 무시해야 합니다.

모듈 이름 지정 규칙

R에서는 버전과 사용 설정된 백엔드의 조합마다 스터브 라이브러리 모듈이 자동으로 생성됩니다. 연결을 위해 특정 스터브 라이브러리 모듈을 참조하려면 aidl_interface 모듈 이름이 아닌 ifacename-version-backend로 된 스터브 라이브러리 모듈 이름을 사용합니다. 각각의 의미는 다음과 같습니다.

  • ifacename: aidl_interface 모듈의 이름입니다.
  • version은 다음 중 하나입니다.
    • Vversion-number: 고정된 버전
    • unstable: 아직 고정되지 않은 기타 버전
  • backend는 다음 중 하나입니다.
    • java: 자바 백엔드
    • cpp: C++ 백엔드
    • ndk 또는 ndk_platform: NDK 백엔드. 전자는 앱용이고 후자는 플랫폼용입니다.

최신 고정 버전에서는 자바 타겟을 제외한 버전 필드를 생략합니다. 즉, module-Vlatest-frozen-version-(cpp|ndk|ndk_platform)은 생성되지 않습니다.

이름이 foo이고 최신 버전이 2인 모듈이 있으며 NDK 및 C++를 모두 지원한다고 가정해 보겠습니다. 이 경우 AIDL은 다음과 같은 모듈을 생성합니다.

  • 버전 1 기반
    • foo-V1-(java|cpp|ndk|ndk_platform)
  • 버전 2 기반(최신 안정 버전)
    • foo-(java|cpp|ndk|ndk_platform)
    • foo-V2-java(콘텐츠는 foo-java와 동일)
  • ToT 버전 기반
    • foo-unstable-(java|cpp|ndk|ndk_platform)

대부분의 경우 출력 파일 이름은 모듈 이름과 동일합니다. 하지만 C++ 또는 NDK 모듈의 ToT 버전에서 출력 파일의 이름은 모듈 이름과 다릅니다.

예를 들어 foo-unstable-cpp의 출력 파일 이름은 아래에 표시된 것과 같이 foo-V3-cpp.so이고 foo-unstable-cpp.so가 아닙니다.

  • 버전 1 기반: foo-V1-(cpp|ndk|ndk_platform).so
  • 버전 2 기반: foo-V2-(cpp|ndk|ndk_platform).so
  • ToT 버전 기반: foo-V3-(cpp|ndk|ndk_platform).so

새 메타 인터페이스 메서드

Android 10은 안정적 AIDL을 위해 여러 메타 인터페이스 메서드를 추가합니다.

원격 객체의 인터페이스 버전 쿼리

클라이언트는 원격 객체가 구현하고 있는 인터페이스의 버전을 쿼리하고 반환된 버전을 클라이언트가 사용 중인 인터페이스 버전과 비교할 수 있습니다.

cpp 백엔드 예:

sp<IFoo> foo = ... // the remote object
    int32_t my_ver = IFoo::VERSION;
    int32_t remote_ver = foo->getInterfaceVersion();
    if (remote_ver < my_ver) {
      // the remote side is using an older interface
    }
    

ndkndk_platform 백엔드 예:

IFoo* foo = ... // the remote object
    int32_t my_ver = IFoo::version;
    int32_t remote_ver = 0;
    if (foo->getInterfaceVersion(&remote_ver).isOk() && remote_ver < my_ver) {
      // the remote side is using an older interface
    }
    

java 백엔드 예:

IFoo foo = ... // the remote object
    int myVer = IFoo.VERSION;
    int remoteVer = foo.getInterfaceVersion();
    if (remoteVer < myVer) {
      // the remote side is using an older interface
    }
    

자바 언어의 경우 원격 측은 getInterfaceVersion()을 다음과 같이 구현해야 합니다.

class MyFoo extends IFoo.Stub {
        @Override
        public final int getInterfaceVersion() { return IFoo.VERSION; }
    }
    

이는 생성된 클래스(IFoo, IFoo.Stub 등)가 클라이언트와 서버 간에 공유되기 때문입니다(예: 클래스가 부팅 클래스 경로에 있을 수 있음). 클래스가 공유될 때 서버는 이전 버전의 인터페이스로 빌드된 경우라도 클래스의 최신 버전과도 연동됩니다. 이 메타 인터페이스는 공유된 클래스에 구현될 경우 항상 최신 버전을 반환합니다. 그러나 위와 같이 메서드를 구현하면 (IFoo.VERSION은 참조 시 인라인된 static final int이므로) 인터페이스의 버전 번호가 서버의 코드에 삽입되고, 이에 따라 메서드는 서버가 빌드될 때 사용된 정확한 버전을 반환할 수 있습니다.

이전 인터페이스 처리

클라이언트가 최신 버전의 AIDL 인터페이스로 업데이트되었지만 서버는 이전 AIDL 인터페이스를 사용하고 있을 수 있습니다. 이런 경우 이전 인터페이스의 메서드를 호출하면 UNKNOWN_TRANSACTION이 반환됩니다.

안정적 AIDL을 사용하면 클라이언트는 더 큰 통제권을 갖습니다. 클라이언트 측에서 AIDL 인터페이스의 기본 구현을 설정할 수 있습니다. 기본 구현 메서드는 (메서드가 이전 버전의 인터페이스로 빌드되었으므로) 메서드가 원격 측에서 구현되지 않은 경우에만 호출됩니다. 기본값은 전역으로 설정되므로 잠재적으로 공유된 컨텍스트에서 사용해서는 안 됩니다.

C++ 예:

class MyDefault : public IFooDefault {
      Status anAddedMethod(...) {
       // do something default
      }
    };

    // once per an interface in a process
    IFoo::setDefaultImpl(std::unique_ptr<IFoo>(MyDefault));

    foo->anAddedMethod(...); // MyDefault::anAddedMethod() will be called if the
                             // remote side is not implementing it
    

자바 예시:

IFoo.Stub.setDefaultImpl(new IFoo.Default() {
        @Override
        public xxx anAddedMethod(...)  throws RemoteException {
            // do something default
        }
    }); // once per an interface in a process

    foo.anAddedMethod(...);
    

AIDL 인터페이스에 모든 메서드의 기본 구현을 제공할 필요는 없습니다. (메서드가 AIDL 인터페이스 설명에 있을 때 원격이 빌드될 것이 확실하므로) 원격 측에서 구현되도록 보장되는 메서드는 기본 impl 클래스에서 재정의할 필요가 없습니다.

기존 AIDL을 구조화된/안정적 AIDL로 변환

기존 AIDL 인터페이스와 이를 사용하는 코드가 있는 경우 다음 단계에 따라 인터페이스를 안정적 AIDL 인터페이스로 변환합니다.

  1. 인터페이스의 모든 종속 항목을 확인합니다. 인터페이스가 사용하는 각 패키지와 관련하여 해당 패키지가 안정적 AIDL로 정의되었는지 확인합니다. 정의되지 않은 경우 패키지를 변환해야 합니다.

  2. 인터페이스의 모든 parcelable을 안정적 parcelable로 변환합니다(인터페이스 파일 자체는 변경되지 않은 상태로 둘 수 있음). AIDL 파일에서 구조를 직접 표현하여 이를 처리합니다. 이러한 새 유형을 사용하려면 관리 클래스를 다시 작성해야 합니다. 아래에서 aidl_interface 패키지를 생성하기 전에 이 작업을 할 수 있습니다.

  3. (위에서 설명한 대로) 모듈 이름, 종속 항목 및 필요한 기타 정보가 포함된 aidl_interface 패키지를 생성합니다. 구조화하는 것뿐만 아니라 안정화하려면 버전이 지정되어야 합니다. 자세한 내용은 버전 관리 인터페이스를 참조하세요.