2

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

Martin Lau
  • 219
  • 3
  • 13

2 Answers2

3

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.

Martin Lau
  • 219
  • 3
  • 13
1

The singleton pattern is the evil :) Please avoid it completely and use a fragment with setReteainInstance(true) as explained here

greywolf82
  • 21,813
  • 18
  • 54
  • 108
  • 1
    Thanks a lot greywolf! I will try this, but somehow, is there a way to have a global cache? If I use a fragment with the cache, then there will be 2 caches needed (as I have 2 fragements) and many bitmaps will be cached twice. that is what I want to avoid. – Martin Lau Dec 29 '14 at 19:24
  • I changed the singleton to a normal class and pass its instance across the fragments. still have the same issue. – Martin Lau Dec 29 '14 at 21:09
  • 1
    You could extend the Application class and put your singleton there as alternative. Could you post the part of code where you call cacheImage? – greywolf82 Dec 30 '14 at 08:11
  • Hi greywolf82, thanks for your response. as mentioned in above update, now I tried to declare the cache instance (LruCache) in my two fragments and the main activity, then pass the cache instance to the adapter object, but still the same issue. I am going to try to have the cache per fragment to see if this will work. – Martin Lau Dec 30 '14 at 14:15
  • It seems like some issue with the CursorAdapter. For the two fragments I have 2 different adapters: ArrayAdapter and CursorAdapter. There is no issue with the cache in the ArrayAdapter. I am checking what the exact issue is. But can you please tell why singleton won't work in this case, thank you! – Martin Lau Dec 30 '14 at 14:59
  • I can't help you without the code. The singleton in Android doesn't work as usual because the OS can unload your class at any time, static variables and singleton should not be used. Indeed the docs says to use a fragment with retain instance. – greywolf82 Dec 30 '14 at 15:16
  • HI greywolf82, thanks! and I have put the code of the 2 adapters in the post now. so actually now I pass the LruCache instance from the fragment to the adapter objects. and the one with NewsTopicItemAdapter works fine, but the another one with NewsCursorAdapter has the issue that sometimes the bitmap cannot be displayed. the bitmap objects from cache are not null and not recycled, but some are not displayed. – Martin Lau Dec 30 '14 at 17:02
  • I have added the log when the bitmap is retrieved from the cache, and I can clearly see that the length of the bitmap is valid: 120. so it seems that the bitmap from cache is OK, but somehow it cannot be displayed in the ImageView object. – Martin Lau Dec 30 '14 at 17:17