Why does Bitmap cause OOM?
1. Each model sets an upper limit of VM value of application heap memory Dalvik when compiling ROM vm. Heapgrowthlimit is used to limit the maximum memory available for each application. Exceeding this maximum value will report OOM. This threshold is generally increased according to the dpi size of the mobile phone screen. The smaller the dpi, the lower the maximum memory available for each application. Therefore, when the number of loaded images is large, it is easy to exceed this threshold, resulting in OOM.
2. The higher the image resolution, the greater the memory consumption. When loading high-resolution images, it will take up a lot of memory. If it is not handled properly, it will OOM. For example, a picture with a resolution of 1920x1080. If Bitmap is used ARGB_8888 32-bit tiled display will occupy 1920x1080x4 bytes of memory and nearly 8M of memory. It can be imagined that if the picture is not processed, it will be OOM.
Bitmap Basics
Memory occupied by a picture Bitmap = picture length x picture width x bytes occupied by a pixel, undefined and Bitmap Config is an important parameter for specifying the number of bytes per pixel.
Among them, A Transparency of representation; R Represents red; G Represents green; B Represents blue.
ALPHA_8
Represents 8 bits Alpha bitmap ,Namely A=8,One pixel occupies 1 byte,It has no color,Only transparency
ARGB_4444
Represents 16 bits ARGB Bitmap, i.e A=4,R=4,G=4,B=4,One pixel accounts for 4+4+4+4=16 Bit, 2 bytes
ARGB_8888
Represents 32 bits ARGB Bitmap, i.e A=8,R=8,G=8,B=8,One pixel accounts for 8+8+8+8=32 Bit, 4 bytes
RGB_565
Represents 16 bits RGB bitmap ,Namely R=5,G=6,B=5,It has no transparency,One pixel accounts for 5+6+5=16 Bit, 2 bytes
Memory occupied by a picture Bitmap = picture length x picture width x bytes occupied by a pixel
According to the above algorithm, the memory occupied by pictures can be calculated, taking 100 * 100 pixel pictures as an example
image.png
Let's begin to learn the optimization scheme of Bitmap
1, Bitmap quality compression
Via bitmap compress(Bitmap.CompressFormat.JPEG, options, baos); Ways to reduce picture quality
public static Bitmap compressImage(Bitmap bitmap){
ByteArrayOutputStream baos = new ByteArrayOutputStream();
//Quality compression method, where 100 means no compression, and the compressed data is stored in baos
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
int options = 100;
//Cycle to judge whether the compressed picture is greater than 50kb and continue compression
while ( baos.toByteArray().length / 1024>50) {
//Empty baos
baos.reset();
bitmap.compress(Bitmap.CompressFormat.JPEG, options, baos);
options -= 10;//10% less each time
}
//Store the compressed data baos in ByteArrayInputStream
ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());
//Generate images from ByteArrayInputStream data
Bitmap newBitmap = BitmapFactory.decodeStream(isBm, null, null);
return newBitmap;
}
2, Scaling compression
int ratio = 8;
//Creates a new bitmap based on parameters
Bitmap result = Bitmap.createBitmap(bmp.getWidth() / ratio, bmp.getHeight() / ratio, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(result);
Rect rect = new Rect(0, 0, bmp.getWidth() / ratio, bmp.getHeight() / ratio);
canvas.drawBitmap(bmp, null, rect, null);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
result.compress(Bitmap.CompressFormat.JPEG, 100, baos);
try {
FileOutputStream fos = new FileOutputStream(file);
fos.write(baos.toByteArray());
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
3, Sample rate compression (size compression)
The core idea of Bitmap optimized loading is to use bitmapfactory Options to load a picture of the desired size.
For example, images are displayed through ImageView. In many cases, ImageView is not as large as the original size of the image. If the entire image is loaded and set to ImageView,ImageView cannot display the original image. Via BitmapFactory Options can load the reduced image at a certain sampling rate and display the reduced image in ImageView, which will reduce the memory occupation, avoid OOM to a certain extent and improve the performance of Bitmap loading. BitmapFactory supports all four class methods for loading pictures provided by BitmapFactory Options parameter, which makes it easy to sample and zoom an image.
In order to avoid OOM exceptions, it is best to check the size of each image when parsing it, and then decide whether to load the whole image into memory or compress the image and load it into memory. The following factors need to be considered:
1. Estimate the memory required to load the whole picture
2. How much memory are you willing to provide in order to load a picture
3. The actual size of the control used to display this picture
4. Screen size and resolution of the current device
Via bitmapfactory Options to zoom the picture, mainly using its inSampleSize parameter, that is, the sampling rate. When inSampleSize is 1, the size of the sampled picture is the original size of the picture; When inSampleSize is greater than 1, such as 2, the width and height of the sampled image are 1 / 2 of the size of the original image, the number of pixels is 1 / 4 of the size of the original image, and its occupied memory size is also 1 / 4 of the size of the original image.
The sampling rate must be an integer greater than 1 to reduce the image. The sampling rate acts on both width and height. The scaling ratio is 1 / (the power of inSampleSize). For example, if inSampleSize is 4, the scaling ratio is 1 / 16. The official document indicates that the inSampleSize has an index of 2: 1, 2, 4, 8, 16, etc.
How to obtain the sampling rate?
1. Bitmapfactory Set the inJustDecodeBounds parameter of options to true and load the picture; undefined 2. From bitmapfactory Take out the original width and height information of the picture in options, which correspond to the outWidth and outHeight parameters; undefined 3. Calculate the sampling rate inSampleSize according to the rules of the sampling rate and the required size of the target View; undefined 4. Bitmapfactory Set the inJustDecodeBounds parameter of options to false, and then reload the picture.
/**
* The compression parameters by picture size are bitmap * @param bitmap * @param pixelW * @param pixelH * @return */ public static Bitmap compressImageFromBitmap(Bitmap bitmap, int pixelW, int pixelH) { ByteArrayOutputStream os = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os); if( os.toByteArray().length / 1024>512) {//Judge that if the picture is greater than 0.5M, compress it to avoid overflow when generating the picture (BitmapFactory.decodeStream) os.reset(); bitmap.compress(Bitmap.CompressFormat.JPEG, 50, os);//Here, 50% is compressed and the compressed data is stored in Bao s } ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray()); BitmapFactory.Options options = new BitmapFactory.Options(); //First sampling options.inJustDecodeBounds = true;//Only the bitmap boundary is loaded, occupying part of the memory options.inPreferredConfig = Bitmap.Config.RGB_565;//Set color mode BitmapFactory.decodeStream(is, null, options);//Configure preferences //Second sampling options.inJustDecodeBounds = false; options.inSampleSize = computeSampleSize(options , pixelH > pixelW ? pixelW : pixelH ,pixelW * pixelH ); is = new ByteArrayInputStream(os.toByteArray()); //Configure the final preferences to the new bitmap object Bitmap newBitmap = BitmapFactory.decodeStream(is, null, options); return newBitmap; }
/**
* Dynamically calculate the image inSampleSize * @param options * @param minSideLength * @param maxNumOfPixels * @return */ public static int computeSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels) { int initialSize = computeInitialSampleSize(options, minSideLength, maxNumOfPixels); int roundedSize; if (initialSize <= 8) { roundedSize = 1; while (roundedSize < initialSize) { roundedSize <<= 1; } } else { roundedSize = (initialSize + 7) / 8 * 8; } return roundedSize; }
private static int computeInitialSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels) {
double w = options.outWidth;
double h = options.outHeight;
int lowerBound = (maxNumOfPixels == -1) ? 1 : (int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels));
int upperBound = (minSideLength == -1) ? 128 :(int) Math.min(Math.floor(w / minSideLength), Math.floor(h / minSideLength));
if (upperBound < lowerBound) {
return lowerBound;
}
if ((maxNumOfPixels == -1) && (minSideLength == -1)) {
return 1;
} else if (minSideLength == -1) {
return lowerBound;
} else {
return upperBound;
}
}
}
4, Bitmap color mode compression
Android uses the ARGB8888 configuration to process colors by default, occupying 4 bytes. If you use RGB565, it will only occupy 2 bytes. The price is that the displayed colors will be relatively small, which is suitable for scenes that do not require high color richness.
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;//Set color mode
5, Libjpeg So library compression
Libjpeg is a widely used open source JPEG image library, and Android also relies on libjpeg to compress images. However, Android does not directly encapsulate libjpeg, but is based on another open source project called skia as its image processing engine. Skia is a large and comprehensive engine maintained by Google, in which various image processing functions are implemented, and is widely used in Google's own and other company's products (such as Chrome, Firefox Android, etc.). Skia has well encapsulated libjpeg. Based on this engine, it is convenient to develop image processing functions for operating systems, browsers and so on.
**Java's native methods are as follows:
public static native String compressBitmap(Bitmap bit, int w, int h, int
quality, byte[] fileNameBytes, boolean optimize);**
The specific steps of the following C code are as follows:
1. Decode and convert Android's bitmap into RGB data undefined 2. Allocate space for JPEG objects and initialize undefined 3. Specify compression data source undefined 4. Obtain file information undefined 5. Set parameters for compression, including image size, color space undefined 6. Start compression undefined 7. Finish compression undefined 8. Release resources
#include <string.h>
#include <bitmap.h>
#include <log.h>
#include "jni.h"
#include <stdio.h>
#include <setjmp.h>
#include <math.h>
#include <stdint.h>
#include <time.h>
#include "jpeg/android/config.h"
#include "jpeg/jpeglib.h"
#include "jpeg/cdjpeg.h" /* Common decls for cjpeg/djpeg applications */
#define LOG_TAG "jni"
//#define LOGW(...) __android_log_write(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__)
//#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
#define true 1
#define false 0
typedef uint8_t BYTE;
char *error;
struct my_error_mgr {
struct jpeg_error_mgr pub;
jmp_buf setjmp_buffer;
};
typedef struct my_error_mgr * my_error_ptr;
METHODDEF(void)
my_error_exit (j_common_ptr cinfo)
{
my_error_ptr myerr = (my_error_ptr) cinfo->err;
(*cinfo->err->output_message) (cinfo);
error=(char*)myerr->pub.jpeg_message_table[myerr->pub.msg_code];
LOGE("jpeg_message_table[%d]:%s", myerr->pub.msg_code,myerr->pub.jpeg_message_table[myerr->pub.msg_code]);
// LOGE("addon_message_table:%s", myerr->pub.addon_message_table);
// LOGE("SIZEOF:%d",myerr->pub.msg_parm.i[0]);
// LOGE("sizeof:%d",myerr->pub.msg_parm.i[1]);
longjmp(myerr->setjmp_buffer, 1);
}
int generateJPEG(BYTE* data, int w, int h, int quality,
const char* outfilename, jboolean optimize) {
int nComponent = 3;
// The structure of jpeg, which stores information such as width, height, bit depth, picture format, etc
struct jpeg_compress_struct jcs;
struct my_error_mgr jem;
jcs.err = jpeg_std_error(&jem.pub);
jem.pub.error_exit = my_error_exit;
if (setjmp(jem.setjmp_buffer)) {
return 0;
}
jpeg_create_compress(&jcs);
// Open output file wb: writable byte
FILE* f = fopen(outfilename, "wb");
if (f == NULL) {
return 0;
}
// Set the file path of the structure
jpeg_stdio_dest(&jcs, f);
jcs.image_width = w;
jcs.image_height = h;
// Set Huffman code
jcs.arith_code = false;
jcs.input_components = nComponent;
if (nComponent == 1)
jcs.in_color_space = JCS_GRAYSCALE;
else
jcs.in_color_space = JCS_RGB;
jpeg_set_defaults(&jcs);
jcs.optimize_coding = optimize;
jpeg_set_quality(&jcs, quality, true);
// Start compression and write all pixels
jpeg_start_compress(&jcs, TRUE);
JSAMPROW row_pointer[1];
int row_stride;
row_stride = jcs.image_width * nComponent;
while (jcs.next_scanline < jcs.image_height) {
row_pointer[0] = &data[jcs.next_scanline * row_stride];
jpeg_write_scanlines(&jcs, row_pointer, 1);
}
jpeg_finish_compress(&jcs);
jpeg_destroy_compress(&jcs);
fclose(f);
return 1;
}
typedef struct {
uint8_t r;
uint8_t g;
uint8_t b;
} rgb;
char* jstrinTostring(JNIEnv* env, jbyteArray barr) {
char* rtn = NULL;
jsize alen = (*env)->GetArrayLength(env, barr);
jbyte* ba = (*env)->GetByteArrayElements(env, barr, 0);
if (alen > 0) {
rtn = (char*) malloc(alen + 1);
memcpy(rtn, ba, alen);
rtn[alen] = 0;
}
(*env)->ReleaseByteArrayElements(env, barr, ba, 0);
return rtn;
}
jstring Java_com_effective_bitmap_utils_EffectiveBitmapUtils_compressBitmap(JNIEnv* env,
jobject thiz, jobject bitmapcolor, int w, int h, int quality,
jbyteArray fileNameStr, jboolean optimize) {
AndroidBitmapInfo infocolor;
BYTE* pixelscolor;
int ret;
BYTE * data;
BYTE *tmpdata;
char * fileName = jstrinTostring(env, fileNameStr);
if ((ret = AndroidBitmap_getInfo(env, bitmapcolor, &infocolor)) < 0) {
LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret);
return (*env)->NewStringUTF(env, "0");;
}
if ((ret = AndroidBitmap_lockPixels(env, bitmapcolor, (void**)&pixelscolor)) < 0) {
LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
}
BYTE r, g, b;
data = NULL;
data = malloc(w * h * 3);
tmpdata = data;
int j = 0, i = 0;
int color;
for (i = 0; i < h; i++) {
for (j = 0; j < w; j++) {
color = *((int *) pixelscolor);
r = ((color & 0x00FF0000) >> 16);
g = ((color & 0x0000FF00) >> 8);
b = color & 0x000000FF;
*data = b;
*(data + 1) = g;
*(data + 2) = r;
data = data + 3;
pixelscolor += 4;
}
}
AndroidBitmap_unlockPixels(env, bitmapcolor);
int resultCode= generateJPEG(tmpdata, w, h, quality, fileName, optimize);
free(tmpdata);
if(resultCode==0){
jstring result=(*env)->NewStringUTF(env, error);
error=NULL;
return result;
}
return (*env)->NewStringUTF(env, "1"); //success
}
6, L3 cache (implemented by LruCache and DiskLruCache)
After loading the picture from the network for the first time, cache the picture in memory and sd card. In this way, we don't need to load pictures in the network frequently. In order to control the memory problem very well, we will consider using LruCache as the storage container of Bitmap in memory, and DiskLruCache on sd card to uniformly manage the picture cache on disk.
Combination of SoftReference and inBitmap parameters
In this way, it is stored as a reuse pool eliminated by LruCache
LruCache is used as the container for storing bitmaps, and one method in LruCache that deserves attention is entryRemoved. According to the statement given in the document, This method is called when the LruCache container is full and the stored objects need to be eliminated to make room (Note: this only means that the object is eliminated from the LruCache container, but it does not mean that the memory of the object will be recycled by the Dalvik virtual machine immediately). At this time, you can wrap the Bitmap with SoftReference in this method and store these recycled bitmaps in a HashSet container prepared in advance. Some people will ask. What is the meaning of such storage? Why In this way, it is also necessary to mention the inBitmap parameter (which began in Android 3.0. For details, please refer to the BitmapFactory.Options parameter in the API). This parameter is mainly provided for us to reuse the Bitmap in memory
When the above conditions are met. When the system decodes the picture, it will check whether there is a reusable Bitmap in the memory. Avoid the system performance degradation caused by frequent loading pictures on the SD card. After all, reusing directly from memory is dozens of times more efficient than IO operation on the SD card