Search code examples
androidandroid-bitmapandroid-lru-cache

Android: LruCache issue with Bitmap


I have a global bitmap cache using LruCache class. when loading thumbnails for the listview, the cache is used first. It works just OK.

But one issue is: sometimes the Bitmap instance from the cache cannot be displayed on the listview. it seems such bitmap from cache is not valid any more. I have checked the bitmap from cache if it is not null and if it is not recycled, but it still seems such bitmap cannot be displayed (even it is not null and it is not recycled).

The cache class:

public class ImageCache {

    private LruCache<String, Bitmap> mMemoryCache;

    private static ImageCache instance;

    public static ImageCache getInstance() {
        if(instance != null) {
            return instance;
        }

        instance = new ImageCache();
        instance.initializeCache();

        return instance;
   }

   protected void initializeCache() {

        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

        // Use 1/8th of the available memory for this memory cache.
        final int cacheSize = maxMemory / 8;

        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {

            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                // The cache size will be measured in kilobytes rather than
                // number of items.
                return bitmap.getByteCount() / 1024;
            }
        };

    }

    public Bitmap getImage(String url) {
        return this.mMemoryCache.get(url);
    }


    public void cacheImage(String url, Bitmap image) {
        this.mMemoryCache.put(url, image);
    }
}

and the code to use the cache is in the Adapter class which is subclass of CursorAdapter:

        final ImageCache cache = ImageCache.getInstance();

        // First get from memory cache
        final Bitmap bitmap = cache.getImage(thumbnailUrl);
        if (bitmap != null && !bitmap.isRecycled()) {
            Log.d(TAG, "The bitmap is valid");
            viewHolder.imageView.setImageBitmap(bitmap);
        } 
        else {
            Log.d(TAG, "The bitmap is invalid, reload it.");
            viewHolder.imageView.setImageResource(R.drawable.thumbnail_small);

            // use the AsyncTask to download the image and set in cache
            new DownloadImageTask(context, viewHolder.imageView, thumbnailUrl, dir, filepath).execute();
        }   

the code of DownloadImageTask:

public class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {

    private ImageView mImageView;
    private String url;
    private String dir;
    private String filename;
    private Context context;

    public DownloadImageTask(Context context, ImageView imageView, String url, String dir, String filename) {
        this.mImageView = imageView;
        this.url = url;
        this.filename = filename;
        this.dir = dir;
        this.context = context;
        this.cache = cache;
    }

    protected Bitmap doInBackground(String... urls) {
        // String urldisplay = urls[0];
        final Bitmap bitmap = FileUtils.readImage(context, dir, filename, url);

        return bitmap;
    }

    protected void onPostExecute(Bitmap result) {
        final ImageCache cache = ImageCache.getInstance();
        if(result != null) {
            cache.put(url, result);
            mImageView.setImageBitmap(result);
        }

    }
}

any help will be appreciated. Thanks!

Updates: I have followed the link suggested by greywolf82: section "Handle Configuration Changes". I put the following attribute in my activity class and the two fragment classes:

public LruCache mMemoryCache;

In the activity class, I try to initialize the cache when calling the fragment:

        // Get the cache
        mMemoryCache = mIndexFragment.mRetainedCache;
        if (mMemoryCache == null) {
            final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

            // Use 1/8th of the available memory for this memory cache.
            final int cacheSize = maxMemory / 8;

            // Initialize the cache
            mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {

                @Override
                protected int sizeOf(String key, Bitmap bitmap) {
                    // The cache size will be measured in kilobytes rather than
                    // number of items.
                    return bitmap.getByteCount() / 1024;
                }
            };

            Log.d(TAG, "Initialized the memory cache");
            mIndexFragment.mRetainedCache = mMemoryCache;
        }

in the fragment class: setRetainInstance(true);

and I pass the cache instance to the adapter constructor so that the adapter can use the cache.

but I still got the same issue.

Update 2:

the two adapter classes with changes to accept the LruCache instance:

NewsCursorAdapter:

public class NewsCursorAdapter extends CursorAdapter {

    private static final String TAG = "NewsCursorAdapter";

    private LruCache<String, Bitmap> cache;

    private Context mContext;

    public NewsCursorAdapter(Context context, LruCache<String, Bitmap> cache) {
        super(context, null, false);
        this.mContext = context;
        this.cache = cache;
    }

    @Override
    public void bindView(View view, Context context, Cursor cursor) {

        final Setting setting = ApplicationContext.getSetting();
        // Get the view holder
        ViewHolder viewHolder = (ViewHolder) view.getTag();

        final String thumbnail = cursor.getString(NewsContract.Entry.THUMBNAIL_CURSOR_INDEX);
        if(thumbnail != null) {
            String pictureDate = cursor.getString(NewsContract.Entry.PIC_DATE_CURSOR_INDEX);
            final String dir = "thumbnails/" + pictureDate + "/";
            final String filepath = thumbnail + "-small.jpg";
            final String thumbnailUrl = setting.getCdnUrl() + dir + filepath;

            //final ImageCache cache = ImageCache.getInstance();

            // First get from memory cache
            final Bitmap bitmap = cache.get(thumbnailUrl);
            if (bitmap != null && !bitmap.isRecycled()) {
                Log.d(TAG, "The bitmap is valid: " + bitmap.getWidth());
                viewHolder.imageView.setImageBitmap(bitmap);
            } 
            else {
                Log.d(TAG, "The bitmap is invalid, reload it.");
                viewHolder.imageView.setImageResource(R.drawable.thumbnail_small);

                new DownloadImageTask(viewHolder.imageView, thumbnailUrl, dir, filepath).execute();
            }   
        }
        else {
            viewHolder.imageView.setVisibility(View.GONE);
        }
    }

    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent) {

        LayoutInflater inflater = (LayoutInflater) context
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        View view = inflater.inflate(R.layout.listview_item_row, parent,
                false);
        // Initialize the view holder
        ViewHolder viewHolder = new ViewHolder();

        viewHolder.titleView = (TextView) view.findViewById(R.id.title);
        viewHolder.timeView = (TextView) view.findViewById(R.id.news_time);
        viewHolder.propsView = (TextView) view.findViewById(R.id.properties);
        viewHolder.imageView = (ImageView) view.findViewById(R.id.icon);
        view.setTag(viewHolder);

        return view;
    }

    static class ViewHolder {
          TextView titleView;
          TextView timeView;
          TextView propsView;
          ImageView imageView;

    }

    private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
        private ImageView mImageView;
        private String url;
        private String dir;
        private String filename;

        public DownloadImageTask(ImageView imageView, String url, String dir, String filename) {
            this.mImageView = imageView;
            this.url = url;
            this.filename = filename;
            this.dir = dir;
        }

        protected Bitmap doInBackground(String... urls) {

            final Bitmap bitmap = FileUtils.readImage(mContext, dir, filename, url);
            return bitmap;
        }

        protected void onPostExecute(Bitmap result) {
            //final ImageCache cache = ImageCache.getInstance();
            if(result != null) {
                cache.put(url, result);
                mImageView.setImageBitmap(result);
            }

        }
    }
}

the list adapter, NewsTopicItemAdapter:

public class NewsTopicItemAdapter extends ArrayAdapter<NewsTopicItem> {

    private Context context = null;

    private EntryViewHolder viewHolder;

    private HeaderViewHolder headerViewHolder;

    private LruCache<String, Bitmap> mCache;

    public NewsTopicItemAdapter(Context context, List<NewsTopicItem> arrayList, LruCache<String, Bitmap> cache) {
        super(context, 0, arrayList);
        this.context = context;
        this.mCache = cache;
    }

    public void setItems(List<NewsTopicItem> items) {
        this.addAll(items);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        final NewsTopicItem item = getItem(position);
        View view;
        if(!item.isHeader()) {
            view = this.getEntryView((NewsTopicEntry)item, convertView, parent);
        }
        else {
            view = this.getHeaderView((NewsTopicHeader)item, convertView, parent);
        }

        return view;
    }

    protected View getEntryView(NewsTopicEntry newsItem, View convertView, ViewGroup parent) {

        View view;

            LayoutInflater inflater = (LayoutInflater) context
                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            viewHolder = new EntryViewHolder();

            view = inflater.inflate(R.layout.listview_item_row, parent,
                        false);
            // Initialize the view holder
            viewHolder.titleView = (TextView) view.findViewById(R.id.title);
            viewHolder.timeView = (TextView) view.findViewById(R.id.news_time);
            viewHolder.propsView = (TextView) view.findViewById(R.id.properties);
            viewHolder.imageView = (ImageView) view.findViewById(R.id.icon);
            view.setTag(viewHolder);

        viewHolder.propsView.setText(newsItem.getSource());

        if (newsItem.getThumbnail() != null) {

            final String dir = "thumbnails/" + newsItem.getPictureDate() + "/";
            final String filepath = newsItem.getThumbnail() + "-small.jpg";
            final String thumbnailUrl = "http://www.oneplusnews.com/static/" + dir + filepath;

            //final ImageCache cache = ImageCache.getInstance();

            // First get from memory cache
            final Bitmap bitmap = mCache.get(thumbnailUrl);
            if (bitmap != null && !bitmap.isRecycled()) {
                viewHolder.imageView.setImageBitmap(bitmap);
            } else {
                viewHolder.imageView.setImageResource(R.drawable.thumbnail_small);

                new DownloadImageTask(viewHolder.imageView, thumbnailUrl, dir, filepath).execute();
            }           
        }
        else {
            viewHolder.imageView.setVisibility(View.GONE);
        }

        viewHolder.titleView.setText(newsItem.getTitle());
        viewHolder.timeView.setText(DateUtils.getDisplayDate(newsItem.getCreated()));

        return view;

    }

    protected View getHeaderView(NewsTopicHeader header, View convertView, ViewGroup parent) {

        View view;


            LayoutInflater inflater = (LayoutInflater) context
                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            headerViewHolder = new HeaderViewHolder();

            view = inflater.inflate(R.layout.news_list_header, parent,
                        false);
            // Initialize the view holder
            headerViewHolder.topicView = (TextView) view.findViewById(R.id.topic);

            view.setTag(headerViewHolder);
            final View imageView = view.findViewById(R.id.more_icon);
            imageView.setOnClickListener(new OnClickListener() {
                public void onClick(View v) {
                    // Start the Fragement
                }
            });

        Topic topic = header.getTopic();
        if(topic.isKeyword()) {
            headerViewHolder.topicView.setText(topic.getName());
        }
        else {
            // This is a hack to avoid error with - in android
            headerViewHolder.topicView.setText(ResourceUtils.getStringByName(context, topic.getName()));
        }

        return view;

    }


    private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
        private ImageView mImageView;
        private String url;
        private String dir;
        private String filename;

        public DownloadImageTask(ImageView imageView, String url, String dir, String filename) {
            this.mImageView = imageView;
            this.url = url;
            this.filename = filename;
            this.dir = dir;
        }

        protected Bitmap doInBackground(String... urls) {

            final Bitmap mIcon11 = FileUtils.readImage(context, dir, filename, url);
            return mIcon11;
        }

        protected void onPostExecute(Bitmap result) {
            //final ImageCache cache = ImageCache.getInstance();
            if(result != null) {
                mCache.put(url, result);
                mImageView.setImageBitmap(result);
            }

        }
    }



    static class EntryViewHolder {
          TextView titleView;
          TextView timeView;
          TextView propsView;
          ImageView imageView;
          TextView topicView;
    }

    static class HeaderViewHolder {
          TextView topicView;
    }
}

Update 3: I have attached the debug information from eclipse: the 1st picture is the working bitmap, and the 2nd one is the non-working bitmap from cache. I didn't find anything suspicious.

The debug information of the working bitmap from the cache:

The debug information of the working bitmap from the cache

The debug information of the non-working bitmap from the cache:

The debug information of the non-working bitmap from the cache


Solution

  • Finally I figured out the problem. It is becuase of the adapter. in the adapter I have set some ImageView as invisible if no thumbnail is needed. when the user scrolls the list view, such ImageView instance will be reused, but the visibility is not updated.

    so the cache itself is OK now. The solution is to check the visibility of the ImageView and update it if needed.

    Anyway thanks a lot to greywolf82 for your time and the tip about the singleton pattern.