Hướng dẫn về quy tắc lập trình AIDL

Các phương pháp hay nhất được trình bày ở đây đóng vai trò là hướng dẫn để phát triển các giao diện AIDL một cách hiệu quả và chú ý đến tính linh hoạt của giao diện, đặc biệt là khi AIDL được dùng để xác định một API hoặc tương tác với các bề mặt API.

Bạn có thể dùng AIDL để xác định một API khi các ứng dụng cần giao tiếp với nhau trong một quy trình ở chế độ nền hoặc cần giao tiếp với hệ thống. Để biết thêm thông tin về cách phát triển các giao diện lập trình trong ứng dụng bằng AIDL, hãy xem bài viết Ngôn ngữ định nghĩa giao diện Android (AIDL). Để biết ví dụ về AIDL trong thực tế, hãy xem AIDL cho HALAIDL ổn định.

Lập phiên bản

Mỗi ảnh chụp nhanh tương thích ngược của một API AIDL tương ứng với một phiên bản. Để chụp nhanh, hãy chạy m <module-name>-freeze-api. Bất cứ khi nào một ứng dụng hoặc máy chủ của API được phát hành (ví dụ: trong một bản phát hành Mainline), bạn cần chụp nhanh và tạo một phiên bản mới. Đối với các API từ hệ thống đến nhà cung cấp, điều này sẽ xảy ra với bản sửa đổi nền tảng hằng năm.

Để biết thêm thông tin chi tiết và thông tin về loại thay đổi được phép, hãy xem phần Tạo phiên bản cho các giao diện.

Nguyên tắc thiết kế API

Giải pháp chung

1. Lưu hồ sơ về mọi thứ

  • Ghi lại mọi phương thức cho ngữ nghĩa, đối số, cách sử dụng các ngoại lệ tích hợp, ngoại lệ dành riêng cho dịch vụ và giá trị trả về.
  • Ghi lại mọi giao diện cho ngữ nghĩa của giao diện đó.
  • Ghi lại ý nghĩa ngữ nghĩa của các enum và hằng số.
  • Ghi lại bất cứ điều gì có thể không rõ ràng đối với người triển khai.
  • Cung cấp ví dụ nếu phù hợp.

2. Vỏ

Sử dụng kiểu viết hoa chữ cái đầu cho các loại và kiểu viết hoa chữ cái đầu (trừ chữ cái đầu tiên) cho các phương thức, trường và đối số. Ví dụ: MyParcelable cho loại có thể phân chia và anArgument cho một đối số. Đối với từ viết tắt, hãy coi từ viết tắt là một từ (NFC -> Nfc).

[-Wconst-name] Các giá trị và hằng số của enum phải là ENUM_VALUECONSTANT_NAME

Giao diện

1. Đặt tên

[-Winterface-name] Tên giao diện phải bắt đầu bằng I, chẳng hạn như IFoo.

2. Tránh giao diện lớn có "đối tượng" dựa trên mã nhận dạng

Ưu tiên các giao diện phụ khi có nhiều lệnh gọi liên quan đến một API cụ thể. Điều này mang lại những lợi ích sau:

  • Giúp mã máy khách hoặc máy chủ dễ hiểu hơn
  • Giúp vòng đời của các đối tượng trở nên đơn giản hơn
  • Tận dụng lợi thế của các trình liên kết không thể giả mạo.

Không nên dùng: Một giao diện lớn duy nhất có các đối tượng dựa trên mã nhận dạng

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
}

Nên dùng: Giao diện riêng lẻ

interface IManager {
    IFoo getFoo();
}

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

3. Đừng kết hợp các phương thức một chiều với hai chiều

[-Wmixed-oneway] Không kết hợp phương thức một chiều với phương thức không một chiều, vì điều này khiến việc tìm hiểu mô hình phân luồng trở nên phức tạp đối với máy khách và máy chủ. Cụ thể, khi đọc mã ứng dụng của một giao diện cụ thể, bạn cần tra cứu từng phương thức để biết phương thức đó có chặn hay không.

4. Tránh trả về mã trạng thái

Các phương thức nên tránh mã trạng thái làm giá trị trả về, vì tất cả các phương thức AIDL đều có mã trạng thái trả về ngầm. Hãy xem ServiceSpecificException hoặc EX_SERVICE_SPECIFIC. Theo quy ước, các giá trị này được xác định là hằng số trong giao diện AIDL. Bạn có thể xem thêm thông tin chi tiết trong phần Xử lý lỗi của các chương trình phụ trợ AIDL.

5. Mảng dưới dạng tham số đầu ra được coi là có hại

[-Wout-array] Các phương thức có tham số đầu ra là mảng, chẳng hạn như void foo(out String[] ret) thường không tốt vì máy khách phải khai báo và phân bổ kích thước mảng đầu ra trong Java, do đó, máy chủ không thể chọn kích thước của đầu ra mảng. Hành vi không mong muốn này xảy ra do cách hoạt động của mảng trong Java (không thể phân bổ lại). Thay vào đó, hãy ưu tiên các API như String[] foo().

6. Tránh dùng thông số đầu vào/đầu ra

[-Winout-parameter] Điều này có thể khiến các ứng dụng gặp khó khăn vì ngay cả các tham số in cũng trông giống như các tham số out.

7. Tránh các tham số @nullable không phải mảng out và inout

[-Wout-nullable] Vì phần phụ trợ Java không xử lý chú thích @nullable trong khi các phần phụ trợ khác xử lý, out/inout @nullable T có thể dẫn đến hành vi không nhất quán trên các phần phụ trợ. Ví dụ: các phần phụ trợ không phải Java có thể đặt một tham số @nullable ngoài thành giá trị rỗng (trong C++, đặt tham số này thành std::nullopt) nhưng ứng dụng Java không thể đọc tham số này dưới dạng giá trị rỗng.

Các đối tượng có thể chuyển đổi tuần tự có cấu trúc

1. Trường hợp sử dụng

Sử dụng các đối tượng có thể chuyển đổi tuần tự có cấu trúc khi bạn có nhiều loại dữ liệu cần gửi.

Hoặc khi bạn có một kiểu dữ liệu duy nhất nhưng dự kiến sẽ cần mở rộng kiểu dữ liệu đó trong tương lai. Ví dụ: không sử dụng String username. Sử dụng một đối tượng có thể mở rộng, chẳng hạn như đối tượng sau:

parcelable User {
    String username;
}

Nhờ đó, trong tương lai, bạn có thể mở rộng phạm vi của quy tắc này như sau:

parcelable User {
    String username;
    int id;
}

2. Cung cấp các giá trị mặc định một cách rõ ràng

[-Wexplicit-default, -Wenum-explicit-default] Cung cấp giá trị mặc định rõ ràng cho các trường.

Các đối tượng có thể chuyển đổi không có cấu trúc

1. Trường hợp sử dụng

Các đối tượng có thể chuyển đổi tuần tự không có cấu trúc có trong Java với @JavaOnlyStableParcelable và trong phần phụ trợ NDK với @NdkOnlyStableParcelable. Thông thường, đây là những đối tượng có thể chuyển đổi cũ và hiện có mà không thể có cấu trúc.

Hằng số và enum

1. Bitfield nên sử dụng các trường hằng số

Bitfield nên sử dụng các trường hằng số (ví dụ: const int FOO = 3; trong một giao diện).

2. Enum phải là các tập hợp khép kín.

Enum phải là các tập hợp khép kín. Lưu ý: chỉ chủ sở hữu giao diện mới có thể thêm các phần tử enum. Nếu nhà cung cấp hoặc OEM cần mở rộng các trường này, thì cần có một cơ chế thay thế. Bất cứ khi nào có thể, bạn nên ưu tiên chức năng của nhà cung cấp thượng nguồn. Tuy nhiên, trong một số trường hợp, các giá trị tuỳ chỉnh của nhà cung cấp có thể được cho phép (mặc dù các nhà cung cấp phải có một cơ chế để tạo phiên bản cho việc này, có thể là chính AIDL, họ không được xung đột với nhau và các giá trị này không được hiển thị cho các ứng dụng bên thứ ba).

3. Tránh các giá trị như "NUM_ELEMENTS"

Vì enum được tạo phiên bản, nên bạn nên tránh những giá trị cho biết có bao nhiêu giá trị. Trong C++, bạn có thể giải quyết vấn đề này bằng enum_range<>. Đối với Rust, hãy sử dụng enum_values(). Trong Java, hiện chưa có giải pháp.

Không nên dùng: Sử dụng giá trị được đánh số

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

4. Tránh tiền tố và hậu tố dư thừa

[-Wredundant-name] Tránh tiền tố và hậu tố dư thừa hoặc lặp lại trong các hằng số và trình liệt kê.

Không nên: Sử dụng tiền tố dư thừa

enum MyStatus {
    STATUS_GOOD,
    STATUS_BAD // BAD
}

Nên dùng: Đặt tên trực tiếp cho enum

enum MyStatus {
    GOOD,
    BAD
}

FileDescriptor

[-Wfile-descriptor] Bạn không nên sử dụng FileDescriptor làm đối số hoặc giá trị trả về của phương thức giao diện AIDL. Đặc biệt, khi AIDL được triển khai bằng Java, điều này có thể gây ra tình trạng rò rỉ chỉ số mô tả tệp, trừ phi được xử lý cẩn thận. Về cơ bản, nếu chấp nhận một FileDescriptor, bạn cần đóng nó theo cách thủ công khi không còn sử dụng nữa.

Đối với các phần phụ trợ gốc, bạn sẽ an toàn vì FileDescriptor ánh xạ đến unique_fd có thể tự động đóng. Nhưng bất kể bạn sử dụng ngôn ngữ phụ trợ nào, tốt nhất là bạn KHÔNG nên sử dụng FileDescriptor vì điều này sẽ hạn chế khả năng thay đổi ngôn ngữ phụ trợ của bạn trong tương lai.

Thay vào đó, hãy sử dụng ParcelFileDescriptor (có thể tự động đóng).

Đơn vị biến đổi

Đảm bảo rằng các đơn vị biến số được đưa vào tên để các đơn vị này được xác định rõ ràng và dễ hiểu mà không cần tham khảo tài liệu

Ví dụ

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

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

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

Dấu thời gian phải cho biết thông tin tham chiếu

Dấu thời gian (thực tế là tất cả các đơn vị!) phải cho biết rõ đơn vị và điểm tham chiếu.

Ví dụ

/**
 * 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;