Jack Sierkstra
Jack Sierkstra

Reputation: 1434

Android memory handling with Bitmap in Xamarin.Android

Fellow developer, i seek your help.

I have a problem which is memory related. I don't really know how to tackle this so i will just present my code snippets. Bear in mind that, although it is on Xamarin.Android, it will also apply to normal Android.

I use a library which just starts a camera intent. The library (or component) i am using is : http://components.xamarin.com/view/xamarin.mobile. That is not really relevant, but maybe you can point me to other insights why i should or shouldn't be using this library.

Anyway, to start the camera and capture the input. I use the following code :

    private void StartCamera() {
        var picker = new MediaPicker (this);

        if (! picker.IsCameraAvailable) {

            Toast.MakeText (this, "No available camera found!", ToastLength.Short).Show ();

        } else {
            var intent = picker.GetTakePhotoUI (new StoreCameraMediaOptions{
                Name = "photo.jpg",
                Directory = "photos"
            });

            StartActivityForResult (intent, 1);

        }
    }

The onActivityForResult() method is called when i return from this camera intent. In this method, i do the following :

    protected override async void OnActivityResult (int requestCode, Result resultCode, Intent data)
    {
        // User canceled
        if (resultCode == Result.Canceled)
            return;

        System.GC.Collect ();

        dialog = new ProgressDialog (this);
        dialog.SetProgressStyle (ProgressDialogStyle.Spinner);
        dialog.SetIconAttribute (Android.Resource.Attribute.DialogIcon);
        dialog.SetTitle (Resources.GetString(Resource.String.dialog_picture_sending_title));
        dialog.SetMessage (Resources.GetString(Resource.String.dialog_picture_sending_text));
        dialog.SetCanceledOnTouchOutside (false);
        dialog.SetCancelable (false);
        dialog.Show ();

        MediaFile file = await data.GetMediaFileExtraAsync (this);

        await ConvertingAndSendingTask ( file );

        dialog.Hide();

        await SetupView ();

    }

Then, in my ConvertingAndSendingTask() i convert the picture into the desired dimensions with a scaled bitmap. The code is as follows :

public async Task ConvertingAndSendingTask(MediaFile file) {

        try{

            System.GC.Collect();

            int targetW = 1600;
            int targetH = 1200;

            BitmapFactory.Options options = new BitmapFactory.Options();
            options.InJustDecodeBounds = true;
            Bitmap b = BitmapFactory.DecodeFile (file.Path, options);

            int photoW = options.OutWidth;
            int photoH = options.OutHeight;

            int scaleFactor = Math.Min(photoW/targetW, photoH/targetH);

            options.InJustDecodeBounds = false;
            options.InSampleSize       = scaleFactor;
            options.InPurgeable        = true;

            Bitmap bitmap = BitmapFactory.DecodeFile(file.Path, options);

            float resizeFactor = CalculateInSampleSize (options, 1600, 1200);

            Bitmap bit = Bitmap.CreateScaledBitmap(bitmap, (int)(bitmap.Width/resizeFactor),(int)(bitmap.Height/resizeFactor), false);

            bitmap.Recycle();

            System.GC.Collect();

            byte[] data = BitmapToBytes(bit);

            bit.Recycle();

            System.GC.Collect();

            await app.api.SendPhoto (data, app.ChosenAlbum.foreign_id);

            bitmap.Recycle();

            System.GC.Collect();


        } catch(Exception e) {

            System.Diagnostics.Debug.WriteLine (e.StackTrace);

        }




    }

Well, this method sends it good on newer devices with more memory but on lower end devices it ends up in a Out of memory error. Or anyway a nearly OOM. When Somethimes i goes well but when i want to take the second or third picture, it always ends up in OOM errors.

I realize that what i am doing is memory intensive. For example :

  1. First i want the initial Width and Height of the original image.
  2. Then it is sampled down (i don't really know if it is done well).
  3. Then i load the sampled Bitmap into the memory.
  4. When i loaded it into memory, my scaled bitmap has to be loaded in memory aswell prior to Recycle() the first Bitmap.
  5. Ultimately i need a byte[] for sending the Bitmap over the web. But i need to convert it first prior to releasing my scaled Bitmap.
  6. Then i release my scaled Bitmap and send the byte[].
  7. Then as a final step, the byte[] needs to be released from memory aswell. I already do that on my BitmapToBytes() method as shown below, but i wanted to include it for maybe other insights.

    static byte[] BitmapToBytes(Bitmap bitmap) {
        byte[] data = new byte[0];
        using (MemoryStream stream = new MemoryStream ()) 
        {
            bitmap.Compress (Bitmap.CompressFormat.Jpeg, 90, stream);
            stream.Close ();
            data = stream.ToArray ();
        }
        return data;
    }
    

Does somebody sees any good parts where i can optimize this process? I know i am loading to much into memory but i can't think of another way.

It should be mentioned that i always want my images to be 1600x1200(Landscape) or 1200x1600(Portrait). I calculate that value in the following way :

public static float CalculateInSampleSize(BitmapFactory.Options options,
                                            int reqWidth, int reqHeight) {
        // Raw height and width of image
        int height = options.OutHeight;
        int width = options.OutWidth;
        float inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {

            // Calculate ratios of height and width to requested height and
            // width
            float heightRatio = ((float) height / (float) reqHeight);
            float widthRatio = ((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;
        }

        if(height < reqHeight || width < reqWidth) {
            // Calculate ratios of height and width to requested height and
            // width
            float heightRatio = ((float) reqHeight / (float) height);
            float widthRatio =  ((float) reqWidth / (float) width);

           // 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;
    }

Does anybody has any recommendations or an alternative workflow?

I would be so much helped with this!

Upvotes: 0

Views: 2517

Answers (1)

Suchith
Suchith

Reputation: 1527

This might be a very delayed reply but may helpful to somebody who got the same issue.

  • Use the the calculated InSampleSize value (option)to decode file to bitmap. this itself generates the scaled down bitmap instead of using in CreateScaledBitmap.
  • If you are expecting High resolution image as output then it is difficult to handle OutOfMemory issue. Because An image with a higher resolution does not provide any visible benefit, but still takes up precious memory and incurs additional performance overhead due to additional scaling performed by the view.[xamarin doc]
  • Calculate the bitmap target width and height relating to the imageview height and width. you can calculate this by MeasuredHeight and MeasuredWidth Property. [Note: This works only after complete image draw]
  • Consider using async method to decode file instead running on main thread [DecodeFileAsync]

For more detail follow this http://appliedcodelog.blogspot.in/2015/07/avoiding-imagebitmap.html

Upvotes: 2

Related Questions