Вызовы API Android обычно сопровождаются значительными задержками и вычислительными затратами на каждый вызов. Поэтому кэширование на стороне клиента играет важную роль при проектировании полезных, корректных и производительных API.
Мотивация
API, предоставляемые разработчикам приложений в Android SDK, часто реализуются в виде клиентского кода в Android Framework, который выполняет вызов 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 Package Manager, намеренное отключение кэширования из-за большого объема вызовов во время загрузки.
 - Не установлен: кэш существует, но не инициализирован. Одноразовый код был не установлен, что означает, что кэш никогда не был аннулирован.
 - Обход: преднамеренное решение пропустить кэш.
 
 - Высокие показатели указывают на потенциальную неэффективность использования кэша.
 
Делает недействительным:
- Определение: Процесс маркировки кэшированных данных как устаревших или неактуальных.
 - Значимость: дает сигнал о том, что система работает с самыми актуальными данными, предотвращая ошибки и несоответствия.
 - Обычно инициируется сервером, владеющим данными.
 
Текущий размер:
- Определение: Текущее количество элементов в кэше.
 - Значимость: указывает на использование ресурсов кэша и потенциальное влияние на производительность системы.
 - Более высокие значения обычно означают, что кэш использует больше памяти.
 
Максимальный размер:
- Определение: Максимальный объем пространства, выделенного для кэша.
 - Значимость: определяет емкость кэша и его способность хранить данные.
 - Установка подходящего максимального размера помогает сбалансировать эффективность кэша с использованием памяти. По достижении максимального размера новый элемент добавляется путём вытеснения наиболее редко используемого элемента, что может указывать на неэффективность.
 
Наивысшая отметка:
- Определение: Максимальный размер кэша с момента его создания.
 - Значимость: дает представление о пиковом использовании кэша и потенциальной нагрузке на память.
 - Мониторинг наивысшей точки может помочь выявить потенциальные узкие места или области для оптимизации.
 
Переполнения:
- Определение: Количество раз, когда кэш превышал максимальный размер и приходилось удалять данные, чтобы освободить место для новых записей.
 - Значимость: указывает на нехватку кэша и потенциальное снижение производительности из-за вытеснения данных.
 - Высокие показатели переполнения указывают на необходимость корректировки размера кэша или переоценки стратегии кэширования.
 
Эту же статистику можно найти в отчете об ошибке.
Настройте размер кэша
У кэшей есть максимальный размер. При его превышении записи удаляются в порядке 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 Гц. Но гипотетически клиентский код 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"];
  }
}