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 ổn định, tương thích ngược.
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.
AIDL ổn định với @VintfStability được dùng cho các giao diện HAL và cho phép ứng dụng cũng như máy chủ được cập nhật độc lập. Điều này đòi hỏi khả năng tương thích ngược và dữ liệu có cấu trúc.
Để 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 HAL và AIDL ổ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.
Khi một giao diện bị đóng băng (được lưu trong thư mục aidl_api có phiên bản), bạn không bao giờ được sửa đổi giao diện đó. Bạn chỉ có thể chỉnh sửa thư mục current. Bạn có thể thêm các phương thức vào cuối giao diện, các trường vào cuối một đối tượng có thể chuyển đổi thành gói, các trình liệt kê vào một enum và các thành viên vào một union một cách an toàn.
Các ứng dụng gọi phương thức mới trên các máy chủ cũ sẽ nhận được lỗi UNKNOWN_TRANSACTION. Ứng dụng cần xử lý lỗi này một cách thích hợp.
Để 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.
Xây dựng các phần phụ thuộc
Các mô-đun Android không thể phụ thuộc vào nhiều phiên bản khác nhau của các thư viện đã tạo từ một aidl_interface. Các phiên bản khác nhau của thư viện xác định cùng một loại trong cùng một không gian tên. Hệ thống xây dựng aidl của Android xác định vấn đề này và gửi lỗi với từng biểu đồ phần phụ thuộc kết thúc bằng các phiên bản thư viện không khớp.
Điều này có thể gây khó khăn cho việc cập nhật một phiên bản của giao diện chung khi một mô-đun chứa nhiều phần phụ thuộc có phần phụ thuộc riêng.
Nhà phát triển có thể sử dụng aidl_interface_defaults để khai báo các phần phụ thuộc của giao diện dùng chung trên các giao diện khác để không cần phải cập nhật tất cả các giao diện một cách độc lập.
Bạn nên sử dụng các mô-đun *_defaults (chẳng hạn như rust_defaults, cc_defaults, java_defaults) để sắp xếp các phần phụ thuộc trên các thư viện đã tạo. Thông thường, bạn sẽ có một giá trị mặc định cho phiên bản latest của các giao diện cũng như giá trị mặc định cho các phiên bản trước nếu chúng vẫn được dùng.
Nhà phát triển có thể sử dụng aidl_interface_defaults để khai báo các phần phụ thuộc của giao diện dùng chung trên các giao diện khác để không cần phải cập nhật tất cả các giao diện một cách độc lập.
Nguyên tắc thiết kế API
Quy định 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 trường hợp ngoại lệ tích hợp, các trường hợp ngoại lệ cụ thể của 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 có liên quan.
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_VALUE và CONSTANT_NAME
3. Tránh yêu cầu kiến thức toàn cầu
API không nên giả định rằng nhà phát triển có kiến thức toàn cầu về toàn bộ cơ sở mã hoặc kiến thức chuyên môn cụ thể về miền. Khi xử lý các giá trị nhận dạng dành riêng cho miền (chẳng hạn như tên, mã hoặc tên người dùng của thiết bị):
- Hãy trình bày rõ ràng và ghi lại nguồn gốc cũng như định dạng của các giá trị nhận dạng này nếu cả hai phía của giao diện đều cần biết.
- Ngoài ra, hãy sử dụng các giá trị nhận dạng dành riêng cho giao diện (chẳng hạn như các đối tượng liên kết hoặc mã thông báo tuỳ chỉnh) và để một bên quản lý việc liên kết với các giá trị cơ bản. Điều này giúp giảm xung đột và tránh yêu cầu người dùng hiểu rõ các chi tiết triển khai bên ngoài khu vực của họ.
4. Tất cả dữ liệu đều có cấu trúc và tương thích ngược
Dữ liệu không có cấu trúc như string, byte[] và bộ nhớ dùng chung phải có định dạng ổn định cho nội dung của chúng hoặc không rõ ràng đối với một bên của giao diện.
Ví dụ: đối số chuỗi được dùng làm thông báo lỗi cho một kết quả có thể được nhận và ghi nhật ký để gỡ lỗi, nhưng không được phân tích cú pháp và diễn giải vì định dạng và nội dung có thể không tương thích ngược. Nếu phía còn lại của giao diện cần biết lỗi là gì tại thời gian chạy, hãy sử dụng một enum, hằng số hoặc ServiceSpecificException.
Tương tự, đừng chuyển đổi các đối tượng thành byte[] hoặc bộ nhớ dùng chung, trừ phi các đối tượng đó ổn định và tương thích ngược. Trong một số trường hợp, bạn có thể dùng chú thích @FixedSize để chia sẻ các đối tượng có thể đóng gói và các union trong bộ nhớ dùng chung và Hàng đợi tin nhắn nhanh.
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 đối tượng 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] Đừng kết hợp các phương thức một chiều với các phương thức không phải 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. Nếu cần có độ trễ tuỳ chỉnh hoặc dữ liệu lỗi riêng biệt cùng với lỗi, thì đó là thời điểm duy nhất đối tượng phản hồi tuỳ chỉnh sẽ biểu thị lỗi. Để biết thêm thông tin chi tiết, hãy xem phần Xử lý lỗi.
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ì kích thước mảng đầu ra phải được khai báo và phân bổ bởi máy khách 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 và out
[-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.
8. Sử dụng các yêu cầu và phản hồi riêng biệt
Nhóm tất cả các tham số cần thiết vào một đầu vào duy nhất parcelable.
Tạo các đối tượng có thể chuyển đổi tuần tự yêu cầu và phản hồi chuyên dụng cho mọi phương thức giao diện thay vì truyền các kiểu dữ liệu nguyên thuỷ (ví dụ: sử dụng ComputeResponse compute(in ComputeRequest request) thay vì truyền các biến riêng biệt). Điều này cho phép bạn thêm các đối số mới sau này mà không cần thay đổi chữ ký hàm. Bạn nên sử dụng mẫu này khi dự kiến sẽ có thêm các tham số trong tương lai hoặc nếu một phương thức đã có hơn 4 tham số.
Những phương thức không yêu cầu thêm dữ liệu đầu vào hoặc đầu ra sẽ không được hưởng lợi từ đề xuất này. Việc suy nghĩ rõ ràng về từng trường hợp và duy trì tính linh hoạt cho những thay đổi trong tương lai có thể giúp giảm số lượng phương thức không dùng nữa và giảm độ phức tạp cho mã tương thích ngược.
Nếu một phương thức không được tạo bằng mẫu này, bạn có thể chuyển sang mẫu này bằng cách tạo một phương thức mới có yêu cầu và phản hồi có thể đóng gói, đồng thời không dùng phương thức cũ nữa. Ví dụ:
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.
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 loại dữ liệu duy nhất nhưng dự kiến sẽ cần mở rộng loại 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. Khi các trường mới được thêm vào một đối tượng có thể chuyển đổi, các máy khách và máy chủ cũ sẽ loại bỏ các trường đó, nhưng các giá trị mặc định sẽ tự động điền cho các máy khách và máy chủ mới.
3. Sử dụng ParcelableHolder cho các tiện ích của nhà cung cấp
Nếu bạn xác định một parcelable AOSP mà nhà triển khai thiết bị cần mở rộng, hãy nhúng một thực thể của ParcelableHolder vào đối tượng của bạn. Thao tác này đóng vai trò là một điểm mở rộng mà không tạo ra xung đột hợp nhất. Điều này tương tự như các tiện ích mở rộng giao diện được đính kèm nhưng cho phép người triển khai đưa parcelable thuộc quyền sở hữu riêng của họ cùng với parcelable hiện có mà không cần tạo giao diện và các loại riêng.
4. Cấu trúc dữ liệu
- Sử dụng mảng hoặc
Listcủa các đối tượng có thể phân chia để biểu thị bản đồ, vì AIDL không hỗ trợ tự nhiên các loạiMapdịch an toàn trên tất cả các phần phụ trợ gốc (ví dụ:FeatureToScoreEntry[]). - Sử dụng mảng các đối tượng
parcelablecho các trường lặp lại thay vì mảng các kiểu dữ liệu nguyên thuỷ để tránh nhu cầu về các mảng song song trong tương lai. - Sử dụng các đối tượng
parcelableđược nhập mạnh thay vì các chuỗi được chuyển đổi tuần tự hoặc JSON qua IPC. - Sử dụng enum thay vì boolean cho các trạng thái để cho phép mở rộng trong tương lai. Đối với mặt nạ bit, hãy sử dụng các loại
const intthay vìenumđể tránh việc truyền dữ liệu rườm rà ở một số phần phụ trợ.
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 giá trị này, có thể là chính AIDL, họ không được phép xung đột với nhau và các giá trị này không được phép 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 sử dụng tiền tố và hậu tố trùng lặp hoặc lặp lại nhiều lần 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
Đảm bảo rằng các đơn vị biến đượ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;
Tính đồng thời và các thao tác không đồng bộ
Xử lý các thao tác chạy trong thời gian dài bằng giao diện không đồng bộ (oneway) để tránh chặn.
Nếu một dịch vụ không tin tưởng các ứng dụng của mình, thì mọi lệnh gọi lại mà dịch vụ đó nhận được từ các ứng dụng phải là giao diện oneway. Điều này ngăn các ứng dụng chặn dịch vụ vô thời hạn.
Cấu trúc các API không đồng bộ bao gồm một lệnh gọi chuyển tiếp, đối số đầu vào và một giao diện gọi lại để nhận kết quả. Hãy xem phần Sử dụng các yêu cầu và phản hồi duy nhất để biết các đề xuất về đối số.