Các phương pháp hay nhất được trình bày tại đây đóng vai trò là hướng dẫn để phát triển 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 API hoặc tương tác với các nền tảng 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 giao diện lập trình trong ứng dụng bằng AIDL, hãy xem phần Ngôn ngữ định nghĩa giao diện Android (AIDL). Để biết ví dụ về AIDL trong thực tế, hãy xem phần AIDL cho HAL và AIDL ổn định.
Lập phiên bản
Mỗi ảnh chụp nhanh có khả năng tương thích ngược của một API AIDL đều 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 đoàn tàu Mainline), bạn cần chụp ảnh nhanh và tạo một phiên bản mới. Đối với API hệ thống đến nhà cung cấp, việc này sẽ diễn ra trong quá trình 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 Giao diện tạo phiên bản.
Nguyên tắc thiết kế API
Giải pháp chung
1. Ghi lại mọi thứ
- Ghi lại mọi phương thức về ngữ nghĩa, đối số, cách sử dụng các ngoại lệ tích hợp, ngoại lệ cụ thể theo dịch vụ và giá trị trả về.
- Ghi lại ngữ nghĩa của mọi giao diện.
- Ghi lại ý nghĩa ngữ nghĩa của enum và hằng số.
- Ghi lại mọi thông tin 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 camel trên cho các loại và kiểu viết lạc đà dưới cho các phương thức, trường và đối số. Ví dụ: MyParcelable
cho loại có thể phân phối và anArgument
cho đố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] Giá trị enum và hằng số phải là ENUM_VALUE
và CONSTANT_NAME
Giao diện
1. Đặt tên
[-Winterface-name] Tên giao diện phải bắt đầu bằng I
như IFoo
.
2. Tránh giao diện lớn với "đối tượng" dựa trên mã nhận dạng
Ưu tiên giao diện con khi có nhiều lệnh gọi liên quan đến một API cụ thể. Điều này mang lại các lợi ích sau:
- Giúp mã máy khách hoặc mã máy chủ dễ hiểu hơn
- Làm cho vòng đời của đối tượng đơn giản hơn
- Tận dụng lợi thế của các yếu tố ràng buộc không thể sửa đổi được.
Không nên: Giao diện lớn, đơn lẻ với 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. Không kết hợp phương thức một chiều với phương thức hai chiều
[-Wmixed-oneway] Đừng kết hợp phương thức một chiều với phương thức không phải một chiều, vì điều này khiến việc hiểu mô hình tạo luồng trở nên phức tạp đối với máy khách và máy chủ. Cụ thể là khi đọc mã ứng dụng khách của một giao diện cụ thể, bạn cần tra cứu từng phương thức để xem 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 sử dụng 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ả về trạng thái ngầm ẩn. 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 mục Xử lý lỗi trong phần phụ trợ AIDL.
5. Mảng bị coi là tham số đầu ra có hại
[-Wout-array] Các phương thức có tham số đầu ra 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 do ứng dụng khai báo và phân bổ 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 các mảng trong Java (không thể phân bổ lại các mảng). Thay vào đó, hãy ưu tiên các API như String[] foo()
.
6. Tránh thông số đầu vào
[-Winout-parameter] Điều này có thể gây nhầm lẫn cho ứng dụng 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ố không phải mảng @nullable out và inout
[-Wout-null] Vì phần phụ trợ Java không xử lý chú giải @nullable
trong khi các phần phụ trợ khác vẫn xử lý, nên 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 là Java có thể đặt tham số @nullable
thành giá trị rỗng (trong C++, đặt tham số đó là std::nullopt
) nhưng ứng dụng Java không thể đọc tham số đó là rỗng.
Gói có cấu trúc
1. Trường hợp sử dụng
Sử dụng các gói 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 gói có thể mở rộng, như sau:
parcelable User {
String username;
}
Nhờ đó, trong tương lai, bạn có thể mở rộng phạm vi này như sau:
parcelable User {
String username;
int id;
}
2. Cung cấp 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.
Gói bưu kiện không có cấu trúc
1. Trường hợp sử dụng
Các gói không có cấu trúc có sẵn trong Java với @JavaOnlyStableParcelable
và trong phần phụ trợ NDK với @NdkOnlyStableParcelable
. Thông thường, đây là các gói có thể phân phối cũ và hiện có mà không thể được định cấu trúc.
Hằng số và enum
1. Trường bit phải sử dụng trường hằng số
Trường bit phải sử dụng các trường hằng số (ví dụ: const int FOO = 3;
trong giao diện).
2. Enums phải là tập hợp kín.
Enums phải là tập hợ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 nhà sản xuất thiết bị gố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 ngược dòng. 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 thông qua (mặc dù nhà cung cấp phải có cơ chế để tạo phiên bản này, có thể là chính AIDL, các giá trị này không được xung đột với nhau và 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 các giá trị cho biết có bao nhiêu giá trị hiện có. 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, chưa có giải pháp.
Không nên: 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 sử dụng tiền tố và hậu tố thừa
[-Wredundant-name] Tránh sử dụng tiền tố và hậu tố dư thừa hoặc lặp lại trong các hằng số và bộ đếm.
Không nên: Sử dụng tiền tố thừa
enum MyStatus {
STATUS_GOOD,
STATUS_BAD // BAD
}
Nên dùng: Trực tiếp đặt tên cho enum
enum MyStatus {
GOOD,
BAD
}
Mô tả tệp
[-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 trong Java, điều này có thể gây 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 FileDescriptor
, bạn cần đóng FileDescriptor
theo cách thủ công khi không còn sử dụng nữa.
Đối với phần phụ trợ gốc, bạn sẽ an toàn vì FileDescriptor
liên kết đến unique_fd
có thể tự động đóng. Tuy nhiên, bất kể ngôn ngữ phụ trợ bạn dùng là gì, bạn không nên sử dụng FileDescriptor
vì điều này sẽ hạn chế quyền tự do thay đổi ngôn ngữ phụ trợ trong tương lai.
Thay vào đó, hãy sử dụng ParcelFileDescriptor
có thể tự động đóng.
Đơn vị biến
Hãy nhớ đưa các đơn vị biến vào tên để các đơn vị của biến được xác định rõ 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 của chúng.
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;