AIDL API 指南

本文列出的最佳做法可做為指南,協助您有效開發 AIDL 介面,並注意介面的彈性,特別是使用 AIDL 定義穩定且回溯相容的 API 時。

當應用程式需要在背景程序中相互介接,或需要與系統介接時,可以使用 AIDL 定義 API。

穩定版 AIDL (含 @VintfStability) 用於 HAL 介面,可讓用戶端和伺服器獨立更新。這需要回溯相容性和結構化資料。

如要進一步瞭解如何使用 AIDL 在應用程式中開發程式設計介面,請參閱「Android 介面定義語言 (AIDL)」。如需 AIDL 實務範例,請參閱「適用於 HAL 的 AIDL」和「穩定版 AIDL」。

版本管理

AIDL API 的每個回溯相容快照都會對應至一個版本。 如要建立快照,請執行 m <module-name>-freeze-api。每當發布 API 的用戶端或伺服器時 (例如在 Mainline 列車中),您都需要擷取快照並建立新版本。如果是系統對供應商的 API,應在每年修訂平台時進行這項作業。

介面凍結 (儲存在版本化 aidl_api 目錄中) 後,就絕不能修改。您只能編輯 current 目錄。您可以安全地在介面結尾新增方法、在可封送物件結尾新增欄位、在列舉結尾新增列舉值,以及在聯集結尾新增成員。

在舊版伺服器上呼叫新方法的用戶端會收到 UNKNOWN_TRANSACTION 錯誤,用戶端應妥善處理這項錯誤。

如要瞭解詳情和允許的變更類型,請參閱介面版本管理

建立依附元件

Android 模組無法依附 aidl_interface 中產生的多個不同版本程式庫。程式庫的不同版本會在相同命名空間中定義相同型別。Android 的 aidl 建構系統會找出這個問題,並針對每個以程式庫版本不符結尾的依附元件圖表擲回錯誤。

如果模組包含許多相依項目,而這些相依項目本身也有相依項目,更新通用介面的其中一個版本就會變得困難。

開發人員可以使用 aidl_interface_defaults 宣告共用介面在其他介面上的依附元件,這樣就不必獨立更新所有介面。

建議使用 *_defaults 模組 (例如 rust_defaultscc_defaultsjava_defaults) 整理所產生程式庫的依附元件。如果仍在使用舊版介面,通常會有介面latest版本的預設值,以及舊版的預設值。

開發人員可以利用 aidl_interface_defaults 宣告共用介面在其他介面上的依附元件,這樣就不必獨立更新所有介面。

API 設計指南

一般

1. 完整記錄

  • 記錄每個方法的語意、引數、內建例外狀況的使用情形、服務專屬例外狀況和回傳值。
  • 記錄每個介面的語意。
  • 記錄列舉和常數的語意。
  • 記錄實作者可能不清楚的內容。
  • 請視情況舉例。

2. 編織密度

型別使用大寫駝峰式大小寫,方法、欄位和引數則使用小寫駝峰式大小寫。舉例來說,可封送類型為 MyParcelable,引數則為 anArgument。如果是縮寫,請將縮寫視為一個字 (NFC -> Nfc)。

[-Wconst-name] 列舉值和常數應為 ENUM_VALUECONSTANT_NAME

3. 避免要求具備全球知識

API 不應假設開發人員具備整個程式碼集的全面知識,或特定領域的專業知識。處理特定網域的 ID (例如裝置名稱、ID 或控制碼) 時:

  • 如果介面兩端都需要知道這些 ID,請明確記錄 ID 來源和格式。
  • 或者,您也可以使用介面專屬的 ID (例如繫結器物件或自訂權杖),並讓其中一端管理與基礎值的對應關係。這樣可減少衝突,避免使用者需要瞭解自身領域以外的實作詳細資料。

4. 所有資料都經過結構化處理,且可向後相容

stringbyte[] 和共用記憶體等非結構化資料的內容必須採用穩定格式,或對介面的一端不透明。

舉例來說,用做結果錯誤訊息的字串引數可以接收並記錄以進行偵錯,但不得剖析和解讀,因為格式和內容可能不具回溯相容性。如果介面的另一端需要瞭解執行階段的錯誤,請使用列舉、常數或 ServiceSpecificException

同樣地,除非物件穩定且可向後相容,否則請勿將物件序列化為 byte[] 或共用記憶體。在某些情況下,您可以使用 @FixedSize 註解,在共用記憶體和快速訊息佇列中分享可打包物件和聯集。

介面

1. 命名

[-Winterface-name] 介面名稱應以 I 開頭,例如 IFoo

2. 避免使用以 ID 為基礎的「物件」建立大型介面

如果有多個與特定 API 相關的呼叫,建議使用子介面。這項做法有以下優點:

  • 更容易瞭解用戶端或伺服器程式碼
  • 簡化物件的生命週期
  • 利用不可偽造的繫結器。

不建議:單一大型介面,內含以 ID 為基礎的物件

interface IManager {
   int getFooId();
   void beginFoo(int id); // clients in other processes can guess an ID
   void opFoo(int id);
   void recycleFoo(int id); // ownership not handled by type
}

建議做法:個別介面

interface IManager {
    IFoo getFoo();
}

interface IFoo {
    void begin(); // clients in other processes can't guess a binder
    void op();
}

3. 請勿混用單向和雙向方法

[-Wmixed-oneway] 請勿將單向方法與非單向方法混用,因為這會讓用戶端和伺服器難以瞭解執行緒模型。具體來說,讀取特定介面的用戶端程式碼時,您需要為每個方法查詢該方法是否會封鎖。

4. 避免傳回狀態碼

方法應避免將狀態碼做為回傳值,因為所有 AIDL 方法都有隱含的狀態回傳碼。請參閱ServiceSpecificExceptionEX_SERVICE_SPECIFIC。按照慣例,這些值會在 AIDL 介面中定義為常數。如果錯誤需要自訂延遲或專屬錯誤資料,自訂回應物件就應代表錯誤。如需更多詳細資訊,請參閱「錯誤處理」。

5. 陣列做為輸出參數會被視為有害

[-Wout-array] 具有陣列輸出參數 (例如 void foo(out String[] ret)) 的方法通常不佳,因為輸出陣列大小必須由 Java 中的用戶端宣告及分配,因此伺服器無法選擇陣列輸出的大小。發生這種不理想的行為,是因為 Java 中陣列的運作方式 (無法重新配置)。建議改用 String[] foo() 等 API。

6. 避免使用 inout 參數

[-Winout-parameter] 這可能會讓用戶端感到困惑,因為即使是 in 參數,看起來也像 out 參數。

7. 避免使用 out 和 inout @nullable 非陣列參數

[-Wout-nullable] 由於 Java 後端不會處理 @nullable 註解,其他後端則會,因此 out/inout @nullable T 可能會導致後端行為不一致。舉例來說,非 Java 後端可將 out @nullable 參數設為空值 (在 C++ 中,設為 std::nullopt),但 Java 用戶端無法將其讀取為空值。

8. 使用不重複的要求和回應

將所有必要參數歸入單一輸入 parcelable。 為每個介面方法建立專屬要求和回應 Parcelable,而非傳遞基本型別 (例如使用 ComputeResponse compute(in ComputeRequest request),而非傳遞個別變數)。這樣一來,日後就能新增引數,不必變更函式簽章。如果預期日後可能會新增更多參數,或方法已超過四個參數,強烈建議採用這種模式。

如果方法不需要額外輸入或輸出內容,就不會受到這項建議影響。明確考量每個情況,並為日後的變更保持彈性,可減少已淘汰的方法,並降低回溯相容程式碼的複雜度。

如果方法不是使用這個模式建立,您可以建立新的方法 (包含要求和回應可封送物件),並淘汰舊方法,藉此切換至這個模式。例如:

void foo(int a, int b, int c); // original version, but deprecated in favor of the next version
void fooV2(in MyArg arg); // new version having int a, b, c, and d.

結構化可 Parcel 化物件

1. 使用時機

如果要傳送多種資料類型,請使用結構化可封送物件。

或者,當您只有單一資料型別,但預期日後需要擴充時,舉例來說,請勿使用 String username。使用可擴充的 Parcelable,例如:

parcelable User {
    String username;
}

這樣日後就能按照下列方式擴充:

parcelable User {
    String username;
    int id;
}

2. 明確提供預設值

[-Wexplicit-default, -Wenum-explicit-default] Provide explicit defaults for fields. 如果可封送物件新增欄位,舊版用戶端和伺服器會捨棄這些欄位,但新版用戶端和伺服器會自動填入預設值。

3. 針對供應商擴充功能使用 ParcelableHolder

如果您定義裝置實作人員需要擴充的 AOSP parcelable,請在物件中嵌入 ParcelableHolder 的執行個體。這可做為擴充點,不會造成合併衝突。這與附加介面擴充功能類似,但實作者可將自己的專屬 parcelable 連同現有 parcelable 一併納入,不必建立自己的介面和型別。

4. 資料結構

  • 使用陣列或可打包物件的 List 表示對應,因為 AIDL 本身不支援可安全地跨所有原生後端轉換的 Map 型別 (例如 FeatureToScoreEntry[])。
  • 針對重複欄位使用 parcelable 物件陣列,而非原始型別陣列,以免日後需要使用平行陣列。
  • 請使用強型別 parcelable 物件,而非透過 IPC 序列化字串或 JSON。
  • 請使用列舉而非布林值表示狀態,以便日後擴充。如果是位元遮罩,請使用 const int 而非 enum 型別,以免在某些後端進行繁瑣的轉換。

非結構化可 Parcelable 物件

1. 使用時機

Java 中的非結構化可 Parcel 化物件會使用 @JavaOnlyStableParcelable,NDK 後端則會使用 @NdkOnlyStableParcelable。通常是無法結構化的舊有可封送物件。

常數和列舉

1. 位元欄位應使用常數欄位

位元欄位應使用常數欄位 (例如介面中的 const int FOO = 3;)。

2. 列舉應為封閉集合。

列舉應為封閉集合。注意:只有介面擁有者可以新增列舉元素。如果供應商或原始設備製造商 (OEM) 需要擴充這些欄位,則必須採用替代機制。盡可能優先上傳供應商功能。不過,在某些情況下,系統可能會允許自訂供應商值通過 (但供應商應設有版本控制機制,或許是 AIDL 本身,這些值不應彼此衝突,也不應向第三方應用程式公開)。

3. 避免使用「NUM_ELEMENTS」等值

由於列舉是經過版本化的,因此應避免使用表示值數量的列舉值。在 C++ 中,這可以透過 enum_range<> 解決。如果是 Rust,請使用 enum_values()。Java 目前沒有解決方案。

不建議:使用編號值

@Backing(type="int")
enum FruitType {
    APPLE = 0,
    BANANA = 1,
    MANGO = 2,
    NUM_TYPES, // BAD
}

4. 避免使用多餘的前置字串和後置字串

[-Wredundant-name] 避免常數和列舉值中出現多餘或重複的前置字元和後置字元。

不建議:使用多餘的前置字串

enum MyStatus {
    STATUS_GOOD,
    STATUS_BAD // BAD
}

建議:直接命名列舉

enum MyStatus {
    GOOD,
    BAD
}

FileDescriptor

[-Wfile-descriptor] 強烈建議不要將 FileDescriptor 做為 AIDL 介面方法的引數或傳回值。特別是當 AIDL 是以 Java 實作時,如果未妥善處理,可能會導致檔案描述元洩漏。基本上,如果您接受 FileDescriptor,則不再使用時必須手動關閉。

如果是原生後端,由於 FileDescriptor 會對應至 unique_fd,因此您很安全。但無論使用哪種後端語言,都不建議使用 FileDescriptor,因為這樣會限制您日後變更後端語言的自由。

請改用可自動關閉的 ParcelFileDescriptor

變數單位

請務必在名稱中加入變數單位,確保單位定義明確,不需參考文件即可瞭解

範例

long duration; // Bad
long durationNsec; // Good
long durationNanos; // Also good

double energy; // Bad
double energyMilliJoules; // Good

int frequency; // Bad
int frequencyHz; // Good

時間戳記必須指出其參照

時間戳記 (事實上,所有單位都一樣!) 必須清楚標示單位和參考點。

範例

/**
 * Time since device boot in milliseconds
 */
long timestampMs;

/**
 * UTC time received from the NTP server in units of milliseconds
 * since January 1, 1970
 */
long utcTimeMs;

並行和非同步作業

使用非同步 (oneway) 介面處理長時間執行的作業,以免遭到封鎖。

如果服務不信任用戶端,則從用戶端收到的任何回呼都應為 oneway 介面。這樣可避免用戶端無限期封鎖服務。

建構非同步 API,包括轉送呼叫、輸入引數和回呼介面,以取得結果。如需引數建議,請參閱「使用不重複的要求和回應」。