1. Bitmap
在Android开发中,通过Bitmap对象可以很方便的对图片进行操作,但是bitmap特别耗内存,如果图片处理不当就会造成内存溢出(OOM),可通过
压缩(重点在属性inSampleSize值的计算)、裁剪和及时回收(recycle方法)
来避免内存泄漏。Bitmap在Android中指的是一张图片,图片类型可以是png、jpg等。
(1) Bitmap在内存中的大小
我们日常用像素形容图片的大小,android也不例外,但是像素并不代表图片在内存中的大小,还和ARGB通道有很大关系,其中A:表示透明度,R:表示三原色中的红,G:表示三原色中的绿,B:表示三原色中的蓝。即像素(长*宽)和像素位数(色彩模式)是用来描述图片的,可以通过这些信息计算出图片的像素占用内存的大小
。而在Bitmap的一个内部类Config中就定义了图片的色彩模式,也就确定了图片在内存中所占的大小:
Config | 常量值(色彩模式) | 描述 | 内存消耗(字节/像素) |
---|---|---|---|
Bitmap.Config | ARGB_8888 | 32位的ARGB位图 | 4 (argb_8888模式每个像素占用内存4byte) |
Bitmap.Config | ARGB_4444 | 16位的ARGB位图 | 2 (argb_444模式每个像素占用内存2byte) |
Bitmap.Config | RGB_565 | 16位的RGB位图 | 2 (rgb_565模式每个像素占用内存2byte) |
Bitmap.Config | ALPHA_8 | 8位的Alpha位图 | 1 (alpha_8模式每个像素占用内存1byte) |
Bitmap在内存中的大小与图片
像素
和色彩模式
有关,图片不同的色彩模式每个像素都有着不同的内存消耗。如:加载一张分辨率为1024 * 768的图片,若采用argb_8888色彩模式,则占用空间10247684=3M(1M=1024KB=1024*1024字节);如果采用argb_444占用内存就能减半,所以如果不考虑透明通道的话,建议使用RGB_565色彩模式,内存消耗较小。
ARGB_8888:四个通道都是8位,每个像素占用4个字节,图片质量是最高的,但是占用的内存也是最大的;
ARGB_4444:四个通道都是4位,每个像素占用2个字节,图片的失真比较严重;
RGB_565:没有A通道,每个像素占用2个字节,图片失真小,但是没有透明度;
ALPHA_8:只有A通道,每个像素占用1个字节大大小,只有透明度,没有颜色值。
使用场景总结:ARGB_4444失真严重,基本不用;ALPHA_8使用场景特殊,比如设置遮盖效果等;不需要设置透明度,RGB_565是个不错的选择;既要设置透明度,对图片质量要求又高,就用ARGB_8888。
(2) 构造方法
/**
* Private constructor that must received an already allocated native bitmap
* int (pointer).
*/
// called from JNI
Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density,
boolean isMutable, boolean requestPremultiplied,
byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
if (nativeBitmap == 0) {
throw new RuntimeException("internal error: native bitmap is 0");
}
mWidth = width;
mHeight = height;
mIsMutable = isMutable;
mRequestPremultiplied = requestPremultiplied;
mBuffer = buffer;
mNinePatchChunk = ninePatchChunk;
mNinePatchInsets = ninePatchInsets;
if (density >= 0) {
mDensity = density;
}
mNativePtr = nativeBitmap;
long nativeSize = NATIVE_ALLOCATION_SIZE;
if (buffer == null) {
nativeSize += getByteCount();
}
NativeAllocationRegistry registry = new NativeAllocationRegistry(
Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
registry.registerNativeAllocation(this, nativeBitmap);
}
构造方法不是public的,也就是说不能直接的去new Bitmap();而且注释说明 called from JNI,也就是要使用bitmap的辅助类BitmapFactory来操作图片。掌握
使用Bitmap的两个辅助类BitmapFactory和Options来对图片进行解码(即加载)--->操作--->保存--->释放(内存)
等操作,掌握了BitmapFactory和Options即掌握了Bitmap。
2. BitmapFactory
2.1 解码(加载)图片
BitmapFactory类提供了四类方法来加载图片:decodeResource、decodeFile、decodeStream、decodeByteArray。
(1) BitmapFactory.decodeResource(Resources,int),通过本地资源文件
加载bitmap
(2) BitmapFactory.decodeResource(Resources,int,Options),设置图片的Options属性
private void getDecodeResourceBitmap(){
Bitmap bitmap = BitmapFactory.decodeResource(getContext().getResources(),R.drawable.sample);
mImage.setImageBitmap(bitmap);
}
(3) BitmapFactory.decodeFile(String),通过本地文件路径
加载图片
(4) BitmapFactory.decodeFile(String,Options),设置图片的Options属性
private void getDecodeFileBitmap() {
String fileName = "/sdcard/daniulivelogo.png"; //自己手机sd图片路径
File file = new File(fileName);
if (file.exists()) {
Bitmap bm = BitmapFactory.decodeFile(fileName);
mImage.setImageBitmap(bm);
} else {
Log.v("hjz","文件不存在");
}
}
(5) public static Bitmap decodeStream(InputStream),从输入流中
中解码图片数据
(6) public static Bitmap decodeStream(InputStream,Rect,Options),设置Rect和Options属性
try {
FileInputStream in = new FileInputStream("/sdcard/Download/sample.png");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
Bitmap bitmap = BitmapFactory.decodeStream(in);//从输入流中加载图片
Bitmap bm = BitmapFactory.decodeFile(sd_path); //从文件中加载图片
private void getDecodeStreamBitmap(){
new Thread(new Runnable() {
@Override
public void run() {
final Bitmap bitmap = getImageFromNet("http://img5.imgtn.bdimg.com/it/u=274881988,3237971911&fm=27&gp=0.jpg");
runOnUiThread(new Runnable() {
@Override
public void run() {
mImage.setImageBitmap(bitmap);
}
});
}
}).start();
}
private Bitmap getImageFromNet(String path) {
HttpURLConnection httpURLConnection = null;
InputStream inputStream = null;
try {
URL url = new URL(path);
httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setRequestMethod("GET");
httpURLConnection.setReadTimeout(6 * 1000);
if (httpURLConnection.getResponseCode() == HttpURLConnection.HTTP_OK) {
inputStream = httpURLConnection.getInputStream();
return BitmapFactory.decodeStream(inputStream);
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
if (inputStream != null){
try {
inputStream.close();
inputStream = null;
} catch (IOException e) {
e.printStackTrace();
}
}
if (httpURLConnection != null){
httpURLConnection.disconnect();
}
}
return null;
}
(7) public static Bitmap decodeByteArray(byte[] data, int offset, int length),从字节数组
中加载图片
- data:图片数据
- offset:解码起始位置
- length:要解码的长度
(8) public static Bitmap decodeByteArray(byte[] data, int offset, int length,Options),设置图片的Options属性
// InputStream转换成byte[]
Bitmap bm = BitmapFactory.decodeByteArray(myByte,0,myByte.length);
private void getBitmap(){
//主要是把网络图片的数据流读入到内存中
new Thread(new Runnable() {
@Override
public void run() {
//Android4.0以后,网络请求一定要在子线程中进行
final byte[] data = getImages("http://img5.imgtn.bdimg.com/it/u=274881988,3237971911&fm=27&gp=0.jpg");
//更新UI可以在runOnUiThread这个方法或通过handler
runOnUiThread(new Runnable() {
@Override
public void run() {
Bitmap bitmap = BitmapFactory.decodeByteArray(data,0,data.length);
mImage.setImageBitmap(bitmap);
}
});
}
}).start();
}
private byte[] getImages(String path){
try {
URL url = new URL(path);
HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setRequestMethod("GET");
httpURLConnection.setReadTimeout(6*1000);
InputStream inputStream = null;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = -1;
if (httpURLConnection.getResponseCode() == HttpURLConnection.HTTP_OK){
inputStream = httpURLConnection.getInputStream();
while ((len = inputStream.read(buffer)) !=-1){
outputStream.write(buffer,0,len);
}
outputStream.close();
inputStream.close();
}
return outputStream.toByteArray();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
这些方法没什么特殊的,重点是Options类,弄明白Options就算搞定Bitmap了
2.2 高效加载类Options
备注:高效加载的核心主要是采样压缩(Options)
、缓存策略(LRU算法)
、异步加载(Fresco)
等。
Options是BitmapFractory的一个内部类,定义了若干属性,图片加载时通过设置Options的各个属性来指定图片的各种属性、压缩、宽高控制等等。常用属性如下:
属性名 | 类型 | 默认值 | 备注 |
---|---|---|---|
inJustDecodeBounds | boolean | - | 解码时返回仅图片宽高还是本身数据 :设置为true则解码只返回bitmap尺寸,不为其分配内存;设置为false,解码返回位图本身数据,为其分配内存。当我们加载时只需获取图片宽高时,就可以设置为true,减低对内存的占用。 |
inSampleSize | int | - | 解码时图片压缩比例 :设置值>1时,按相关比例缩放bitmap宽高,返回较小图片再保存;设置值<=1时,就当做1来处理。 inSampleSize = 原图尺寸/显示尺寸 ,如:width=100,height=100,inSampleSize = 2,则返回width=50,height=50,像素50*50=250 ,图片为原来1/4。 |
inPreferredConfig | 4种色彩模式 | Bitmap.Config.ARGB_8888 | 设置图片色彩模式 :若不考虑透明通道的话,建议使用RGB_565模式(一个像素2byte),内存消耗较小。 |
inPremultiplied | boolean | true | 解码返回的bitmap颜色通道上预先附加透明通道 |
inDither | boolean | false | 抖动解码:图片不同的色彩模式每个像素都有着不同的内存消耗,32位的图像比16位的清晰,所保存的颜色数据自然要多得多。当要把一张32位的图像解码成16位的图像的时候,自然会丢失很多颜色的数据,会使得图片失真,特别是有颜色渐变的时候会产生较为明显的断裂,这时候我们如果使用抖动解码的话能在一定程度上缓解这种现象 |
inScaled | boolean | true | Bitmap是否可以被缩放 |
outWidth和outHeight | int | - | Bitmap的宽和高(解码错误值均为-1):一般和inJustDecodeBounds一起使用获取Bitmap的宽高 |
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
Bitmap bmp = BitmapFactory.decodeFile(path, options);
//这时候的bmp只能取到图片的宽高
2.3 对Bitmap的常见操作
(1) 采样压缩(BitmapFactory.Options示例,重点在属性inSampleSize值的计算)
开发中很多时候并不需要图片原图大小和原图这么高清,通过Options对图片进行采样压缩,降低内存占有空间,从而减少了内存溢出(OOM)问题。
String fileName = "/sdcard/aa.jpg";//(1) 原图路径
BitmapFactory.Options options = getBitmapFactory(fileName,400,400);//(2) 设置Options对象各属性值,并返回Options
Bitmap bmp = BitmapFactory.decodeFile(fileName,options);//(3) 加载(解码)图片
mImage2.setImageBitmap(bmp);//(4) 显示图片
/**
* 设置Options对象各属性值,并返回Options
* @param fileName 原图路径
* @param pixelW 原图宽度
* @param pixelH 原图高度
* @return 返回Options对象
*/
private BitmapFactory.Options getBitmapFactory(String fileName,int pixelW,int pixelH){
BitmapFactory.Options options = new BitmapFactory.Options();
File file = new File(fileName);
if (file.exists()){
//inJustDecodeBounds为true,不返回bitmap,只返回这个bitmap的尺寸
options.inJustDecodeBounds = true;
//设置图片色彩
options.inPreferredConfig = Bitmap.Config.RGB_565;//使用RGB_565减少图片大小
//预加载图片
Bitmap bitmap = BitmapFactory.decodeFile(fileName,options);
//获取原始图片宽高
int originalW = options.outWidth;
int originalH = options.outHeight;
//上面设置为true获取bitmap尺寸大小,在这里一定要重新设置为false,否则位图加载不出来
options.inJustDecodeBounds = false;
options.inSampleSize = getSampleSize(originalW,originalH,pixelW,pixelH);//利用返回的原图片的宽高,可以计算出缩放比inSampleSize(只能是2的整数次幂)
return options;
}
return null;
}
/**
* 计算压缩比:官网提供的算法
* @param options
* @param reqWidth
* @param reqHeigth
* @return
*/
private int calculateInSampleSize(BitmapFactory.Options options,int reqWidth,int reqHeigth){
int inSampleSize = 1;//默认压缩比
final int width = options.outWidth;//原图宽度
final int heigth = options.outHeight;//原图高度
if (width > reqWidth || heigth > reqHeigth){//原图宽或高大于指定图片的尺寸
if (width > heigth){
inSampleSize = Math.round((float)heigth / (float)reqHeigth);
}else {
inSampleSize = Math.round((float)width / (float)reqWidth);
}
}
return inSampleSize;
}
/**
* 计算压缩比例inSampleSize的值
* @param originalW 原图宽
* @param originalH 原图高
* @param pixelW 指定图片宽度
* @param pixelH 指定图片高度
* @return 返回原图缩放大小
*/
private int getSampleSize(int originalW, int originalH, int pixelW, int pixelH) {
int simpleSize = 1;
if (originalW > originalH && originalW > pixelW){
simpleSize = originalW / pixelW;
}else if (originalH > originalW && originalH > pixelH){
simpleSize = originalH / pixelH;
}
if (simpleSize <=0){
simpleSize = 1;
}
return simpleSize;
}
/**
* 计算压缩比例inSampleSize的值
* @param ctx
* @param file 图片
* @param reqHeight 指定图片高度
* @param reqWidth 指定图片宽度
* @return
*/
private int getSampleSize(Context ctx, Object file, int reqHeight, int reqWidth) {
int inSampleSize = 1;
Bitmap bitmap = null;
BitmapFactory.Options ops = new BitmapFactory.Options();
ops.inJustDecodeBounds = true;// //这里我们只需要图片的宽高,而不需要图片的真实数据
if (file instanceof String) {
bitmap = BitmapFactory.decodeFile((String) file, ops);//从本地文件中加载图片
} else if (file instanceof Integer) {
bitmap = BitmapFactory.decodeResource(ctx.getResources(), (Integer) file, ops);//从本地资源文件中加载图片
}
int height, width;
if (bitmap == null) {
return inSampleSize;
}
height = ops.outHeight;//获取原始图片高度
width = ops.outWidth;//获取原始图片宽度
if (height > reqHeight || width > reqWidth) {
if (height / reqHeight > width / reqWidth) {
inSampleSize = height / reqHeight;
} else {
inSampleSize = width / reqWidth;
}
}
return inSampleSize;
}
/**
* 计算压缩比例inSampleSize的值
* @param options
* @param reqWidth 指定图片宽度
* @param reqHeight 指定图片高度
* @return
*/
private int getSampleSize(BitmapFactory.Options options,int reqWidth,int reqHeight){
int inSampleSize = 1;
int width = options.outWidth;
int height =options.outHeight;
int halfWidth = width / 2;
int halfHeight = height / 2;
while((halfWidth / inSampleSize) >= reqWidth && (halfHeight / inSampleSize) >= reqHeight){
inSampleSize *= 2;
}
return inSampleSize;
}
/**
* 计算压缩比例inSampleSize的值
* @param path 图片路径
* @param reqHeight 指定图片高度
* @param reqWidth 指定图片宽度
*/
private int getSampleSize(String path, int reqHeight, int reqWidth) {
int inSampleSize = 1;
BitmapFactory.Options options = new BitmapFactory.Options();
//(1) 获取原图尺寸
options.inJustDecodeBounds = true;//设置为true则解码只返回bitmap尺寸,不为其分配内存
BitmapFactory.decodeFile(path, options);//解码
int height = options.outHeight;
int width = options.outWidth;
//(2) 计算压缩比例inSampleSize的值
if (height > reqHeight || width > reqWidth) {
final int heightRatio = Math.round((float) height / (float) reqHeight);
final int widthRatio = Math.round((float) width / (float) reqWidth);
options.inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
} else {
options.inSampleSize = 1;
}
return inSampleSize;
}
这里要特别注意的是inSampleSize的值最终都会被换算成2的整数倍,也就是如果我们计算出来的是5,最终取值为4,所以准确性上来说回稍有误差,如果过于追求性能,可以考虑使用Matrix。
(2) 使用compress()对bitmap进行压缩
private static Bitmap compressImage(Bitmap image) {
if (image == null) {
return null;
}
ByteArrayOutputStream baos = null;
try {
baos = new ByteArrayOutputStream();
/**
* 参数1:压缩格式
* 参数2:压缩率,30表示图片被压缩了70%,100表示不压缩
* 参数3:将压缩完成后的图片数据存入流中
*/
image.compress(Bitmap.CompressFormat.JPEG, 30, baos);
byte[] bytes = baos.toByteArray();
//根据字节数组获取一个输入流
ByteArrayInputStream isBm = new ByteArrayInputStream(bytes);
//将输入流转换成最终的bitmap
Bitmap bitmap = BitmapFactory.decodeStream(isBm);
return bitmap;
} catch (OutOfMemoryError e) {
} finally {
try {
if (baos != null) {
baos.close();
}
} catch (IOException e) {
}
}
return null;
}
2.4 存储
2.3以前:图片存储在native内存中。虚拟机无法自动进行垃圾回收,必须手动使用recycle,很容易导致内存泄露。
3.0以后:图片存储在Java堆中。垃圾回收能够自动进行,内存占用也能方便的展示在monitor中。
4.0以后:传输方式发生变化,大数据会通过ashmem(匿名共享内存)来传递(不占用Java内存),小数据通过直接拷贝的方式(在内存中操作),放宽了图片大小的限制。参考:Android4.0 Bitmap存储以及Parcel传输源码分析
6.0以后:加强了ashmen存储图片的方式。参考:Android6.0 Bitmap存储以及Parcel传输源码分析
2.5 内存优化
优化处理:如采样压缩获取缩略图(在不同的情形下加载不同大小的图片到内存,避免原图加载造成OOM)、缓存方式选择(如:三级缓存)、内存管理等。
(1) 采样压缩获取缩略图:计算压缩比例inSampleSize,根据inSampleSize值获取缩略图并存储在内存
package comi.example.liy.mytestdemo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
/**
* Created by liy on 2019-11-14 10:19
* 自定义Bitmap工具类
*/
public class BitmapUtils {
/**
* 计算压缩比:官网提供的方法
* @param options
* @param reqWidth 指定图片宽度
* @param reqHeigth 指定图片高度
* @return
*/
public int calculateInSampleSize(BitmapFactory.Options options,int reqWidth,int reqHeigth){
int inSampleSize = 1;//默认压缩比
final int width = options.outWidth;//原图宽度
final int heigth = options.outHeight;//原图高度
if (width > reqWidth || heigth > reqHeigth){//原图宽或高大于指定图片的尺寸
if (width > heigth){
inSampleSize = Math.round((float)heigth / (float)reqHeigth);
}else {
inSampleSize = Math.round((float)width / (float)reqWidth);
}
}
return inSampleSize;
}
/**
* 获取缩略图
* @param pathName
* @param maxWidth
* @param maxHeigth
* @return
*/
public Bitmap getThumbnail(String pathName,int maxWidth,int maxHeigth){
BitmapFactory.Options options = new BitmapFactory.Options();
//计算压缩比
options.inJustDecodeBounds = true;//设置加载图片仅返回图片尺寸
Bitmap bitmap = BitmapFactory.decodeFile(pathName,options);//加载图片inJustDecodeBounds设置为true时,加载图片仅返回尺寸(不用为其分配内存)
int inSampleSize = calculateInSampleSize(options,maxWidth,maxHeigth);//获取压缩比(options中仅包含原图尺寸)
//设置options各属性
options.inPreferredConfig = Bitmap.Config.RGB_565;//设置色彩模式:如果不考虑透明通道的话,建议使用RGB_565色彩模式,内存消耗较小
options.inSampleSize =inSampleSize;//设置压缩比
options.inJustDecodeBounds = false;//设置加载返回图片本身(需分配内存):上面设置为true获取bitmap尺寸大小,在这里一定要重新设置为false,否则位图加载不出来
//加载图片
if (bitmap != null || !bitmap.isRecycled()){
bitmap.recycle();//回收
}
bitmap = BitmapFactory.decodeFile(pathName,options);
return bitmap;
}
public Bitmap getThumbnailTest(String pathName,int maxWidth,int maxHeigth){
BitmapFactory.Options options = new BitmapFactory.Options();
//计算压缩比
options.inJustDecodeBounds = true;//设置加载图片仅返回图片尺寸
BitmapFactory.decodeFile(pathName,options);//加载图片:inJustDecodeBounds设置为true时,加载图片仅返回尺寸(不用为其分配内存)
int inSampleSize = calculateInSampleSize(options,maxWidth,maxHeigth);//获取压缩比(options中仅包含原图尺寸)
//设置options各属性
options.inPreferredConfig = Bitmap.Config.RGB_565;//设置色彩模式:如果不考虑透明通道的话,建议使用RGB_565色彩模式,内存消耗较小
options.inSampleSize =inSampleSize;//设置压缩比
options.inJustDecodeBounds = false;//设置加载返回图片本身(需分配内存):上面设置为true获取bitmap尺寸大小,在这里一定要重新设置为false,否则位图加载不出来
//加载图片
Bitmap bitmap = BitmapFactory.decodeFile(pathName,options);
return bitmap;
}
}
(2) LRU算法:三级缓存会用到
LRU算法(最近最少使用)实现原理:LRU算法定义了一个泛型类LRUCache,
内部用LinkedHashMap的实现
。通过put()
和get()
方法来添加和获取缓存对象,当缓存队列满的时候,通过trimToSize()
方法将最近最少使用的缓存对象从缓存队列中移除(remove方法),并将新的缓存对象添加到缓存队列。
(3) 三级缓存:网络缓存、本地缓存和内存缓存
三级缓存包括网络缓存、本地缓存和内存缓存:当首次加载一张图片时,需向网络请求,网络获取图片后将图片保存到本地和内存各一份,当再次加载显示图片时就从本地或内存来获取。
2.6 开源框架(起码阅读Glide和Fresco源码)
常用的图片加载框架有: ImageLoader、Glide(google)、Fresco(FaceBook)、Picasso(Square)。
- Picasso包体积小、清晰,但功能有局限不能加载gif、只能缓存全尺寸
- Glide功能全面,擅长大型图片流,提交较大
- Fresco内存优化,减少oom,体积更大