Các lệnh gọi API Android thường có độ trễ và mức tính toán đáng kể cho mỗi lệnh gọi. Do đó, việc lưu vào bộ nhớ đệm phía máy khách là một yếu tố quan trọng cần cân nhắc khi thiết kế các API hữu ích, chính xác và hiệu quả.
Động lực
Các API được cung cấp cho nhà phát triển ứng dụng trong Android SDK thường được triển khai dưới dạng mã ứng dụng trong Android Framework. Mã này thực hiện lệnh gọi Binder IPC đến một dịch vụ hệ thống trong một quy trình nền tảng. Nhiệm vụ của dịch vụ này là thực hiện một số phép tính và trả về kết quả cho ứng dụng. Độ trễ của thao tác này thường bị ảnh hưởng bởi 3 yếu tố:
- Chi phí IPC: một lệnh gọi IPC cơ bản thường có độ trễ gấp 10.000 lần so với một lệnh gọi phương thức cơ bản trong quy trình.
- Tranh chấp phía máy chủ: công việc được thực hiện trong dịch vụ hệ thống để phản hồi yêu cầu của ứng dụng có thể không bắt đầu ngay lập tức, chẳng hạn như nếu một luồng máy chủ đang bận xử lý các yêu cầu khác đến trước đó.
- Tính toán phía máy chủ: bản thân công việc xử lý yêu cầu trên máy chủ có thể đòi hỏi nhiều công sức.
Bạn có thể loại bỏ cả 3 yếu tố gây ra độ trễ này bằng cách triển khai bộ nhớ đệm ở phía máy khách, miễn là bộ nhớ đệm đó:
- Chính xác: bộ nhớ đệm phía máy khách không bao giờ trả về kết quả khác với kết quả mà máy chủ sẽ trả về.
- Hiệu quả: các yêu cầu của máy khách thường được phân phát từ bộ nhớ đệm, ví dụ: bộ nhớ đệm có tỷ lệ trùng khớp cao.
- Hiệu quả: bộ nhớ đệm phía máy khách sử dụng hiệu quả các tài nguyên phía máy khách, chẳng hạn như bằng cách biểu thị dữ liệu được lưu vào bộ nhớ đệm theo cách nhỏ gọn và bằng cách không lưu trữ quá nhiều kết quả được lưu vào bộ nhớ đệm hoặc dữ liệu cũ trong bộ nhớ của máy khách.
Cân nhắc việc lưu kết quả của máy chủ vào bộ nhớ đệm trong máy khách
Nếu khách hàng thường thực hiện chính xác cùng một yêu cầu nhiều lần và giá trị được trả về không thay đổi theo thời gian, thì bạn nên triển khai bộ nhớ đệm trong thư viện ứng dụng được khoá theo các tham số yêu cầu.
Hãy cân nhắc sử dụng IpcDataCache trong quá trình triển khai:
public class BirthdayManager {
private final IpcDataCache.QueryHandler<User, Birthday> mBirthdayQuery =
new IpcDataCache.QueryHandler<User, Birthday>() {
@Override
public Birthday apply(User user) {
return mService.getBirthday(user);
}
};
private static final int BDAY_CACHE_MAX = 8; // Maximum birthdays to cache
private static final String BDAY_API = "getUserBirthday";
private final IpcDataCache<User, Birthday> mCache
new IpcDataCache<User, Birthday>(
BDAY_CACHE_MAX, MODULE_SYSTEM, BDAY_API, BDAY_API, mBirthdayQuery);
/** @hide **/
@VisibleForTesting
public static void clearCache() {
IpcDataCache.invalidateCache(MODULE_SYSTEM, BDAY_API);
}
public Birthday getBirthday(User user) {
return mCache.query(user);
}
}
Để xem ví dụ đầy đủ, hãy xem android.app.admin.DevicePolicyManager.
IpcDataCache có sẵn cho tất cả mã hệ thống, bao gồm cả các mô-đun chính.
Ngoài ra, còn có PropertyInvalidatedCache gần giống nhưng chỉ hiển thị cho khung. Ưu tiên IpcDataCache khi có thể.
Vô hiệu hoá bộ nhớ đệm khi có thay đổi phía máy chủ
Nếu giá trị do máy chủ trả về có thể thay đổi theo thời gian, hãy triển khai một lệnh gọi lại để theo dõi các thay đổi và đăng ký một lệnh gọi lại để bạn có thể làm mất hiệu lực bộ nhớ đệm phía máy khách cho phù hợp.
Huỷ các bộ nhớ đệm giữa các trường hợp kiểm thử đơn vị
Trong một bộ kiểm thử đơn vị, bạn có thể kiểm thử mã ứng dụng với một đối tượng kiểm thử thay vì máy chủ thực. Nếu có, hãy nhớ xoá mọi bộ nhớ đệm phía máy khách giữa các trường hợp kiểm thử. Điều này nhằm giữ cho các trường hợp kiểm thử khép kín lẫn nhau và ngăn một trường hợp kiểm thử can thiệp vào một trường hợp kiểm thử khác.
@RunWith(AndroidJUnit4.class)
public class BirthdayManagerTest {
@Before
public void setUp() {
BirthdayManager.clearCache();
}
@After
public void tearDown() {
BirthdayManager.clearCache();
}
...
}
Khi viết các kiểm thử CTS thực hiện một ứng dụng API sử dụng bộ nhớ đệm nội bộ, bộ nhớ đệm là một chi tiết triển khai không được cung cấp cho tác giả API. Do đó, các kiểm thử CTS không yêu cầu bất kỳ kiến thức đặc biệt nào về bộ nhớ đệm được dùng trong mã ứng dụng.
Nghiên cứu các lượt truy cập và lượt bỏ lỡ bộ nhớ đệm
IpcDataCache và PropertyInvalidatedCache có thể in số liệu thống kê trực tiếp:
adb shell dumpsys cacheinfo
...
Cache Name: cache_key.is_compat_change_enabled
Property: cache_key.is_compat_change_enabled
Hits: 1301458, Misses: 21387, Skips: 0, Clears: 39
Skip-corked: 0, Skip-unset: 0, Skip-bypass: 0, Skip-other: 0
Nonce: 0x856e911694198091, Invalidates: 72, CorkedInvalidates: 0
Current Size: 1254, Max Size: 2048, HW Mark: 2049, Overflows: 310
Enabled: true
...
Trường
Lượt truy cập:
- Định nghĩa: Số lần một phần dữ liệu được yêu cầu được tìm thấy thành công trong bộ nhớ đệm.
- Ý nghĩa: Cho biết khả năng truy xuất dữ liệu nhanh chóng và hiệu quả, giảm tình trạng truy xuất dữ liệu không cần thiết.
- Số lượng càng cao thì thường càng tốt.
Xoá:
- Định nghĩa: Số lần bộ nhớ đệm bị xoá do quá trình xác thực không hợp lệ.
- Lý do xoá:
- Vô hiệu hoá: Dữ liệu đã lỗi thời trên máy chủ.
- Quản lý dung lượng: Giải phóng dung lượng cho dữ liệu mới khi bộ nhớ đệm đầy.
- Số lượng lớn có thể cho thấy dữ liệu thường xuyên thay đổi và có khả năng không hiệu quả.
Bỏ lỡ:
- Định nghĩa: Số lần bộ nhớ đệm không cung cấp được dữ liệu được yêu cầu.
- Nguyên nhân:
- Bộ nhớ đệm không hiệu quả: Bộ nhớ đệm quá nhỏ hoặc không lưu trữ đúng dữ liệu.
- Dữ liệu thường xuyên thay đổi.
- Yêu cầu lần đầu.
- Số lượng lớn cho thấy các vấn đề tiềm ẩn về việc lưu vào bộ nhớ đệm.
Lượt bỏ qua:
- Định nghĩa: Các trường hợp không sử dụng bộ nhớ đệm, mặc dù có thể sử dụng.
- Lý do bỏ qua:
- Corking: Cụ thể là đối với các bản cập nhật Trình quản lý gói Android, cố ý tắt tính năng lưu vào bộ nhớ đệm do có số lượng lớn các lệnh gọi trong quá trình khởi động.
- Chưa đặt: Bộ nhớ đệm tồn tại nhưng chưa được khởi chạy. Giá trị chỉ dùng một lần chưa được đặt, tức là bộ nhớ đệm chưa bao giờ bị vô hiệu hoá.
- Bypass: Quyết định có chủ ý để bỏ qua bộ nhớ đệm.
- Số lượng lớn cho thấy có thể có những điểm không hiệu quả trong việc sử dụng bộ nhớ đệm.
Vô hiệu hoá:
- Định nghĩa: Quá trình đánh dấu dữ liệu được lưu vào bộ nhớ đệm là lỗi thời.
- Mức độ quan trọng: Cung cấp tín hiệu cho biết hệ thống hoạt động với dữ liệu mới nhất, ngăn chặn lỗi và sự không nhất quán.
- Thường do máy chủ sở hữu dữ liệu kích hoạt.
Kích thước hiện tại:
- Định nghĩa: Số lượng phần tử hiện tại trong bộ nhớ đệm.
- Mức độ quan trọng: Cho biết mức sử dụng tài nguyên của bộ nhớ đệm và tác động tiềm ẩn đến hiệu suất hệ thống.
- Các giá trị cao hơn thường có nghĩa là bộ nhớ đệm sử dụng nhiều bộ nhớ hơn.
Kích thước tối đa:
- Định nghĩa: Lượng không gian tối đa được phân bổ cho bộ nhớ đệm.
- Mức độ quan trọng: Xác định dung lượng của bộ nhớ đệm và khả năng lưu trữ dữ liệu của bộ nhớ đệm đó.
- Việc đặt kích thước tối đa phù hợp sẽ giúp cân bằng hiệu quả của bộ nhớ đệm với mức sử dụng bộ nhớ. Khi đạt đến kích thước tối đa, một phần tử mới sẽ được thêm vào bằng cách loại bỏ phần tử được sử dụng gần đây nhất, điều này có thể cho thấy sự không hiệu quả.
High Water Mark (Đỉnh điểm):
- Định nghĩa: Kích thước tối đa mà bộ nhớ đệm đạt được kể từ khi được tạo.
- Ý nghĩa: Cung cấp thông tin chi tiết về mức sử dụng bộ nhớ đệm cao nhất và áp lực bộ nhớ tiềm ẩn.
- Việc theo dõi mức sử dụng bộ nhớ cao nhất có thể giúp xác định các điểm tắc nghẽn tiềm ẩn hoặc các khu vực cần tối ưu hoá.
Tràn:
- Định nghĩa: Số lần bộ nhớ đệm vượt quá kích thước tối đa và phải loại bỏ dữ liệu để tạo chỗ trống cho các mục mới.
- Mức độ quan trọng: Cho biết áp lực bộ nhớ đệm và hiệu suất có thể bị giảm do việc loại bỏ dữ liệu.
- Số lượng tràn cao cho thấy bạn có thể cần điều chỉnh kích thước bộ nhớ đệm hoặc đánh giá lại chiến lược lưu vào bộ nhớ đệm.
Bạn cũng có thể tìm thấy các số liệu thống kê tương tự trong báo cáo lỗi.
Điều chỉnh kích thước của bộ nhớ đệm
Bộ nhớ đệm có kích thước tối đa. Khi vượt quá kích thước bộ nhớ đệm tối đa, các mục sẽ bị loại bỏ theo thứ tự LRU.
- Việc lưu vào bộ nhớ đệm quá ít mục có thể ảnh hưởng tiêu cực đến tỷ lệ truy cập bộ nhớ đệm.
- Việc lưu vào bộ nhớ đệm quá nhiều mục sẽ làm tăng mức sử dụng bộ nhớ của bộ nhớ đệm.
Tìm ra sự cân bằng phù hợp cho trường hợp sử dụng của bạn.
Loại bỏ các lệnh gọi không cần thiết của ứng dụng
Ứng dụng có thể thực hiện cùng một truy vấn đến máy chủ nhiều lần trong một khoảng thời gian ngắn:
public void executeAll(List<Operation> operations) throws SecurityException {
for (Operation op : operations) {
for (Permission permission : op.requiredPermissions()) {
if (!permissionChecker.checkPermission(permission, ...)) {
throw new SecurityException("Missing permission " + permission);
}
}
op.execute();
}
}
Hãy cân nhắc việc sử dụng lại kết quả từ các lệnh gọi trước:
public void executeAll(List<Operation> operations) throws SecurityException {
Set<Permission> permissionsChecked = new HashSet<>();
for (Operation op : operations) {
for (Permission permission : op.requiredPermissions()) {
if (!permissionsChecked.add(permission)) {
if (!permissionChecker.checkPermission(permission, ...)) {
throw new SecurityException(
"Missing permission " + permission);
}
}
}
op.execute();
}
}
Cân nhắc việc ghi nhớ phía máy khách đối với các phản hồi gần đây của máy chủ
Các ứng dụng khách có thể truy vấn API với tốc độ nhanh hơn tốc độ mà máy chủ của API có thể tạo ra các phản hồi mới có ý nghĩa. Trong trường hợp này, một phương pháp hiệu quả là ghi nhớ phản hồi cuối cùng của máy chủ ở phía máy khách cùng với dấu thời gian, đồng thời trả về kết quả đã ghi nhớ mà không cần truy vấn máy chủ nếu kết quả đã ghi nhớ đủ gần đây. Tác giả của ứng dụng API có thể xác định thời lượng ghi nhớ.
Ví dụ: một ứng dụng có thể hiển thị cho người dùng số liệu thống kê về lưu lượng truy cập mạng bằng cách truy vấn số liệu thống kê trong mọi khung hình được vẽ:
@UiThread
private void setStats() {
mobileRxBytesTextView.setText(
Long.toString(TrafficStats.getMobileRxBytes()));
mobileRxPacketsTextView.setText(
Long.toString(TrafficStats.getMobileRxPackages()));
mobileTxBytesTextView.setText(
Long.toString(TrafficStats.getMobileTxBytes()));
mobileTxPacketsTextView.setText(
Long.toString(TrafficStats.getMobileTxPackages()));
}
Ứng dụng có thể vẽ các khung hình ở tốc độ 60 Hz. Tuy nhiên, theo giả thuyết, mã ứng dụng trong TrafficStats có thể chọn truy vấn máy chủ để lấy số liệu thống kê tối đa một lần mỗi giây và nếu được truy vấn trong vòng một giây kể từ lần truy vấn trước, hãy trả về giá trị được thấy gần đây nhất.
Điều này được phép vì tài liệu API không cung cấp bất kỳ hợp đồng nào liên quan đến tính mới của kết quả được trả về.
participant App code as app
participant Client library as clib
participant Server as server
app->clib: request @ T=100ms
clib->server: request
server->clib: response 1
clib->app: response 1
app->clib: request @ T=200ms
clib->app: response 1
app->clib: request @ T=300ms
clib->app: response 1
app->clib: request @ T=2000ms
clib->server: request
server->clib: response 2
clib->app: response 2
Hãy cân nhắc việc tạo mã phía máy khách thay vì truy vấn máy chủ
Nếu máy chủ có thể biết kết quả truy vấn tại thời gian xây dựng, hãy cân nhắc xem ứng dụng cũng có thể biết kết quả đó tại thời gian xây dựng hay không, đồng thời cân nhắc xem API có thể được triển khai hoàn toàn ở phía ứng dụng hay không.
Hãy xem xét đoạn mã ứng dụng sau đây để kiểm tra xem thiết bị có phải là đồng hồ hay không (tức là thiết bị đang chạy Wear OS):
public boolean isWatch(Context ctx) {
PackageManager pm = ctx.getPackageManager();
return pm.hasSystemFeature(PackageManager.FEATURE_WATCH);
}
Thuộc tính này của thiết bị được biết tại thời điểm tạo, cụ thể là tại thời điểm Framework được tạo cho hình ảnh khởi động của thiết bị này. Mã phía máy khách cho hasSystemFeature có thể trả về ngay một kết quả đã biết, thay vì truy vấn dịch vụ hệ thống PackageManager từ xa.
Loại bỏ các lệnh gọi lại trùng lặp của máy chủ trong ứng dụng
Cuối cùng, ứng dụng API có thể đăng ký lệnh gọi lại với máy chủ API để nhận thông báo về các sự kiện.
Thông thường, các ứng dụng sẽ đăng ký nhiều lệnh gọi lại cho cùng một thông tin cơ bản. Thay vì để máy chủ thông báo cho ứng dụng một lần cho mỗi lệnh gọi lại đã đăng ký bằng IPC, thư viện ứng dụng nên có một lệnh gọi lại đã đăng ký bằng IPC với máy chủ, sau đó thông báo cho từng lệnh gọi lại đã đăng ký trong ứng dụng.
digraph d_front_back {
rankdir=RL;
node [style=filled, shape="rectangle", fontcolor="white" fontname="Roboto"]
server->clib
clib->c1;
clib->c2;
clib->c3;
subgraph cluster_client {
graph [style="dashed", label="Client app process"];
c1 [label="my.app.FirstCallback" color="#4285F4"];
c2 [label="my.app.SecondCallback" color="#4285F4"];
c3 [label="my.app.ThirdCallback" color="#4285F4"];
clib [label="android.app.FooManager" color="#F4B400"];
}
subgraph cluster_server {
graph [style="dashed", label="Server process"];
server [label="com.android.server.FooManagerService" color="#0F9D58"];
}
}