Nguyên tắc lưu vào bộ nhớ đệm phía máy khách của API Android

Các lệnh gọi API Android thường liên quan đến độ trễ và quá trình tính toán đáng kể cho mỗi lần 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 suất cao.

Động lực

Các API được cung cấp cho nhà phát triển ứng dụng trong SDK Android thường được triển khai dưới dạng mã phía máy khách trong Khung Android. Mã này sẽ thực hiện lệnh gọi Binder IPC đến một dịch vụ hệ thống trong 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 máy khách. Độ 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 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 máy khách có thể không bắt đầu ngay lập tức, ví dụ: 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 đó.
  • Quá trình tính toán phía máy chủ: bản thân công việc xử lý yêu cầu trong 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ệ 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 biểu thị 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 việc lưu kết quả của máy chủ vào bộ nhớ đệm ở máy khách

Nếu máy khách 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 máy khách được khoá bằng các tham số yêu cầu.

Hãy cân nhắc việ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 hệt nhau, nhưng chỉ khung mới có thể nhìn thấy. Hãy ưu tiên sử dụng 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.

Vô hiệu hoá bộ nhớ đệm giữa các trường hợp kiểm thử đơn vị

Trong bộ kiểm thử đơn vị, bạn có thể kiểm thử mã máy khách dựa trên một đối tượng kiểm thử thay vì máy chủ thực. Nếu vậy, 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 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 bài kiểm thử CTS sử dụng 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 bài kiểm thử CTS không cần kiến thức đặc biệt nào về bộ nhớ đệm được sử dụng trong mã máy khách.

Nghiên cứu các lượt truy cập và lượt bỏ qua bộ nhớ đệm

IpcDataCachePropertyInvalidatedCache 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 quá trình truy xuất dữ liệu hiệu quả và nhanh chóng, giảm việc truy xuất dữ liệu không cần thiết.
  • Số lượng cao hơn thường tốt hơn.

Lượt xoá:

  • Định nghĩa: Số lần bộ nhớ đệm bị xoá do bị vô hiệu hoá.
  • Lý do xoá:
    • Vô hiệu hoá: Dữ liệu lỗi thời từ máy chủ.
    • Quản lý dung lượng: Tạo chỗ trống cho dữ liệu mới khi bộ nhớ đệm đầy.
  • Số lượng cao có thể cho biết dữ liệu thay đổi thường xuyên và có thể không hiệu quả.

Lượt bỏ qua:

  • Đị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.

Lượt bỏ qua:

  • Định nghĩa: Các trường hợp bộ nhớ đệm không được sử dụng, mặc dù có thể sử dụng.
  • Lý do bỏ qua:
    • Corking: Cụ thể đố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 số lượng lệnh gọi lớn 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. Nonce chưa được đặt, nghĩa là bộ nhớ đệm chưa bao giờ bị vô hiệu hoá.
    • Bỏ qua: Quyết định cố ý bỏ qua bộ nhớ đệm.
  • Số lượng cao cho thấy có thể không hiệu quả trong việc sử dụng bộ nhớ đệm.

Lượt 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 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 ngừa 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 đế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.
  • 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ớ. Sau 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ả.

Mức cao nhất:

  • Đị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 tiềm ẩn về bộ nhớ.
  • Việc giám sát mức 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á.

Lượt 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.
  • Ý nghĩa: Cho biết áp lực bộ nhớ đệm và sự suy giảm hiệu suất tiềm ẩn do việc loại bỏ dữ liệu.
  • Số lượt tràn cao cho thấy 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ệ kết quả tìm kiếm trong bộ nhớ cache.
  • 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 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 máy khách dư thừa

Máy khách 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();
  }
}

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ủ

Các ứng dụng máy 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ủ được thấy ở phía máy khách cùng với dấu thời gian và trả về kết quả được ghi nhớ mà không cần truy vấn máy chủ nếu kết quả được ghi nhớ đủ gần đây. Tác giả ứ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ị số liệu thống kê về lưu lượng truy cập mạng cho người dù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 ở tần số 60 Hz. Tuy nhiên, theo giả thuyết, mã máy khách 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ừ truy vấn trước đó, hãy trả về giá trị được 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 liên quan đến độ 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 gian xây dựng, thì hãy cân nhắc xem ứng dụng có thể biết kết quả đó tại thời gian xây dựng hay không và cân nhắc xem có thể triển khai API hoàn toàn ở phía ứng dụng hay không.

Hãy cân nhắc đ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 xây dựng, cụ thể là tại thời điểm Khung được xây dựng 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.

Khử trùng các lệnh gọi lại máy chủ trong máy khách

Cuối cùng, ứng dụng API có thể đăng ký lệnh gọi lại với máy chủ API để được thông báo về các sự kiện.

Các ứng dụng thường đă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 máy khách một lần cho mỗi lệnh gọi lại đã đăng ký bằng IPC, thư viện máy khách 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"];
  }
}