Android API 呼叫通常會在每次叫用時產生大量的延遲和運算。因此,在設計有用、正確且效能良好的 API 時,請務必考量用戶端快取功能。
動機
在 Android SDK 中向應用程式開發人員公開的 API,通常會在 Android 架構中實作為用戶端程式碼,用於在平台程序中對系統服務發出 Binder 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
...
欄位
命中:
- 定義:快取中成功找到要求的資料次數。
- 重要性:表示資料的擷取方式高效快速,可減少不必要的資料擷取。
- 數量越多,效果通常越好。
清除:
- 定義:因無效而清除快取的次數。
- 清除原因:
- 無效:來自伺服器的過時資料。
- 空間管理:在快取已滿時為新資料釋出空間。
- 數量高可能表示資料經常變更,且可能效率不彰。
未接通:
- 定義:快取無法提供要求資料的次數。
- 原因:
- 快取效率不佳:快取空間太小或未儲存正確的資料。
- 資料變更頻繁。
- 首次要求。
- 數量高表示可能有快取問題。
略過次數:
- 定義:即使可以使用快取,但例項完全未使用快取。
- 略過的原因:
- Corking:針對 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 用戶端作者可以決定 memoization 的時間長度。
舉例來說,應用程式可以透過查詢每個繪製影格中的統計資料,向使用者顯示網路流量統計資料:
@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
中的用戶端程式碼選擇每秒最多查詢一次伺服器的統計資料,且如果在先前查詢後的 1 秒內進行查詢,則會傳回上次查詢的值。這是因為 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
考慮使用用戶端 codegen 而非伺服器查詢
如果伺服器在建構期間可得知查詢結果,請考慮是否也能在建構期間讓用戶端得知這些結果,並考慮是否可在用戶端端完全實作 API。
請考慮使用以下應用程式程式碼,檢查裝置是否為手錶 (也就是裝置是否執行 Wear OS):
public boolean isWatch(Context ctx) {
PackageManager pm = ctx.getPackageManager();
return pm.hasSystemFeature(PackageManager.FEATURE_WATCH);
}
裝置的這項屬性會在建構期間知曉,特別是在為這部裝置的啟動映像檔建構 Framework 時。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"];
}
}