Search code examples
androidlistviewbitmaprecycle

ListView recycles Bitmap without noticing in lrucache


I'm writing a gallery app. It works from the androidstudio template for list fragment, with an AbsList.

I override getView to use a task and an lrucache to cache some bitmaps.

Each view from the listview is a RelativeLayout with an ImageView above a TextView. If there is no Bitmap in the cache, then an AsyncTask loads it and puts it into the cache and getView draws a resource on the ImageView. After it is loaded, onPostExecute puts the bitmap into the ImageView.

If there is a corresponding Bitmap on the cache, the it is set into the ImageView

I set an object holding the TextView and the ImageView along with an id into the getView's convertView parameter tag to keep track of the correct Bitmap to draw.

I have these two problems, though:

  1. When I scroll down the first time, the new Image views appear with a previous bitmap for an instant before the task finishes setting up the correct bitmap (even though I draw a resource Bitmap on the adapter's getView) I don't understand why.

  2. When I scroll back, most times the app crashes because the Bitmap on the cache turns out to be recycled, though I have no idea who recycled it.

Can anyone help me understand what happens here?

public View getView(int position, View convertView, ViewGroup parent) {
            Log.i(TAG, "getView: Asking for view " + position);
            GalleryItemViewHolder lViewHolder;
            if (convertView == null) {
                convertView = getActivity().getLayoutInflater().inflate(R.layout
                        .gallery_item, null);
                lViewHolder = new GalleryItemViewHolder();
                convertView.setTag(lViewHolder);

            } else {
                lViewHolder = (GalleryItemViewHolder) convertView.getTag();
            }
            lViewHolder.setId(position);
            lViewHolder.setTextView((TextView) convertView.findViewById(R.id.gallery_infoTextView));
            lViewHolder.setImageView((ImageView) convertView.findViewById(R.id.gallery_imageView));

            lViewHolder.getTextView().setText(getItem(position).getName() + ": (" + getItem
                    (position).getCount() + ")");
            if (!getItem(position).paintCover(lViewHolder.getImageView())) {
                Log.i(TAG,"getView: task");
                new GalleryItemTask(position, lViewHolder)
                        .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null);
            }
            Log.i(TAG,"getView: return");
            return convertView;
        }

The Cover class has this paintCover method, where the mId is the uri/stream to the image

public boolean paintCover(ImageView imageView) {
    Bitmap lBitmap;
    if (mId == null || (lBitmap = BitmapCacheManager.getInstance().get(mId)) == null) {
        i(TAG, "paintCover: Sin Cache ");
        imageView.getHeight();
        imageView.getWidth();
        imageView.setImageResource(android.R.drawable.alert_dark_frame);
        return false;

    } else

    {
        i(TAG, "paintCover: En Cache "+lBitmap.isRecycled());
        imageView.setImageBitmap(lBitmap);
        return true;
    }

}

More detail. At the Fragment's onCreate, I run this method:

private void prepareGalleryLoaders() {
    LoaderManager lm = getLoaderManager();
    Log.i(TAG, "prepareGalleryLoaders: Iniciando loader");
    lm.initLoader(IdConstants.GALLERY_LOADER, null, new GalleryLoaderCallbacks());
}



/**
 * Callbacks para cargar los datos de las galerías
 * Al terminar de cargarlas, se crea el nuevo arreglo
 */
private class GalleryLoaderCallbacks implements LoaderManager.LoaderCallbacks<List<Gallery>> {
    @Override
    public Loader<List<Gallery>> onCreateLoader(int id, Bundle args) {

        return new GalleriesLoader(getActivity());
    }private class GalleryItemTask extends AsyncTask<Void, Void, Gallery> {
    private static final String TAG = "GalleryItemTask";
    private int mId;
    private String mCoverId;
    private GalleryItemViewHolder mViewHolder;
    private Bitmap mBitmap;

    GalleryItemTask(int id, GalleryItemViewHolder galleryItemViewHolder) {
        mViewHolder = galleryItemViewHolder;
        mId = id;
    }



    @Override
    protected void onPostExecute(Gallery galleries) {

        if (mId != mViewHolder.getId()) {
            Log.i(TAG, "onPostExecute: IDs difieren!!! "+mId+" - "+mViewHolder.getId());
            mBitmap.recycle();
            mBitmap=null;
            return;
        }
        // Validar y actualizar bitmap


        mViewHolder.getImageView().setImageBitmap(mBitmap);
        //mGalleries.get(mId).setBitmap(mBitmap);

        super.onPostExecute(galleries);
    }

    @Override
    protected Gallery doInBackground(Void... params) {
        // generar bitmap (y posiblemente agregarlo a algún cache)


        String[] queryProjection = {
                MediaStore.Images.ImageColumns.DATA, MediaStore.Images.ImageColumns.TITLE};
        String[] selectionArgs = new String[]{String.valueOf(mGalleries.get(mId).getId())};
        Cursor lCursor = getView().getContext().getContentResolver().query(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                queryProjection, MediaStore.Images.ImageColumns.BUCKET_ID + "= ?",
                selectionArgs, MediaStore.Images.ImageColumns.TITLE);
        lCursor.moveToFirst();
        while (!lCursor.isAfterLast()) {
            //Log.i(TAG,"doInBackground: "+mGalleries.get(mId).getName()+" - "+lCursor.getString
            //        (1)+" - "+ lCursor.getString(0));
            lCursor.moveToNext();
        }
        lCursor.moveToFirst();
        Log.i(TAG, "doInBackground: " + mId + " - " + mViewHolder.getId());

        BitmapFactory.Options lOptions = new BitmapFactory.Options();
        lOptions.inJustDecodeBounds = true;
        mBitmap = BitmapFactory.decodeFile(lCursor.getString(0), lOptions);
        lOptions.inSampleSize = ImageUtils.calculateInSampleSize(lOptions, 256, 256);
        lOptions.inJustDecodeBounds = false;
        mBitmap = BitmapFactory.decodeFile(lCursor.getString(0), lOptions);

        BitmapCacheManager.getInstance().put(lCursor.getString(0), mBitmap);


        //if(mGalleries.get(mId).getBitmap()!=null)
        //    mGalleries.get(mId).getBitmap().recycle();
        //mGalleries.get(mId).setBitmap(mBitmap);



        if(!mGalleries.get(mId).hasCover()) {
            SimpleCover lSimpleCover=new SimpleCover(getActivity(),lCursor.getString(0));
            mGalleries.get(mId).setCover(lSimpleCover);
        }
        lCursor.close();
        return null;
    }
}


    @Override
    public void onLoadFinished(Loader<List<Gallery>> loader, List<Gallery> data) {
        if (mGalleries != null) {
            mGalleries.clear();

        } else
            mGalleries = new ArrayList<Gallery>();
        mGalleries.addAll(data);
        for (Gallery lGallery : data) {
            Log.i(TAG, "onLoadFinished: " + lGallery.getName());
        }

        mAdapter.notifyDataSetChanged();
    }

At this point, there are no covers defined, the gallery list is just loaded with titles and total contents and id data. The images (covers) are loaded at getView from the list adapter.

The GalleryItemTask class:

 private class GalleryItemTask extends AsyncTask<Void, Void, Gallery> {
    private static final String TAG = "GalleryItemTask";
    private int mId;
    private String mCoverId;
    private GalleryItemViewHolder mViewHolder;
    private Bitmap mBitmap;

    GalleryItemTask(int id, GalleryItemViewHolder galleryItemViewHolder) {
        mViewHolder = galleryItemViewHolder;
        mId = id;
    }



    @Override
    protected void onPostExecute(Gallery galleries) {

        if (mId != mViewHolder.getId()) {
            Log.i(TAG, "onPostExecute: IDs difieren!!! "+mId+" - "+mViewHolder.getId());
            mBitmap.recycle();
            mBitmap=null;
            return;
        }
        // Validar y actualizar bitmap


        mViewHolder.getImageView().setImageBitmap(mBitmap);
        //mGalleries.get(mId).setBitmap(mBitmap);

        super.onPostExecute(galleries);
    }

    @Override
    protected Gallery doInBackground(Void... params) {
        // generar bitmap (y posiblemente agregarlo a algún cache)


        String[] queryProjection = {
                MediaStore.Images.ImageColumns.DATA, MediaStore.Images.ImageColumns.TITLE};
        String[] selectionArgs = new String[]{String.valueOf(mGalleries.get(mId).getId())};
        Cursor lCursor = getView().getContext().getContentResolver().query(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                queryProjection, MediaStore.Images.ImageColumns.BUCKET_ID + "= ?",
                selectionArgs, MediaStore.Images.ImageColumns.TITLE);
        lCursor.moveToFirst();
        while (!lCursor.isAfterLast()) {
            //Log.i(TAG,"doInBackground: "+mGalleries.get(mId).getName()+" - "+lCursor.getString
            //        (1)+" - "+ lCursor.getString(0));
            lCursor.moveToNext();
        }
        lCursor.moveToFirst();
        Log.i(TAG, "doInBackground: " + mId + " - " + mViewHolder.getId());

        BitmapFactory.Options lOptions = new BitmapFactory.Options();
        lOptions.inJustDecodeBounds = true;
        mBitmap = BitmapFactory.decodeFile(lCursor.getString(0), lOptions);
        lOptions.inSampleSize = ImageUtils.calculateInSampleSize(lOptions, 256, 256);
        lOptions.inJustDecodeBounds = false;
        mBitmap = BitmapFactory.decodeFile(lCursor.getString(0), lOptions);

        BitmapCacheManager.getInstance().put(lCursor.getString(0), mBitmap);


        //if(mGalleries.get(mId).getBitmap()!=null)
        //    mGalleries.get(mId).getBitmap().recycle();
        //mGalleries.get(mId).setBitmap(mBitmap);



        if(!mGalleries.get(mId).hasCover()) {
            SimpleCover lSimpleCover=new SimpleCover(getActivity(),lCursor.getString(0));
            mGalleries.get(mId).setCover(lSimpleCover);
        }
        lCursor.close();
        return null;
    }
}

Solution

  • When I scroll down the first time, the new Image views appear with a previous bitmap for an instant before the task finishes setting up the correct bitmap (even though I draw a resource Bitmap on the adapter's getView) I don't understand why.

    It should be because you put notifyDataSetChanged() on the wrong place. Please post the code where you put it.

    When I scroll back, most times the app crashes because the Bitmap on the cache turns out to be recycled, though I have no idea who recycled it.

    I think its because you don't specify what to do if the paintCover is true :

    if (!getItem(position).paintCover(lViewHolder.getImageView())) {
           Log.i(TAG,"getView: task");
           new GalleryItemTask(position, lViewHolder)
                   .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null);
    }
    else
    {
       //what should the adapter do if paintCover is true?
    }
    

    If the error still exist, please post your GalleryItemTask code.