Когда следует перерабатывать растровое изображение с использованием LRUCache?

Я использую LRUCache для кэширования растровых изображений, которые хранятся в файловой системе. Я построил кеш, основанный на примерах здесь: http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html

Проблема в том, что я вижу, что OutOfMemory часто сбой при использовании приложения. Я считаю, что когда LRUCache вытесняет изображение, чтобы освободить место для другого, память не освобождается.

Я добавил вызов Bitmap.recycle (), когда изображение выведено:

  // use 1/8 of the available memory for this memory cache final int cacheSize = 1024 * 1024 * memClass / 8; mImageCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { return bitmap.getByteCount(); } @Override protected void entryRemoved(boolean evicted, String key, Bitmap oldBitmap, Bitmap newBitmap) { oldBitmap.recycle(); oldBitmap = null; } }; 

Это исправляет сбои, однако это также приводит к тому, что изображения иногда не появляются в приложении (просто черное пространство, где должно быть изображение). В любое время, когда это происходит, я вижу это сообщение в своем Logcat: Cannot generate texture from bitmap .

Быстрый поиск в Google показывает, что это происходит, потому что изображение, которое отображается, было переработано.

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

ПОСТАНОВИЛИ: В случае, если это полезно кому-либо еще, решение этой проблемы, предложенное принятым ответом, НЕ должно делать то, что я сделал в приведенном выше примере кода (не перерабатывайте растровые изображения в entryRemoved() ).

Вместо этого, когда вы закончите работу с ImageView (например, onPause() в onPause() или когда представление переработано в адаптере), проверьте, все ли isImageInCache() в кеше (я добавил метод isImageInCache() в свой кеш Класс), а если нет, то переработайте растровое изображение. В противном случае оставьте это в покое. Это фиксировало мои исключения OutOfMemory и предотвратило использование растровых изображений, которые все еще использовались.

Solutions Collecting From Web of "Когда следует перерабатывать растровое изображение с использованием LRUCache?"

Я считаю, что когда LRUCache вытесняет изображение, чтобы освободить место для другого, память не освобождается.

Этого не будет, пока Bitmap будет переработан или собран из мусора.

Быстрый поиск в Google показывает, что это происходит, потому что изображение, которое отображается, было переработано.

Вот почему вы не должны перерабатывать там.

Почему переработанные изображения все еще находятся в LRUCache, если я только перерабатываю их после их удаления?

Предположительно, они не находятся в LRUCache . Они находятся в ImageView или что-то еще, которое все еще использует Bitmap .

Какова альтернатива для реализации кеша?

Для аргумента предположим, что вы используете объекты Bitmap в виджетах ImageView , например, в строках ListView .

Когда вы закончите с Bitmap (например, строка в ListView переработана), вы проверяете, все еще ли она в кеше. Если это так, вы оставите его в покое. Если это не так, вы recycle() .

Кэш просто позволяет вам узнать, какие объекты Bitmap заслуживают внимания. Кэш не знает, используется ли Bitmap где-то.

BTW, если вы находитесь на уровне API 11+, подумайте об использовании inBitmap . OutOMemoryErrors запускаются, когда распределение не может быть выполнено. Последнее, что я проверил, у Android нет сборщика мусора, поэтому вы можете получить OutOfMemoryError из-за фрагментации (хотите выделить что-то большее, чем самый большой единственный доступный блок).

Столкнулся с тем же и благодаря @CommonsWare для обсуждения. Публикация полного решения здесь, так что это помогает большему количеству людей, приезжающих сюда по той же проблеме. Изменения и комментарии приветствуются. ура

  When should I recycle a bitmap using LRUCache? 
  • Точно, когда ваш битмап не находится ни в кеше, ни в ссылках с любого ImageView.

  • Чтобы поддерживать ссылочный счет битмапа, мы должны расширить класс BitmapDrawable и добавить к ним ссылочные атрибуты.

  • Этот образец андроида отвечает на это точно. DisplayingBitmaps.zip

Мы перейдем к деталям и коду ниже.

 (don't recycle the bitmaps in the entryRemoved() call). 

Не совсем.

  • В делегате entryRemoved проверьте, по-прежнему ли Bitmap ссылается на любой ImageView. Если не. Переработайте его там сами.

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

  • Ключевым моментом здесь является необходимость проверить на обоих местах, можно ли переработать растровое изображение или нет.

Я объясню свой конкретный случай, когда я использую LruCache для хранения растровых изображений для меня. И отображение их в ListView. И вызывать переработку на растровых изображениях, когда они больше не используются.

RecyclingBitmapDrawable.java и RecyclingImageView.java образца, упомянутого выше, являются основными элементами, которые нам нужны здесь. Они прекрасно справляются с вещами. Их методы setIsCached и setIsDisplayed делают то, что нам нужно.

Код можно найти в ссылке, упомянутой выше. Но также отправляя полный код файла в нижней части ответа, в случае, если в дальнейшем ссылка опустится или изменится. Сделала небольшую модификацию переопределения setImageResource также для проверки состояния предыдущего растрового изображения.

— Вот код для вас —

Поэтому ваш менеджер LruCache должен выглядеть примерно так.

LruCacheManager.java

 package com.example.cache; import android.os.Build; import android.support.v4.util.LruCache; public class LruCacheManager { private LruCache<String, RecyclingBitmapDrawable> mMemoryCache; private static LruCacheManager instance; public static LruCacheManager getInstance() { if(instance == null) { instance = new LruCacheManager(); instance.init(); } return instance; } private void init() { // We are declaring a cache of 6Mb for our use. // You need to calculate this on the basis of your need mMemoryCache = new LruCache<String, RecyclingBitmapDrawable>(6 * 1024 * 1024) { @Override protected int sizeOf(String key, RecyclingBitmapDrawable bitmapDrawable) { // The cache size will be measured in kilobytes rather than // number of items. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) { return bitmapDrawable.getBitmap().getByteCount() ; } else { return bitmapDrawable.getBitmap().getRowBytes() * bitmapDrawable.getBitmap().getHeight(); } } @Override protected void entryRemoved(boolean evicted, String key, RecyclingBitmapDrawable oldValue, RecyclingBitmapDrawable newValue) { super.entryRemoved(evicted, key, oldValue, newValue); oldValue.setIsCached(false); } }; } public void addBitmapToMemoryCache(String key, RecyclingBitmapDrawable bitmapDrawable) { if (getBitmapFromMemCache(key) == null) { // The removed entry is a recycling drawable, so notify it // that it has been added into the memory cache bitmapDrawable.setIsCached(true); mMemoryCache.put(key, bitmapDrawable); } } public RecyclingBitmapDrawable getBitmapFromMemCache(String key) { return mMemoryCache.get(key); } public void clear() { mMemoryCache.evictAll(); } } 

И ваш getView () адаптера ListView / GridView должен выглядеть нормально, как обычно. Как при установке нового изображения в ImageView с использованием метода setImageDrawable. Его внутренняя проверка ссылочного счета на предыдущем растровом изображении и вызовет его переработку внутри, если не в lrucache.

 @Override public View getView(int position, View convertView, ViewGroup parent) { RecyclingImageView imageView; if (convertView == null) { // if it's not recycled, initialize some attributes imageView = new RecyclingImageView(getActivity()); imageView.setLayoutParams(new GridView.LayoutParams( GridView.LayoutParams.WRAP_CONTENT, GridView.LayoutParams.WRAP_CONTENT)); imageView.setScaleType(ImageView.ScaleType.FIT_CENTER); imageView.setPadding(5, 5, 5, 5); } else { imageView = (RecyclingImageView) convertView; } MyDataObject dataItem = (MyDataObject) getItem(position); RecyclingBitmapDrawable image = lruCacheManager.getBitmapFromMemCache(dataItem.getId()); if(image != null) { // This internally is checking reference count on previous bitmap it used. imageView.setImageDrawable(image); } else { // You have to implement this method as per your code structure. // But it basically doing is preparing bitmap in the background // and adding that to LruCache. // Also it is setting the empty view till bitmap gets loaded. // once loaded it just need to call notifyDataSetChanged of adapter. loadImage(dataItem.getId(), R.drawable.empty_view); } return imageView; } 

Вот ваш RecyclingImageView.java

 /* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.cache; import android.content.Context; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.util.AttributeSet; import android.widget.ImageView; /** * Sub-class of ImageView which automatically notifies the drawable when it is * being displayed. */ public class RecyclingImageView extends ImageView { public RecyclingImageView(Context context) { super(context); } public RecyclingImageView(Context context, AttributeSet attrs) { super(context, attrs); } /** * @see android.widget.ImageView#onDetachedFromWindow() */ @Override protected void onDetachedFromWindow() { // This has been detached from Window, so clear the drawable setImageDrawable(null); super.onDetachedFromWindow(); } /** * @see android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable) */ @Override public void setImageDrawable(Drawable drawable) { // Keep hold of previous Drawable final Drawable previousDrawable = getDrawable(); // Call super to set new Drawable super.setImageDrawable(drawable); // Notify new Drawable that it is being displayed notifyDrawable(drawable, true); // Notify old Drawable so it is no longer being displayed notifyDrawable(previousDrawable, false); } /** * @see android.widget.ImageView#setImageResource(android.graphics.drawable.Drawable) */ @Override public void setImageResource(int resId) { // Keep hold of previous Drawable final Drawable previousDrawable = getDrawable(); // Call super to set new Drawable super.setImageResource(resId); // Notify old Drawable so it is no longer being displayed notifyDrawable(previousDrawable, false); } /** * Notifies the drawable that it's displayed state has changed. * * @param drawable * @param isDisplayed */ private static void notifyDrawable(Drawable drawable, final boolean isDisplayed) { if (drawable instanceof RecyclingBitmapDrawable) { // The drawable is a CountingBitmapDrawable, so notify it ((RecyclingBitmapDrawable) drawable).setIsDisplayed(isDisplayed); } else if (drawable instanceof LayerDrawable) { // The drawable is a LayerDrawable, so recurse on each layer LayerDrawable layerDrawable = (LayerDrawable) drawable; for (int i = 0, z = layerDrawable.getNumberOfLayers(); i < z; i++) { notifyDrawable(layerDrawable.getDrawable(i), isDisplayed); } } } } 

Вот ваш RecyclingBitmapDrawable.java

 /* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.cache; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.util.Log; /** * A BitmapDrawable that keeps track of whether it is being displayed or cached. * When the drawable is no longer being displayed or cached, * {@link android.graphics.Bitmap#recycle() recycle()} will be called on this drawable's bitmap. */ public class RecyclingBitmapDrawable extends BitmapDrawable { static final String TAG = "CountingBitmapDrawable"; private int mCacheRefCount = 0; private int mDisplayRefCount = 0; private boolean mHasBeenDisplayed; public RecyclingBitmapDrawable(Resources res, Bitmap bitmap) { super(res, bitmap); } /** * Notify the drawable that the displayed state has changed. Internally a * count is kept so that the drawable knows when it is no longer being * displayed. * * @param isDisplayed - Whether the drawable is being displayed or not */ public void setIsDisplayed(boolean isDisplayed) { //BEGIN_INCLUDE(set_is_displayed) synchronized (this) { if (isDisplayed) { mDisplayRefCount++; mHasBeenDisplayed = true; } else { mDisplayRefCount--; } } // Check to see if recycle() can be called checkState(); //END_INCLUDE(set_is_displayed) } /** * Notify the drawable that the cache state has changed. Internally a count * is kept so that the drawable knows when it is no longer being cached. * * @param isCached - Whether the drawable is being cached or not */ public void setIsCached(boolean isCached) { //BEGIN_INCLUDE(set_is_cached) synchronized (this) { if (isCached) { mCacheRefCount++; } else { mCacheRefCount--; } } // Check to see if recycle() can be called checkState(); //END_INCLUDE(set_is_cached) } private synchronized void checkState() { //BEGIN_INCLUDE(check_state) // If the drawable cache and display ref counts = 0, and this drawable // has been displayed, then recycle if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed && hasValidBitmap()) { Log.d(TAG, "No longer being used or cached so recycling. " + toString()); getBitmap().recycle(); } //END_INCLUDE(check_state) } private synchronized boolean hasValidBitmap() { Bitmap bitmap = getBitmap(); return bitmap != null && !bitmap.isRecycled(); } }