Các lệnh gọi API Android thường có độ trễ và 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 trong việc thiết kế các API hữu ích, chính xác và hiệu quả.
Động lực
Các API hiển thị cho nhà phát triển ứng dụng trong SDK Android thường được triển khai dưới dạng mã ứng dụng trong Khung Android. Khung 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 quy trình này là thực hiện một số phép tính và trả về kết quả cho ứng dụng. Thông thường, độ trễ của thao tác này chịu ảnh hưởng của 3 yếu tố:
- Mức hao tổn IPC: một lệnh gọi IPC cơ bản thường có độ trễ gấp 10.000 lần so với độ trễ của một lệnh gọi phương thức cơ bản trong quy trình.
- Xung đột 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, ví dụ: nếu luồng máy chủ đang bận xử lý các yêu cầu khác đã đến sớm hơn.
- Tính toán phía máy chủ: chính công việc xử lý yêu cầu trong máy chủ có thể yêu cầu nhiều công sức.
Bạn có thể loại bỏ cả ba yếu tố độ 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ủ trả về.
- Hiệu quả: các yêu cầu của ứng dụng thường được phân phát từ bộ nhớ đệm, ví dụ: bộ nhớ đệm có tỷ lệ truy cậ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 trình bày dữ liệu được lưu vào bộ nhớ đệm theo cách nhỏ gọn và 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 lưu kết quả máy chủ vào bộ nhớ đệm trong ứng dụng
Nếu ứng dụng thường thực hiện 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á bằng 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ỉ khung mới thấy được. Ư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ị được trả về từ máy chủ có thể thay đổi theo thời gian, hãy triển khai lệnh gọi lại để quan sát các thay đổi và đăng ký lệnh gọi lại để bạn có thể vô hiệu hoá bộ nhớ đệm phía máy khách cho phù hợp.
Làm mất hiệu lự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 dựa trên một kiểm thử kép 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 là để giữ cho các trường hợp kiểm thử tương đối khép kín và ngăn một trường hợp kiểm thử can thiệp vào 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 chương trình kiểm thử CTS thực thi ứ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 hiển thị cho tác giả API, do đó, các chương trình kiểm thử CTS không nên yêu cầu bất kỳ kiến thức đặc biệt nào về việc lưu vào bộ nhớ đệm được sử dụng trong mã ứng dụng.
Nghiên cứu các lượt truy cập và lượt bỏ qua 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 việc truy xuất dữ liệu hiệu quả và nhanh chóng, giảm thiểu việc truy xuất dữ liệu không cần thiết.
- Số lượng càng cao thì càng tốt.
Xoá:
- Định nghĩa: Số lần xoá bộ nhớ đệm do không hợp lệ.
- Lý do xoá:
- Không hợp lệ: Dữ liệu đã lỗi thời từ máy chủ.
- Quản lý dung lượng: Dành chỗ cho dữ liệu mới khi bộ nhớ đệm đầy.
- Số lượng cao có thể cho biết dữ liệu thường xuyên thay đổi và có thể không hiệu quả.
Số lần 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:
- Lưu vào 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 thay đổi thường xuyên.
- Yêu cầu lần đầu.
- Số lượng cao cho thấy có thể có vấn đề về bộ nhớ đệm.
Số 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: Riêng đối với các bản cập nhật Trình quản lý gói Android, hãy cố tình tắt tính năng lưu vào bộ nhớ đệm do có nhiều 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. Số chỉ dùng một lần chưa được đặt, nghĩa là bộ nhớ đệm chưa bao giờ bị vô hiệu hoá.
- Bỏ qua: Quyết định chủ ý bỏ qua bộ nhớ đệm.
- Số lượng cao cho biết có thể việc sử dụng bộ nhớ đệm không hiệu quả.
Làm mất hiệu lực:
- Đị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 hoặc cũ.
- Ý nghĩa: 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.
- Ý nghĩa: Cho biết mức sử dụng tài nguyên của bộ nhớ đệm và tác động tiềm ẩn lên hiệu suất hệ thống.
- 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: Dung lượng tối đa được phân bổ cho bộ nhớ đệm.
- Ý nghĩa: 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 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 ít nhất gần đây. Điều này có thể cho thấy sự thiếu hiệu quả.
Hình mờ cao:
- Đị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 cao 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 bộ nhớ:
- Định nghĩa: Số lần bộ nhớ đệm vượt quá kích thước tối đa và phải xoá dữ liệu để tạo không gian cho các mục mới.
- Ý nghĩa: Cho biết áp lực bộ nhớ đệm và khả năng giảm hiệu suất do xoá dữ liệu.
- Số lượng tràn bộ nhớ đệm 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ể xem những số liệu thống kê tương tự trong báo cáo lỗi.
Điều chỉnh kích thước 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 nhập sẽ bị loại bỏ theo thứ tự LRU.
- Việc lưu quá ít mục vào bộ nhớ đệm có thể ảnh hưởng tiêu cực đến tỷ lệ truy cập vào bộ nhớ đệm.
- Việc lưu quá nhiều mục vào bộ nhớ đệm sẽ làm tăng mức sử dụng bộ nhớ của bộ nhớ đệm.
Tìm 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 ứng dụng dư thừa
Ứng dụng có thể gửi 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 các phản hồi gần đây của máy chủ
Ứ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 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à lưu vào bộ nhớ đệm phản hồi máy chủ đã xem gần đây nhất ở phía máy khách cùng với dấu thời gian và trả về kết quả đã lưu vào bộ nhớ đệm mà không cần truy vấn máy chủ nếu kết quả đã lưu vào bộ nhớ đệm đủ mới. Tác giả ứng dụng API có thể xác định thời lượng lưu vào bộ nhớ đệm.
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 đượ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ẽ khung hình ở tốc độ 60 Hz. Nhưng giả sử, mã ứng dụng trong TrafficStats
có thể chọn truy vấn máy chủ để biết 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 sau một truy vấn trước đó, hãy trả về giá trị đã thấy gần đây nhất.
Điều này được cho phép vì tài liệu API không cung cấp bất kỳ hợp đồng nào về độ 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
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 điểm tạo bản dựng, hãy cân nhắc xem máy khách có thể biết kết quả truy vấn tại thời điểm tạo bản dựng hay không và xem liệu API có thể được triển khai hoàn toàn ở phía máy khách hay không.
Hãy xem xét 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ị có đang chạy Wear OS hay không):
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 bản dựng, cụ thể là tại thời điểm tạo Khung 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ề kết quả đã biết ngay lập tức, 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 máy chủ trùng lặp 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ì yêu cầu 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 phải 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"];
}
}