A common issue considered in Android development is OOM (Out Of Memory), which refers to memory overflow. On one hand, loading a large number of images can lead to OOM; on the other hand, displaying a 1024 x 768 pixel image in a 128 x 96 ImageView is clearly not worthwhile. Instead, a suitable smaller version can be loaded into memory through sampling to reduce memory consumption. Bitmap optimization mainly focuses on two aspects as follows:
- Effectively handling large images
- Caching bitmaps
This article mainly focuses on how to effectively handle large bitmaps.
In addition, what factors should be considered when loading a smaller version of a bitmap into memory using bitmap sampling in Android?
- Estimating the memory required to load the full image
- The space required for this image in relation to other memory demands of its program
- The size of the target ImageView or UI component for loading the image
- The screen size or density of the current device.
The content of this article is as follows:
- Bitmap Sampling
- Bitmap Memory Calculation
- Testing Effects
Bitmap Sampling#
Images come in different shapes and sizes, and reading large images consumes memory. To read the size and type of a bitmap, the BitmapFactory class provides many decoding methods to create a bitmap from various resources, selecting the most appropriate decoding method based on image data resources. These methods attempt to request memory allocation to construct the bitmap, which can easily lead to OOM exceptions. Each type of decoding method has additional features that allow you to specify decoding options through the BitmapFactory.Options class. By setting inJustDecodeBounds to true during decoding, you can read the image's size and type without allocating memory. The following code implements simple bitmap sampling:
/**
* Bitmap Sampling
* @param res
* @param resId
* @return
*/
public Bitmap decodeSampleFromResource(Resources res, int resId){
//BitmapFactory creates setting options
BitmapFactory.Options options = new BitmapFactory.Options();
//Set sampling ratio
options.inSampleSize = 200;
Bitmap bitmap = BitmapFactory.decodeResource(res,resId,options);
return bitmap;
}
Note: Other decode... methods are similar to decodeResource, here decodeResource is used as an example.
In practical use, it is necessary to calculate a suitable inSampleSize based on specific width and height requirements for bitmap sampling. For example, encoding an image with a resolution of 2048 x 1536 using an inSampleSize value of 4 produces a 512 x 384 image. Here, assuming the bitmap configuration is ARGB_8888, loading it into memory only takes 0.75M instead of the original 12M. The calculation of memory occupied by the image will be introduced later. Below is the method for calculating the sampling ratio based on the required width and height:
/**
* 1. Calculate bitmap sampling ratio
*
* @param option
* @param reqWidth
* @param reqHeight
* @return
*/
public int calculateSampleSize(BitmapFactory.Options option, int reqWidth, int reqHeight) {
//Get the original width and height of the image
int width = option.outWidth;
int height = option.outHeight;
int inSampleSize = 1;
if (width > reqWidth || height > reqHeight) {
if (width > height) {
inSampleSize = Math.round((float) height / (float) reqHeight);
} else {
inSampleSize = Math.round((float) width / (float) reqWidth);
}
}
return inSampleSize;
}
/**
* 2. Calculate bitmap sampling ratio
* @param options
* @param reqWidth
* @param reqHeight
* @return
*/
public int calculateSampleSize1(BitmapFactory.Options options, int reqWidth, int reqHeight) {
//Get the original width and height of the image
int height = options.outHeight;
int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
// Calculate the ratio of actual width and height to target width and height
final int heightRatio = Math.round((float) height / (float) reqHeight);
final int widthRatio = Math.round((float) width / (float) reqWidth);
/**
* Choose the smallest ratio of width and height as the value of inSampleSize,
* ensuring that the final image's width and height are always greater than or equal to the target width and height.
*/
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
}
return inSampleSize;
}
Once the sampling ratio is obtained, you can process large images based on the required width and height. Below is the sampling of a large bitmap based on the calculated inSampleSize:
/**
* Bitmap Sampling
* @param resources
* @param resId
* @param reqWidth
* @param reqHeight
* @return
*/
public Bitmap decodeSampleFromBitmap(Resources resources, int resId, int reqWidth, int reqHeight) {
//Create a bitmap factory setting options
BitmapFactory.Options options = new BitmapFactory.Options();
//Set this property to true, decoding can only obtain width, height, mimeType
options.inJustDecodeBounds = true;
//Decode
BitmapFactory.decodeResource(resources, resId, options);
//Calculate sampling ratio
int inSampleSize = options.inSampleSize = calculateSampleSize(options, reqWidth, reqHeight);
//Set this property to false for actual decoding
options.inJustDecodeBounds = false;
//Decode
Bitmap bitmap = BitmapFactory.decodeResource(resources, resId, options);
return bitmap;
}
During the decoding process, the BitmapFactory.decodeResource() method is used, as follows:
/**
* Decode the resource file with the specified id
*/
public static Bitmap decodeResource(Resources res, int id, BitmapFactory.Options opts) {
...
/**
* Open a data stream to read resources based on the specified id,
* while copying TypeValue to obtain the original resource's density and other information.
* If the image is in drawable-xxhdpi, then density is 480dpi.
*/
is = res.openRawResource(id, value);
//Decode a Bitmap object from the input stream to scale the corresponding bitmap based on opts
bm = decodeResourceStream(res, value, is, null, opts);
...
}
Clearly, the actual decoding method should be the decodeResourceStream() method, as follows:
/**
* Decode a Bitmap from an input stream and scale it accordingly
*/
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
InputStream is, Rect pad, BitmapFactory.Options opts) {
if (opts == null) {
//Create a default Option object
opts = new BitmapFactory.Options();
}
/**
* If the inDensity value is set, calculate according to the set inDensity
* Otherwise, set inDensity to the density represented by the resource folder
*/
if (opts.inDensity == 0 && value != null) {
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
opts.inDensity = density;
}
}
/**
* Similarly, the inTargetDensity can also be set through the BitmapFactory.Option object
* inTargetDensity represents densityDpi, which is the density of the phone
* Use DisplayMetrics object.densityDpi to obtain it
*/
if (opts.inTargetDensity == 0 && res != null) {
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
//The decodeStream() method calls a native method
return decodeStream(is, pad, opts);
}
After setting inDensity and inTargetDensity, the decodeStream() method is called, which returns the fully decoded Bitmap object, as follows:
/**
* Returns the decoded Bitmap,
*/
public static Bitmap decodeStream(InputStream is, Rect outPadding, BitmapFactory.Options opts) {
...
bm = nativeDecodeAsset(asset, outPadding, opts);
//Calls the native method: nativeDecodeStream(is, tempStorage, outPadding, opts);
bm = decodeStreamInternal(is, outPadding, opts);
Set the newly decoded bitmap's density based on the Options
//Set the latest decoded Bitmap based on Options
setDensityFromOptions(bm, opts);
...
return bm;
}
Clearly, the decodeStream() method mainly calls native methods to complete the Bitmap decoding. Tracing the source code reveals that both nativeDecodeAsset() and nativeDecodeStream() methods call the doDecode() method, with key code as follows:
/**
* BitmapFactory.cpp source code
*/
static jobject doDecode(JNIEnv*env, SkStreamRewindable*stream, jobject padding, jobject options) {
...
if (env -> GetBooleanField(options, gOptions_scaledFieldID)) {
const int density = env -> GetIntField(options, gOptions_densityFieldID);
const int targetDensity = env -> GetIntField(options, gOptions_targetDensityFieldID);
const int screenDensity = env -> GetIntField(options, gOptions_screenDensityFieldID);
if (density != 0 && targetDensity != 0 && density != screenDensity) {
//Calculate scaling ratio
scale = (float) targetDensity / density;
}
}
...
//Original Bitmap
SkBitmap decodingBitmap;
...
//Original bitmap's width and height
int scaledWidth = decodingBitmap.width();
int scaledHeight = decodingBitmap.height();
//Comprehensively calculate the final width and height based on density and targetDensity
if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
scaledWidth = int(scaledWidth * scale + 0.5f);
scaledHeight = int(scaledHeight * scale + 0.5f);
}
...
//Scaling ratio in x and y directions, roughly equal to scale
const float sx = scaledWidth / float(decodingBitmap.width());
const float sy = scaledHeight / float(decodingBitmap.height());
...
//Scale the canvas and then draw the Bitmap
SkCanvas canvas (outputBitmap);
canvas.scale(sx, sy);
canvas.drawARGB(0x00, 0x00, 0x00, 0x00);
canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, & paint);
}
In the above code, you can see the calculation of the scaling ratio and the impact of density and targetDensity on the Bitmap's width and height, which indirectly affects the size of the memory occupied by the Bitmap. This issue will be illustrated with examples later. Note that density is related to the directory of the resource file (image) corresponding to the current Bitmap. For example, if an image is located in the drawable-xxhdpi directory, its corresponding Bitmap density is 480dpi, while targetDensity is the densityDpi of DisplayMetrics, which represents the density of the phone screen. So how can you view the implementation of native methods in Android? Here is the link:
BitmapFactory.cpp, you can directly search for the method names of native methods.
Bitmap Memory Calculation#
First, let's contribute a large image of 6000 x 4000, which is close to 12M. 【Click to obtain】 Loading this image directly into memory will definitely cause OOM. However, by appropriately sampling the bitmap to reduce the image size, OOM can be avoided. So how is the memory occupied by the Bitmap calculated? Generally, it is calculated as follows:
You can use bitmap.getConfig() to get the format of the Bitmap, which is ARGB_8888. In this Bitmap format, one pixel occupies 4 bytes, so multiply by 4. If the image is placed in the Android resource folder, the calculation method is as follows:
The above summarizes the calculation method of the memory occupied by the Bitmap. To verify, you can use the following method to obtain the size of the memory occupied by the Bitmap:
Since loading the selected image directly will lead to OOM, the examples in the following text will first sample and compress, and then calculate the memory occupied by the Bitmap.
Direct Sampling#
This method directly specifies the value of the sampling ratio inSampleSize and then samples before calculating the memory after sampling. Here, inSampleSize is specified as 200.
- Place the image in the drawable-xxhdpi directory, at which point the density represented by drawable-xxhdpi is 480 (density), and the density represented by my phone screen is 480 (targetDensity). Clearly, the scale is 1. First, sample the image and then load it into memory. At this point, the memory occupied by the Bitmap is:
- Place the image in the drawable-xhdpi directory, at which point the density represented by drawable-xhdpi is 320, and the density represented by my phone screen is 480 (targetDensity). When the image is loaded into memory, the memory represented by the Bitmap is:
Calculated Sampling#
This method calculates a suitable inSampleSize based on the requested width and height, rather than arbitrarily specifying inSampleSize. This method is most commonly used in actual development, where the requested width and height are 100x100. The specific inSampleSize calculation has been explained above.
- Place the image in the drawable-xxhdpi directory, at which point the density represented by drawable-xxhdpi is 480, and the density represented by my phone screen is 480 (targetDensity). When the image is loaded into memory, the memory represented by the Bitmap is:
- Place the image in the drawable-xhdpi directory, at which point the density represented by drawable-xhdpi is 320, and the density represented by my phone screen is 480 (targetDensity). When the image is loaded into memory, the memory represented by the Bitmap is:
The process of bitmap sampling and the calculation of memory occupied by Bitmap in different situations is described above.
Testing Effects#
The test effect images are as follows:
drawable-xhdpi | drawable-xxhdpi |
---|
|
Bitmap
bitmap sampling and memory calculation ends here.