Как создать кеш-менеджер, такой как Glide

В этой статье мы узнаем, что такое Cache, разберемся, как работает система кэширования Glide и как реализовать собственный CacheManager с использованием стратегии LRU.

  1. Что такое кеш?
  2. Как мы кэшируем данные?
  3. Стратегии кеширования.
  4. Кэш LRU
  5. Понимание скольжения

Что такое кеш?

Кэш, который произносится как CASH, - это аппаратное или программное обеспечение, которое используется для хранения чего-либо, обычно данных, временно в вычислительной среде. Причины кеширования данных - быстрое извлечение и снижение стоимости сети. Быстрый поиск данных поможет повысить скорость отклика приложения, чтобы пользователи могли лучше взаимодействовать. Проще говоря, мы можем сказать, что кеш - это временное хранилище данных.

Как мы кэшируем данные?

Первоначально мы извлекаем данные из любого внешнего источника, например из сети, что может занять некоторое время после получения данных из сети, затем данные сохраняются или кэшируются локально для последующего извлечения. Ниже перечислены две основные стратегии реализации системы кеширования в Android.

Стратегии кеширования:

  • Кэш памяти: мы храним полученные данные в памяти приложения. Каждый раз, когда приложение уничтожается, кэшированные данные удаляются. Он используется для одного сеанса приложения. Кэш памяти будет самой быстрой стратегией кэширования для извлечения данных, поскольку они находятся внутри памяти.
  • Дисковый кеш: мы храним полученные данные на доступном диске. Так что даже после того, как приложение будет убито, данные сохранятся. Полезно даже после перезапуска приложения. Получение будет медленнее по сравнению с кешем памяти, так как это была операция ввода-вывода.

Кэш LRU

Поскольку память является ограничением для приложений, нам нужно выяснить, какой механизм кеширования будет использоваться. LRU - это аббревиатура от слова Least Recently Used, которое является одним из лучших и наиболее часто используемых методов кэширования данных. Платформа Android предоставляет класс LruCache, начиная с API 12 (или в библиотеке support-v4).

Давайте разберемся в этом на примере загрузки изображений на экран. Что мы делаем, так это загружаем изображение из URL-адреса и отображаем его в виде изображения. Часть загрузки занимает некоторое время в зависимости от скорости сети. Итак, если мы делаем одно и то же каждый раз, мы делаем много сетевых запросов для загрузки изображения, которое уже было загружено ранее. Это не очень хорошая практика, поэтому мы загружаем растровое изображение один раз, кэшируем его в памяти и при необходимости используем снова и снова. Кэш хранит объекты, которые могут быть использованы в будущем. Поскольку у приложений нет лишней памяти в случае загрузки изображений в режим ресайклера, мы не можем позволить себе сохранять все загруженные изображения в памяти. Итак, нам нужно определить определенный объем памяти для Cache. Если кэш заполняется и загружается новое изображение, где мы его кешируем? Здесь нам нужно избавиться от одного элемента, чтобы освободить память для последнего. Итак, вопрос в том, какой элемент нужно удалить? Очевидный ответ заключается в том, что мы пытаемся удалить, какой объект не используется в будущем или который не использовался в течение самого длительного периода. Трудно найти первый случай, который будет использоваться или нет в будущем, потому что мы не уверены.

Класс кэша LRU поддерживает список элементов, которые мы помещаем в него.

Когда мы получаем доступ к любому объекту, он перемещается в начало списка и остается там до тех пор, пока не будет получен доступ к другому элементу.

Когда кеш заполнится, пора выбрать, какой элемент удалить. Ответ прост: недавно использованные объекты будут располагаться в начале списка, а менее используемые объекты - в конце списка.

Этот алгоритм замены называется LRU (наименее использованный в последнее время), в котором говорится, что чем больше объект используется сейчас, тем выше вероятность его использования в будущем, поэтому он должен оставаться в кеше.

Шаг 1:

var bitmapCache = LruCache ‹String, Bitmap› ()

Кэш LRU поддерживает элементы в паре Ключ, Значение. При создании нам нужно выяснить, что используется для ключа и какой тип объекта для значения. Например, как и при кэшировании растровых изображений, мы используем имя файла как ключ, а Bitmap как значение.

Шаг 2:

Нам нужно определить определенный размер во время инициализации, чтобы он не съел всю память. Не существует определенного размера или формулы, подходящей для всех приложений, вам нужно проанализировать свое использование и найти подходящее решение. Слишком маленький кеш вызывает дополнительные накладные расходы без каких-либо преимуществ, слишком большой кеш может снова вызвать исключение OutofMemory и оставить остальной части вашего приложения небольшой объем памяти для работы. Размер кеша зависит от множества факторов, таких как количество загружаемых изображений, размер экрана, плотность, частота обращений и т. Д.

Шаг 3:

При загрузке растрового изображения в ImageView сначала проверяется LruCache. Если запись найдена, она используется немедленно для обновления ImageView, в противном случае создается фоновый поток для обработки изображения.

fun loadBitmap(resId: Int, imageView: ImageView) {
    val imageKey: String = resId.toString()
val bitmap: Bitmap? = getBitmapFromMemCache(imageKey)?.also {
        mImageView.setImageBitmap(it)
    } ?: run {
        mImageView.setImageResource(R.drawable.image_placeholder)
        val task = BitmapWorkerTask()
        task.execute(resId)
        null
    }
}

Шаг 4:

BitmapWorkerTaks также необходимо обновить, чтобы добавить записи в кэш памяти.

private inner class BitmapWorkerTask : AsyncTask<Int, Unit, Bitmap(){
    ...
    // Decode image in background.
    override fun doInBackground(vararg params: Int?): Bitmap? {
    return params[0]?.let { imageId ->
         decodeSampledBitmapFromResource(resources, imageId, 100,
               100)?.also { bitmap ->
               addBitmapToMemoryCache(imageId.toString(), bitmap)
            }
        }
    }
    ...
}

Шаг 5:

Поскольку кэш памяти предназначен только для одного сеанса, лучше всего реализовать DiskCache, чтобы их можно было загружать даже после перезапуска приложения. Я не буду подробно рассказывать о дисковом кэше, пожалуйста, проверьте DiskLruCache

Понимание скольжения

Если вы ранее использовали Glide или любую другую библиотеку загрузки изображений, вы можете знать, что вам ничего не нужно делать для активации кеша, который используется Glide или этой библиотекой. Библиотеки загрузки изображений по умолчанию создают кеш, когда мы начинаем использовать его для загрузки изображений. По умолчанию Glide использует LruResourceCache реализацию интерфейса MemoryCache по умолчанию, который использует фиксированный объем памяти с вытеснением LRU. Размер LruResourceCache определяется классом MemorySizeCalculator Glide, который смотрит на класс памяти устройства, независимо от того, используется ли у устройства низкая оперативная память и разрешение экрана.

По умолчанию Glide проверяет несколько уровней кешей перед запуском нового запроса на изображение:

  1. Активные ресурсы. Отображается ли это изображение в другом представлении прямо сейчас?
  2. Кэш памяти - было ли это изображение загружено недавно и все еще находится в памяти?
  3. Ресурс. Было ли это изображение декодировано, преобразовано и записано в кеш диска раньше?
  4. Данные - были ли данные этого образа ранее получены из записи в кеш диска?

Первые два шага проверяют, находится ли ресурс в памяти, и если да, немедленно вернуть изображение. Вторые два шага проверяют, находится ли образ на диске, и возвращаются быстро, но асинхронно.

Если все четыре шага не позволяют найти изображение, Glide вернется к исходному источнику для извлечения данных (исходный файл, URI, URL-адрес и т. Д.).

При использовании Glide мы можем выбрать стратегию кеширования, а также отказаться от использования определенного кеша.

Glide  
    .with( context )
    .load( imageUrl )
    .skipMemoryCache( true )
    .into( imageView );

Вы также можете выбрать загрузку только из кеша с помощью метода onlyRetrieveFromCache. Если установлено значение true, он будет загружать элемент, только если он найден в кеше, и не будет извлекаться из источника. Если установлено значение false, он будет загружать только элемент из источника.

Glide  
    .with( context )
    .load( imageUrl )
    .onlyRetrieveFromCache( true )
    .into( imageView );

Настроить поведение кэша диска для скольжения

В Glide есть несколько вариантов поведения дискового кеша. Кэширование диска Glide довольно сложно. Например, Picasso только кэширует развернутое изображение. Однако Glide кэширует исходное изображение с полным разрешением и, кроме того, уменьшенные версии этого изображения. Например, если вы запрашиваете изображение размером 1000x1000 пикселей, и если наш ImageView равен 500x500 пикселей, Glide поместит обе версии изображения в кеш.

Теперь давайте разберемся с параметрами перечисления для метода .diskCacheStrategy():

  • Glide 3.x и 4.x: DiskCacheStrategy.NONE ничего не кеширует, как обсуждалось
  • Glide 4.x: DiskCacheStrategy.DATA, Glide 3.x: DiskCacheStrategy.SOURCE кэширует только исходное изображение с полным разрешением. В нашем примере выше это будет размер 1000x1000 пикселей.
  • Glide 4.x: DiskCacheStrategy.RESOURCE Glide 3.x: DiskCacheStrategy.RESULT кэширует только окончательное изображение после уменьшения разрешения (и, возможно, преобразований) (поведение по умолчанию в Glide 3.x)
  • Только Glide 4.x: DiskCacheStrategy.AUTOMATIC разумно выбирает стратегию кэширования в зависимости от ресурса (поведение по умолчанию в Glide 4.x)
  • Glide 3.x и 4.x: DiskCacheStrategy.ALL кэширует все версии изображения
Glide  
    .with( context )
    .load( imageUrl )
    .diskCacheStrategy( DiskCacheStrategy.SOURCE )
    .into( imageView );

Внутреннее устройство

Когда мы вызываем Glide.with (context), мы говорим glide, чтобы начать загрузку, передавая контекст. Он принимает контекст и возвращает RequestManager для приложения верхнего уровня, которое можно использовать для запуска загрузки. RequestManger - это класс для управления и запуска запросов для Glide. Он может использовать события жизненного цикла активности, фрагмента и подключения для разумных стоп, запуска и перезапуска запросов.

public static RequestManager with(Context context) {
 RequestManagerRetriever retriever = RequestManagerRetriever.get();
 return retriever.get(context);
}

Приведенный выше оператор return имеет следующую реализацию: он проверяет тип контекста, при внутреннем выполнении проверяет, является ли контекст живым или нет, и возвращает соответствующий Requestmanger.

public RequestManager get(Context context) {
    if (context == null) {
        throw new IllegalArgumentException("You cannot start a load          .                                              on a null Context");
    } else if (Util.isOnMainThread() && !(context instanceof 
         Application)) {
        if (context instanceof FragmentActivity) {
            return get((FragmentActivity) context);
        } else if (context instanceof Activity) {
            return get((Activity) context);
        } else if (context instanceof ContextWrapper) {
            return get(((ContextWrapper) context).getBaseContext());
        }
    }

    return getApplicationManager(context);
}

Второй шаг, когда мы вызываем .load (URL), который возвращает построитель запросов для загрузки заданного URL или URI. Мы предоставляем строку либо путь к файлу, либо URI или URL, обрабатываемый UriLoader. Он возвращает DrawableTypeRequest ‹String›, который является классом для создания запроса загрузки, который загружает либо рисованный анимированный GIF, либо растровое изображение, либо добавляет ResourceTranscoder , чтобы перекодировать данные в тип ресурса, отличный от Drawable.

Последний шаг при вызове. into (view) загрузит GlideDrable в представление, заданное после завершения загрузки запроса, который был начат на предыдущем шаге.

Glide преодолевает проблемы

OutOfMemory - путем понижающей дискретизации исходного изображения без загрузки полного изображения в память, как описано выше.

Медленная Загрузка - поскольку он использует такие типы механизмов кеширования, как кэш в памяти и кеш диска, загрузка будет быстрее.

Smooth Running- Bitmap Pool - это метод управления памятью для повторного использования памяти растровых изображений. Поскольку он повторно использует растровую память, больше не нужно вызывать GC снова и снова, что обеспечивает бесперебойную работу приложения. Он использует растровое изображение при декодировании растрового изображения в поддерживаемых версиях Android. Все варианты использования версии были обработаны для ее лучшей оптимизации.

Зачем использовать Glide?

Приложение на основе изображений декодирует множество изображений, поэтому в приложении будет происходить постоянное выделение и освобождение памяти, что приводит к очень частым вызов GC (сборщика мусора). И, наконец, из-за частого вызова GC приложение зависает. Используйте пул растровых изображений, чтобы избежать непрерывного выделения и освобождения памяти в приложении и уменьшить накладные расходы на сборщик мусора, что приведет к плавной работе приложения.

Предположим, нам нужно загрузить несколько растровых изображений в приложение для Android. Когда мы загружаем bitmapOne, он выделяет память для bitmap one. Затем, если нам не нужен bitmapOne, не перерабатывайте растровое изображение (как если бы вы перерабатывали, он вызовет GC), поэтому используйте этот bitmapOne как inBitmap для второго растрового изображения, чтобы та же самая память могла быть повторно использована для второго битовая карта. Таким образом, мы можем избежать непрерывного выделения и освобождения памяти в приложении и уменьшить накладные расходы на сборщик мусора. Но проблема в том, что есть несколько ограничений, поскольку версия Android ниже, чем Honeycomb, не поддерживает ее, несколько Android версия меньше, чем Kitkat, только когда мы используем размер выборки = 1, выше он полностью поддерживает и некоторые другие проблемы. Итак, все эти типы случаев обрабатываются в этом Glide.

Поэтому вместо того, чтобы создавать систему с нуля, используйте готовое решение, которое справляется с большинством случаев. Для обучения возьмите каждую концепцию из Glide и проверьте, как она работает.

На этом пока все, надеюсь, вам понравилась эта статья. Пожалуйста, поделитесь им со своими друзьями, если вы нашли его полезным.

Пожалуйста, дайте мне знать ваши предложения и комментарии.

Вы можете найти меня в Medium и LinkedIn

Спасибо за прочтение…