Question

I seem to have a memory leak and I'm not sure how to fix. I've read all of the ImageSwitcher tutorials and examples on android.com, but those all seem to deal with drawables that are already in the drawables folder. My code allows the user to take one or two photos with their camera, store the images to SD, and use an imageswitcher to "flip" the images over.

public class CardViewImageActivity extends Activity implements ViewFactory {

    private CardDBAdapter mDbHelper;

    private String _cardImgGuidFront;
    private String _cardImgGuidBack;
    private Boolean frontShowing = false;
    private Boolean hasFront = false;
    private Boolean hasBack = false;

    private Uri uriFront;
    private Uri uriBack;

    private int cardId;

    private ImageSwitcher iSwitcher;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.card_view_image);

        iSwitcher = (ImageSwitcher)findViewById(R.id.imageSwitcher);
        iSwitcher.setFactory(this);
        iSwitcher.setOnClickListener(SwitcherOnClick);

        this.cardId = Integer.parseInt(getIntent().getExtras().getString(
                "cardId")); //$NON-NLS-1$

        getCardImageGuids(this.cardId);

        if(_cardImgGuidFront != null)
        {
            hasFront = true;
            uriFront = Uri.parse(Environment.getExternalStorageDirectory().toString() + "/" + _cardImgGuidFront + ".jpg");
        }

        if(_cardImgGuidBack != null)
        {
            hasBack = true;
            uriBack = Uri.parse(Environment.getExternalStorageDirectory().toString() + "/" + _cardImgGuidBack + ".jpg");            
        }
        if(hasFront && hasBack)
            Toast.makeText(this, R.string.card_view_touch, Toast.LENGTH_SHORT).show();

        if(hasFront)
        {
            iSwitcher.setImageURI(uriFront);
            frontShowing = true;
        }
        else if(hasBack)
        {
            iSwitcher.setImageURI(uriBack);
            frontShowing = false;
        }
        else
        {
            Toast.makeText(this, R.string.card_no_image, Toast.LENGTH_SHORT).show();
        }
    }
    @Override
    public void onDestroy()
    {
        iSwitcher.setImageURI(null);
        super.onDestroy();
    }

    public View makeView() {
        ImageView iView = new ImageView(this);
        iView.setScaleType(ImageView.ScaleType.FIT_CENTER);
        iView.setLayoutParams(new ImageSwitcher.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));

        return iView;
    }


    protected OnClickListener SwitcherOnClick = new OnClickListener()
    {
        @Override
        public void onClick(View v)
        {
            if(frontShowing && hasBack)
            {
                iSwitcher.destroyDrawingCache();
                iSwitcher.setImageURI(uriBack);
                frontShowing = false;
            }
            else if(!frontShowing && hasFront)
            {
                iSwitcher.destroyDrawingCache();
                iSwitcher.setImageURI(uriFront);
                frontShowing = true;
            }
            else
            {

            }
        }       
    };


    private void getCardImageGuids(int cardId)
    {
        try
        {
            this.mDbHelper = new CardDBAdapter(this);
            this.mDbHelper.open();
            Cursor c = this.mDbHelper.fetchCard(this.cardId);


            _cardImgGuidFront = c.getString(c
                    .getColumnIndex(CardDBAdapter.CARD_IMG_GUID_FRONT));

            _cardImgGuidBack = c.getString(c
                    .getColumnIndex(CardDBAdapter.CARD_IMG_GUID_BACK));
        }
        catch(SQLiteException ex)
        {
            Toast.makeText(CardViewImageActivity.this, ex.toString(), Toast.LENGTH_LONG).show();
        }
        finally
        {
             this.mDbHelper.close();
             this.mDbHelper = null;
        }

    }
}

The above code seems to work sometimes quite well. However, I'm occasionally getting an OutOfMemory error. Now, from my understanding through debugging, makeView() seems to be getting called twice when .setFactory(this) is called and this seems to be fine since ImageSwitcher's purpose is to switch between two images. I'm wondering if there is a better way to switch the images besides SetImageUri(). I don't see anywhere I might be leaking or what might be causing the issue. I don't see anywhere that convertView might even be utilized. Is there a way to cache the images from the Uri? Are the images being reloaded each time .setImageUri() is called? Is there a way to dump that memory(or reuse)? Is this what's eating up my memory?

Not to sound disrespectful or rude, but I would really prefer help without having someone referencing the Avoiding Memory Leaks article or links to the javadocs for imageswitcher? The Avoid Memory Leaks article shows a couple of "you shouldn't do this" but never shows what you "should" do instead. I already have links to the javadocs. I'm looking for someone that can actually explain what I'm doing incorrectly and point me in a better direction with code(I learn best seeing code rather than vague abstract academic theories) rather than just regurgitating the top 3 links from a Google search. :)

Thank you for any help! :)

EDIT:10Feb2012 So I tried to load the Drawables and IMMEDIATELY received an out of memory error which is WORSE than getting the error occasionally with .setImageUri(). The following contains the mods:

private Drawable front;
private Drawable back;

@Override
    public void onCreate(Bundle savedInstanceState) {

...
        Resources res = getResources();

...
        if(_cardImgGuidFront != null)
        {
            hasFront = true;
            String frontPath = Environment.getExternalStorageDirectory().toString() + "/" + _cardImgGuidFront + ".jpg";
            front = new BitmapDrawable(res, frontPath);
        }

        if(_cardImgGuidBack != null)
        {
            hasBack = true;
            String backPath = Environment.getExternalStorageDirectory().toString() + "/" + _cardImgGuidBack + ".jpg";
            back = new BitmapDrawable(res, backPath);
        }

Looking at the SoftReference, usage requires the creation of the SoftReference using another drawable. I don't see how using SoftReference could possibly help since I am now crashing on initial load.

Était-ce utile?

La solution 3

Ok, so this seems to work and I will provide the code to save Java frustration for anyone else flustered by this. It's probably not the prettiest, but so far(knock wood) I have not seen any further Out of memory errors.

import android.app.Activity;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup.LayoutParams;
import android.widget.ImageSwitcher;
import android.widget.ImageView;
import android.widget.Toast;
import android.widget.ViewSwitcher.ViewFactory;

public class CardViewImageActivity extends Activity implements ViewFactory {

    private CardDBAdapter mDbHelper;

    private String _cardImgGuidFront;
    private String _cardImgGuidBack;
    private Boolean frontShowing = false;
    private Boolean hasFront = false;
    private Boolean hasBack = false;

    private Drawable front;
    private Drawable back;

    private int cardId;

    private ImageSwitcher iSwitcher;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.cert_view_image);

        iSwitcher = (ImageSwitcher)findViewById(R.id.imageSwitcher);
        iSwitcher.setFactory(this);
        iSwitcher.setOnClickListener(SwitcherOnClick);
        Resources res = getResources();
        BitmapFactory.Options o = new BitmapFactory.Options();
        o.inSampleSize = 2;

        this.cardId = Integer.parseInt(getIntent().getExtras().getString(
                "cardId")); //$NON-NLS-1$

        getCardImageGuids(this.cardId);

        if(_cardImgGuidFront != null)
        {
            hasFront = true;
            String frontPath = Environment.getExternalStorageDirectory().toString() + "/" + _cardImgGuidFront + ".jpg";
            front = new BitmapDrawable(res, BitmapFactory.decodeFile(frontPath, o));
        }

        if(_cardImgGuidBack != null)
        {
            hasBack = true;
            String backPath = Environment.getExternalStorageDirectory().toString() + "/" + _cardImgGuidBack + ".jpg";
            back = new BitmapDrawable(res, BitmapFactory.decodeFile(backPath, o));
        }
        if(hasFront && hasBack)
            Toast.makeText(this, R.string.card_view_touch, Toast.LENGTH_SHORT).show();

        if(hasFront)
        {
            iSwitcher.setImageDrawable(front);
            frontShowing = true;
        }
        else if(hasBack)
        {
            iSwitcher.setImageDrawable(back);
            frontShowing = false;       }
        else
        {
            Toast.makeText(this, R.string.card_no_image, Toast.LENGTH_SHORT).show();
        }
        res = null;
    }
    @Override
    public void onPause()
    {
        super.onPause();
    }
    @Override
    public void onDestroy()
    {
        front = null;
        back = null;
        super.onDestroy();
    }

    public View makeView() {
        ImageView iView = new ImageView(this);
        iView.setScaleType(ImageView.ScaleType.FIT_CENTER);
        iView.setLayoutParams(new ImageSwitcher.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));

        return iView;
    }


    protected OnClickListener SwitcherOnClick = new OnClickListener()
    {
        @Override
        public void onClick(View v)
        {
            if(frontShowing && hasBack)
            {
                iSwitcher.setImageDrawable(back);
                frontShowing = false;
            }
            else if(!frontShowing && hasFront)
            {
                iSwitcher.setImageDrawable(front);
                frontShowing = true;
            }
            else
            {

            }
        }       
    };


    private void getCardImageGuids(int cardId)
    {
        ...
        // Put your db logic retrieval for the img id here

    }
}

I hope this solution (and ACTUAL code) helps someone else.

Autres conseils

ImageView v = (ImageView)imageSwitcher.getNextView(); 
BitmapDrawable bd = (BitmapDrawable) v.getDrawable();
if (bd != null) 
{
    Bitmap b = bd.getBitmap();
    b.recycle();
}

I'm wondering if there is a better way to switch the images besides SetImageUri().

Call setImageDrawable() using a cached BitmapDrawable.

I don't see anywhere I might be leaking or what might be causing the issue.

Use DDMS and MAT to see where your leaks are.

I don't see anywhere that convertView might even be utilized.

Considering that there is nothing named convertView in your source code, this is not surprising.

Is there a way to cache the images from the Uri?

Yes. Use BitmapFactory, load the images yourself. Cache the results, preferably using SoftReferences. Call recycle() on the Bitmap objects when you no longer need them.

Are the images being reloaded each time .setImageUri() is called?

Yes.

Is there a way to dump that memory(or reuse)?

Not if you have Android create the Bitmap for you. ImageSwitcher seems to be a one-way API, where you can set images but not retrieve them.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top