Android API 呼叫通常會涉及每次呼叫時的顯著延遲和運算。因此,在設計實用、正確且高效的 API 時,用戶端快取是重要的考量因素。
動機
Android SDK 中向應用程式開發人員公開的 API,通常會實作為 Android 架構中的用戶端程式碼,並向平台程序中的系統服務發出繫結器 IPC 呼叫,後者的工作是執行某些運算,然後將結果傳回給用戶端。這項作業的延遲時間通常取決於下列三項因素:
- IPC 負荷:基本 IPC 呼叫的延遲時間通常是基本程序內方法呼叫的 10,000 倍。
 - 伺服器端爭用:系統服務中為回應用戶端要求所完成的工作可能不會立即開始,例如伺服器執行緒正忙於處理先前收到的其他要求。
 - 伺服器端運算:在伺服器中處理要求本身可能需要大量工作。
 
只要在用戶端實作快取,即可消除上述三種延遲因素,前提是快取必須符合下列條件:
- 正確:用戶端快取絕不會傳回與伺服器傳回結果不同的結果。
 - 有效:用戶端要求通常是從快取提供服務,例如快取命中率很高。
 - 效率高:用戶端快取會有效運用用戶端資源,例如以精簡方式表示快取資料,以及不會在用戶端記憶體中儲存過多快取結果或過時資料。
 
考慮在用戶端快取伺服器結果
如果用戶端經常多次提出完全相同的要求,且傳回的值不會隨時間改變,您就應該在用戶端程式庫中實作快取,並以要求參數做為鍵。
建議您在實作中加入 IpcDataCache:
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);
    }
}
如需完整範例,請參閱 android.app.admin.DevicePolicyManager。
IpcDataCache 適用於所有系統程式碼,包括主線模組。此外,還有 PropertyInvalidatedCache,這與  幾乎相同,但只對架構可見。盡可能使用 IpcDataCache。
在伺服器端變更時使快取失效
如果伺服器傳回的值可能會隨時間變更,請實作回呼來觀察變更,並註冊回呼,以便視情況使用戶端快取失效。
在單元測試案例之間撤銷快取
在單元測試套裝組合中,您可能會針對測試替身 (而非實際伺服器) 測試用戶端程式碼。如果是,請務必在測試案例之間清除所有用戶端快取。這是為了確保測試案例彼此密封,避免一個測試案例干擾另一個測試案例。
@RunWith(AndroidJUnit4.class)
public class BirthdayManagerTest {
    @Before
    public void setUp() {
        BirthdayManager.clearCache();
    }
    @After
    public void tearDown() {
        BirthdayManager.clearCache();
    }
    ...
}
編寫 CTS 測試時,如果 API 用戶端在內部使用快取,快取就是 API 作者不會接觸到的實作詳細資料,因此 CTS 測試不應要求具備用戶端程式碼所用快取的任何特殊知識。
研究快取命中和未命中
IpcDataCache 和 PropertyInvalidatedCache 可以列印即時統計資料:
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
  ...
欄位
命中次數:
- 定義:系統在快取中成功找到所要求資料的次數。
 - 重要性:表示資料擷取效率高且速度快,可減少不必要的資料擷取作業。
 - 一般來說,數量越高越好。
 
清除:
- 定義:因失效而清除快取的次數。
 - 清除原因:
- 失效:伺服器中的資料已過時。
 - 空間管理:快取空間已滿時,為新資料釋出空間。
 
 - 如果計數偏高,可能表示資料經常變更,效率可能不彰。
 
未命中:
- 定義:快取無法提供所要求資料的次數。
 - 原因:
- 快取效率不彰:快取太小或未儲存正確資料。
 - 資料變更頻繁。
 - 首次要求。
 
 - 如果計數值偏高,可能表示有快取問題。
 
略過:
- 定義:即使可以使用快取,但完全未使用快取的例項。
 - 略過原因:
- 封鎖:專指 Android 套件管理員更新,由於啟動期間的呼叫量過高,因此刻意關閉快取。
 - 未設定:快取存在但未初始化。Nonce 未設定,表示快取從未失效。
 - 略過:有意略過快取的決策。
 
 - 如果計數值很高,表示快取使用效率可能不佳。
 
失效時間:
- 定義:將快取資料標示為過時或舊資料的程序。
 - 重要性:提供系統使用最新資料的信號,避免發生錯誤和不一致的情況。
 - 通常是由擁有資料的伺服器觸發。
 
目前大小:
- 定義:快取中的元素數量。
 - 重要性:指出快取的資源用量,以及對系統效能的潛在影響。
 - 值越高通常表示快取使用的記憶體越多。
 
大小上限:
- 定義:分配給快取的空間上限。
 - 重要性:決定快取的容量和資料儲存能力。
 - 設定適當的大小上限有助於平衡快取效能與記憶體用量。達到大小上限後,系統會逐出最近最少使用的元素,藉此新增元素,這可能表示效率不彰。
 
高水位線:
- 定義:快取自建立以來達到的最大大小。
 - 重要性:深入瞭解快取用量尖峰和潛在記憶體壓力。
 - 監控高水位線有助於找出潛在瓶頸或可最佳化的區域。
 
溢位:
- 定義:快取超過大小上限的次數,且必須移除資料,才能為新項目騰出空間。
 - 重要性:表示快取壓力,以及因資料清除而可能導致的效能下降。
 - 溢位次數偏高表示可能需要調整快取大小,或重新評估快取策略。
 
您也可以在錯誤報告中找到相同的統計資料。
調整快取大小
快取有大小上限。超過快取大小上限時,系統會依 LRU 順序逐出項目。
- 快取項目過少可能會對快取命中率造成負面影響。
 - 快取項目過多會增加快取的記憶體用量。
 
找出適合您用途的平衡點。
消除多餘的用戶端呼叫
用戶端可能會在短時間內多次向伺服器發出相同查詢:
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();
  }
}
考慮重複使用先前呼叫的結果:
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();
  }
}
考慮在用戶端記憶體中儲存最近的伺服器回應
用戶端應用程式查詢 API 的速度可能比 API 伺服器產生有意義的新回應還快。在這種情況下,有效的方法是在用戶端記憶上次看到的伺服器回應和時間戳記,並在記憶結果夠新時傳回記憶結果,而不查詢伺服器。API 用戶端作者可以決定記憶化時間長度。
舉例來說,應用程式可能會在繪製的每個影格中查詢統計資料,向使用者顯示網路流量統計資料:
@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()));
}
應用程式可能會以 60 Hz 的頻率繪製影格。但假設 TrafficStats 中的用戶端程式碼最多每秒查詢一次伺服器統計資料,且如果是在前次查詢後的一秒內查詢,則會傳回上次看到的值。由於 API 說明文件未提供任何有關傳回結果新鮮度的合約,因此允許這種做法。
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
建議使用用戶端程式碼產生,而非伺服器查詢
如果伺服器在建構時可得知查詢結果,請考慮用戶端在建構時是否也能得知,並考慮是否能在用戶端完整實作 API。
請參考下列應用程式程式碼,檢查裝置是否為手錶 (也就是裝置是否執行 Wear OS):
public boolean isWatch(Context ctx) {
    PackageManager pm = ctx.getPackageManager();
    return pm.hasSystemFeature(PackageManager.FEATURE_WATCH);
}
裝置的這項屬性會在建構時 (具體來說,是在為裝置的開機映像檔建構架構時) 取得。hasSystemFeature 的用戶端程式碼可以立即傳回已知結果,不必查詢遠端 PackageManager 系統服務。
在用戶端中重複資料刪除伺服器回呼
最後,API 用戶端可以向 API 伺服器註冊回呼,以便接收事件通知。
應用程式通常會為相同的基礎資訊註冊多個回呼。伺服器不應使用 IPC 為每個已註冊的回呼通知用戶端,用戶端程式庫應使用 IPC 向伺服器註冊一個回呼,然後通知應用程式中的每個已註冊回呼。
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"];
  }
}