• After 15+ years, we've made a big change: Android Forums is now Early Bird Club. Learn more here.

Can recycling and reloading bitmaps from resources cause memory leaks?

My game was complete and ready to launch until I realized it didn't work on older phones (namely, anything before API level 23 or so had issues). I needed to do some memory management.

So, I started recycling bitmaps that weren't currently being used (and weren't going to be used in the immediate future, except through the click of a menu option). That allowed the game to run on API's as low as 19. 17 and 18 sometimes works and sometimes doesn't, which doesn't make any sense to me. It should either run the whole time or not at all. It occasionally has an OutOfMemory error with all the "GC_" messages. I'm guessing there must be a memory leak.

Any ideas? The following shows how I manage my memory:

Here's the Async bitmap loader class:

Code:
public static class BitmapDecodeTask extends AsyncTask<Void, Void, Bitmap>
{
    //the reason to use a weak reference is to protect from memory leak issues.
    private static WeakReference<Context> mContextReference;
    private static WeakReference<Tools> mToolsReference;
    private int bm;

    public BitmapDecodeTask(final int bm)
    {
        this.bm = bm;
    }

    @Override
    protected Bitmap doInBackground(Void... params)
    {
        Context context = mContextReference.get();
        Tools tools = mToolsReference.get();
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(context.getResources(), bm, options);
        final int width = tools.getRelX(options.outWidth);
        final int height = tools.getRelY(options.outHeight);
        return Bitmap.createScaledBitmap(decodeSampledBitmapFromResource(context.getResources(), bm, width, height), width, height, false);
    }

    private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {

        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {
            // Calculate ratios of height and width to requested height and width
            final int heightRatio = Math.round((float) height / (float) reqHeight);
            final int widthRatio = Math.round((float) width / (float) reqWidth);

            // Choose the smallest ratio as inSampleSize value, this will guarantee
            // a final image with both dimensions larger than or equal to the
            // requested height and width.
            inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
        }
        return inSampleSize;
    }

    private static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
                                                          int reqWidth, int reqHeight) {

        // First decode with inJustDecodeBounds=true to check dimensions
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);

        // Calculate inSampleSize
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

        // Decode bitmap with inSampleSize set
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }

    public static void setContext(final Context context)
    {
        mContextReference = new WeakReference<>(context);
    }

    public static void setTools(final Tools tools)
    {
        mToolsReference = new WeakReference<>(tools);
    }
}

The first method here is where I load a set of specific sprites before I need them, and the following method is where I scrap them:
Code:
public static void loadBitmapsHillaryBody()
{
    if (spritesHillaryBodyWalkRight != null) return;
    try
    {
        Bitmap[] sprites = new Bitmap[Num.I_8];
        Bitmap bitmap = new Tools.BitmapDecodeTask(R.drawable.hillary_body_walk_right).execute().get();
        for (int i = Num.I_0; i < Num.I_8; i++)
        {
            sprites[i] = Bitmap.createBitmap(bitmap, i * bitmap.getWidth() / Num.I_8, Num.I_0, bitmap.getWidth() / Num.I_8, bitmap.getHeight());
        }
        spritesHillaryBodyWalkRight = sprites;
        sprites = Tools.flipSprites(sprites);
        spritesHillaryBodyWalkLeft = sprites;
        sprites = new Bitmap[Num.I_5];
        bitmap = new Tools.BitmapDecodeTask(R.drawable.hillary_body_turn_left).execute().get();
        for (int i = Num.I_0; i < Num.I_5; i++)
        {
            sprites[i] = Bitmap.createBitmap(bitmap, i * bitmap.getWidth() / Num.I_5, Num.I_0, bitmap.getWidth() / Num.I_5, bitmap.getHeight());
        }
        spritesHillaryBodyTurnLeft = sprites;
        sprites = Tools.flipSprites(sprites);
        spritesHillaryBodyTurnRight = sprites;
    }
    catch (final ExecutionException exc)
    {
        exc.printStackTrace();
    }
    catch (final InterruptedException exc)
    {
        exc.printStackTrace();
    }
}

public static void scrapBitmaps()
{
    Tools.scrapBitmaps(spritesHillaryBodyTurnLeft);
    spritesHillaryBodyTurnLeft = null;
    Tools.scrapBitmaps(spritesHillaryBodyTurnRight);
    spritesHillaryBodyTurnRight = null;
    Tools.scrapBitmaps(spritesHillaryBodyWalkLeft);
    spritesHillaryBodyWalkLeft = null;
    Tools.scrapBitmaps(spritesHillaryBodyWalkRight);
    spritesHillaryBodyWalkRight = null;
}

This is the scrampBitmaps method in Tools:
Code:
public static void scrapBitmaps(Bitmap[] bitmaps)
{
    if (bitmaps != null)
    {
        for (int i = Num.I_0; i < bitmaps.length; i++)
        {
            bitmaps[i].recycle();
        }
    }
}
 
Have you running the profiler on it?
No, I didn't. I don't know how to work the profiler. Never learned it. I think I figured out my problem, though. At least the one that was causing this immediate memory issue. Sometimes I was loading bitmaps that were already loaded. I knew I may have been doing this, which is why I checked if they were null first (if not, then don't re-load). I have multiple static bitmap loading methods and I forgot to do the check on one of them, and that was causing the OOM.

Sometimes I request to load bitmaps that are already loaded due to the app being paused and resumed, and sometimes resumed from somewhere in the middle of gameplay, which is why I do the check.

I'm still cutting it really closed on SDK 17-19. You can see the "GC_" warnings pop up like crazy. I'd still like to know if there's anything I can do to cut memory usage down. I'm not even using that much IMO.
 
Well the profiler would tell you where memory was being consumed, and that's the first step to systematically resolving these types of problem.
As for cutting down on memory usage, that's a difficult one to answer. But your prime culprit would probably be the bitmaps, so just try to recycle those as much as possible, and avoid creating new or duplicate ones.
 
Back
Top Bottom