Working with images often requires the app to be able to load very large images. Photos are especially large due to modern cameras producing high resolution images from excellent camera hardware.
We can easily load images from files, resources, or remote sources using the BitmapFactory
type:
DecodeStreamAsync
:using (var stream = Assets.Open("bigimage.png")) using (var bitmap = await BitmapFactory.DecodeStreamAsync(stream)) { imageView.SetImageBitmap(bitmap); }
As images can be quite large, both in resolution and in memory size, it is often better to reduce them using subsampling:
BitmapFactory.Options
type:var options = new BitmapFactory.Options();
options.InJustDecodeBounds = true;
InJustDecodeBounds
property is set to true
, decoding the image only loads the image dimensions, not the actual image data:using (var stream = Assets.Open("bigimage.png")) { await BitmapFactory.DecodeStreamAsync(stream)); }
OutWidth
and OutHeight
properties:var height = options.OutHeight; var width = options.OutWidth;
InSampleSize
property. This takes any value in powers of two:options.InSampleSize = 4;
InJustDecodeBounds
property to false
as we want to load the image data:options.InJustDecodeBounds = false;
Bitmap
instance:Bitmap bitmap; using (var stream = Assets.Open("bigimage.png")) { bitmap = await BitmapFactory.DecodeStreamAsync(stream); }
ImageView
instance or draw it onto a Canvas
instance:imageView.SetImageBitmap(bitmap);
Images take up large amounts of memory on a device, and may even cause the device to run out of memory altogether. To avoid this, we have to dispose of the resources as soon as we no longer need them:
bitmap.Dispose();
bitmap.Recycle();
ImageView
instance, we can also dispose of the old image currently displayed:var old = imageView.Drawable as BitmapDrawable; imageView.SetImageBitmap(null); if (old != null) { old.Bitmap.Recycle(); }
Working with images often requires the app to be able to load very large images. Photos are especially large due to modern cameras producing high resolution images from excellent camera hardware. If we try and load images with very high resolutions, especially if there are multiple images to load, we may cause the device to run out of memory.
Although the device may have a large amount of memory, only a small amount is allowed to be used by the app. Some devices, especially the older ones, only permit a maximum of 24 MB per application.
Additionally, even the images with small file sizes still consume large amounts of memory. This is due to file compression, which results in the image being decompressed into memory. Even though this limitation is not always a problem, we can have issues when developing an image-intensive app, such as an image gallery. There would be no point in loading a 3 MB file, which might decompress into 30 MB, if we were just going to show a 100 x 100 pixel thumbnail. The memory cost and processor usage would be wasted.
Subsampling allows the app to decode a large image file but only load a reduced image with a smaller memory size. To make use of subsampling, we use the BitmapFactory
and BitmapFactory.Options
types.
When loading bitmaps, we are usually able to make use of the various decode methods, such as DecodeStreamAsync
, DecodeFileAsync
, or DecodeResourceAsync
. When loading subsampled images, we still use these methods, but we use the overload that takes a BitmapFactory.Options
instance. Providing options to the decode method allows us to specify a subsampling size.
Before subsampling, we probably would want to check the size of the original image. There is no point in reducing the size of the image if it is already smaller than what we want it to be. To query the size of an image without having to decode the entire image, we set the InJustDecodeBounds
property to true
on the options instance. If we were going to decode a stream, we would use the DecodeStreamAsync()
method and pass the stream as the first parameter and the options as the second. When the method returns, the OutWidth
and OutHeight
properties on the options instance will contain the image's width and height, respectively. We can use these values to calculate an optimal sampling value.
We can use the image width and height to calculate the optimal subsampling size in powers of two. Once we have a size, we set the InSampleSize
property to that value, ensuring that we have set the InJustDecodeBounds
instance back to false
. When the decode method returns, it will have loaded a subsampled, or smaller, version of the image that takes up far less space in memory. This is especially useful when we want to load many images into a list view. We can quickly load many large images and only consume a small amount of memory.
However, even the smaller images take up space, and if not disposed correctly, they will eventually use all the available space. In the case of a list view, this can quickly happen if the user scrolls faster than the garbage collector, which is often the case. As Xamarin.Android involves using two garbage collectors, one for .NET and one for Java, we need to ensure that we allow both to work optimally.
If we are going to assign an image to an ImageView
instance or some other Java type, after which we won't need to access the bitmap data again, we can tell the .NET collector that it can collect the .NET handle. This is done by calling the Dispose()
method on the object. This will release the bitmap data into the Java world. If we ever want to access the bitmap data again, we will have to query Java for a new reference. For example, to access the bitmap in an ImageView
instance, we would use the Drawable
property on the ImageView
instance.
If we are going to need to reference the image again, we can retain a reference, but we need to ensure that we let the Java collector know when it can collect it. This is done by making use of the Recycle()
method. We need to ensure that we aren't using the image anywhere when recycling the bitmap, as whatever tries to display the image after the image was recycled will throw an exception.
3.14.15.94