BitmapFactory
我们不能够通过构造函数创建Bitmap对象。如果需要将图片转成Bitmap对象加载到内存中,就需要使用BitmapFactory类。BitmapFactory跟据图片数据源的不同,提供了几类获取Bitmap的方法。如下:
| 数据源类型 | 方法 |
|---|---|
| byte[] | decodeByteArray(byte[] data, int offset, int length,BitmapFactory.Options opts) |
| byte[] | decodeByteArray(byte[] data, int offset, int length) |
| File | decodeFile(String pathName, BitmapFactory.Options opts) |
| File | decodeFile(String pathName) |
| FileDescriptor | decodeFileDescriptor(FileDescriptor fd) |
| FileDescriptor | decodeFileDescriptor(FileDescriptor fd, BitmapFactory.Options opts) |
| Resource | decodeResource(Resource res, int id) |
| Resource | decodeResource(Resource res, int id, BitmapFactory.Options opts) |
| ResourceStream | decodeResourceStream(Resource res, TypedValue value,InputStream is,Rect pad,BitmapFactory.Options opts) |
| Stream | decodeStream(InputStream is) |
| Stream | decodeStream(InputStream is, Rect outPadding, BitmapFactory.Options opts) |
BitmapFactory.Options
从上面的表格可以看出,每一类数据源的解码方法都有两个。其中一个都有一个BitmapFactory.Options参数。这个参数对解码进行了配置。
它的可选参数如下:
| 参数 | 作用 |
|---|---|
| inBitmap : Bitmap | 重用一个Bitmap对象 |
| inDensity : int | 这张图片解码使用的屏幕密度 |
| inDither : boolean | deprecated in api-24, 如果设置改选项,那么解码的时候会尝试防抖动处理 |
| inInputShareable : boolean | deprecated in api-21 |
| inJustDecodeBounds : boolean | 如果设置该选项,返回值为null。但是可以从Options对象中获取Bitmap的宽高 |
| inMutable : boolean | 如果设置,将会解码出一个可更改的Bitmap对象,而不是不可更改的 |
| inPreferQualityOverSpeed : boolean | deprecated in api-24, 设置它会牺牲时间效率,提升图片的质量 |
| inPreferredConfig : Bitmap.Config | 这里设置Bitmap的像素存储格式,也就是Bitmap的config对象 |
| inPremutiplied : boolean | 默认为true,与dither类似是一种图像处理的方式 |
| inPurgeable : boolean | deprecated in api-21 |
| inSampleSize : int | 如果值大于1,那么生成一个缩略版的Bitmap |
| inScaled : boolean | 如果设置为true,并且inDensity和inTargetDensity不一致的时候,那么生成的bitmap会按照inTargetDensity的密度缩放,而不是系统提供的密度 |
| inScreenDensity : int | 屏幕的真实密度 |
| inTargetDensity : int | 这张bitmap绘制的屏幕密度 |
| inTempStorage : byte[] | 解码用的临时内存区域 |
| mCancel : boolean | deprecated in api 24 |
| outHeight : int | Bitmap的高度 |
| outWidth : int | Bitmap的宽度 |
| outMimeType: int | 解码图片的MimeType |
减少内存占用:
原理:
解码图片设置缩放比例可以减少Bitmap对象的内存占用。关键的参数是inSampleSize参数。举个例子:
一张2018 x 1536 的图片如果完全解码为(ARGB_8888)的Bitmap,那么他的内存占用为 2048 * 1536 * 4=12M;
如果设置inSampleSize为4,那么最终Bitmap对象的尺寸为512 x 384。内存占用为512 * 384 * 4 = 0.75M;
也就是inSampleSize = n的时候,内存占用为1/(n * n) n=1, 2, 4, 8, 16 .....
使用方法:
- 计算图片的尺寸
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;
- 根据期望的
imageview尺寸计算缩放的倍数
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
这里的imSampleSize的结果都是2的幂。根据inSampleSize的文档,如果传进去的inSampleSize非2的幂,那么会向下取2的幂为最终缩放比例。 例如:传 15 最终为 8;传 7最终为4;
- 根据
inSampleSize解码图片
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
这里参考一些文章的思路,根据实验发现利用BitmapFactory.Options中的Density相关的设置也可以控制图像的大小:
如果单独设置inDensity变量,那么只会影响到生成的Bitmap的density的值。
如果想要更根据density缩放,需要同时设置三个值:
| 变量 | 值 |
|---|---|
| inDensity | 图片数据对应的像素密度 |
| inTargetDensity | 生成的bitmap的像素密度 |
| inScale | 是否根据像素密度缩放,需要设置true |
测试代码:
// 这里使用了一张大小960000B大小的图片,放在assets目录下
// 这里的测试机默认屏幕像素密度480
AssetManager assetManager = getAssets();
InputStream is = null;
try {
is = assetManager.open("img_fjords.jpg");
BitmapFactory.Options options = new BitmapFactory.Options();
options.inTargetDensity = DisplayMetrics.DENSITY_HIGH; // 240
options.inDensity = DisplayMetrics.DENSITY_XXHIGH; // 480
options.inScaled = true;
Bitmap bitmap = BitmapFactory.decodeStream(is, new Rect(0, 0, 0, 0), options);
Log.d(LOG_TAG, "default density: " + 480);
Log.d(LOG_TAG, "default size: 960000");
Log.d(LOG_TAG, "bitmap density: " + String.valueOf(bitmap.getDensity()));
Log.d(LOG_TAG, "bitmap size: " + bitmap.getByteCount());
} catch (IOException e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/*
输出结果:
08-20 03:03:06.528 7386-7386/com.example.densitytest D/MainActivity: default density: 480
08-20 03:03:06.528 7386-7386/com.example.densitytest D/MainActivity: default size: 960000
08-20 03:03:06.528 7386-7386/com.example.densitytest D/MainActivity: bitmap density: 240
08-20 03:03:06.528 7386-7386/com.example.densitytest D/MainActivity: bitmap size: 240000
*/
可以看出来,内存大小确实变化了。内存大小关系应该是 finalSize = originSize*(inTargetDensity/inDensity)^2
不过需要注意的是tagetSize如果与屏幕像素密度不一致的时候展示的时候还是会缩放。所以,在使用这个方法控制的内存的时候
通过inDensity来控制,这样就不需要额外修改bitmap的density。
Bitmap
这个类就代表位图,它的一部分接口如下:
| 方法 | 解释 |
|---|---|
| compress(Bitmap.CompressFormat format, int quality, OutputStream stream) : boolean | 把一个位图写入流中 |
| copy(Bitmap.Config config, boolean isMutable) : Bitmap | 使用config配置复制一个Bitmap |
| copyPixelsFromBuffer(Buffer src) : void | 从一个Buffer对象中复制出所有的像素 |
| copyPixelsToBuffer(Buffer dst) : void | 将Bitmap的所有像素都复制到Buffer中 |
| static createBitmap(Bitmap source, int x, int y, int width, int height) : Bitmap | 从已有的Bitmap对象中取一个子集 |
| static createBitmap(int[] colors, int width, int height, Bitmap.Config config) : Bitmap | 根据颜色矩阵生成一幅位图 |
| static createBitmap(DisplayMetrics display, int width, int height, Bitmap.Config config) | 返回一个可更改的Bitmap |
| static createBitmap(int width, int height, Bitmap.Config config):Bitmap | 返回一个可更改的Bitmap |
| static createScaledBitmap(Bitmap src, int dstWidth, int sdtHeight, boolean filter) | 创建一个缩放到指定尺寸的Bitmap |
| describeContents() | ... |
| eraseColor(int c) : void | 将bitmap的所有像素都设置成同一颜色 |
| extractAlpha(): Bitmap | 生成一幅去掉Alpha值的Bitmap |
| extractAlpha(Paint paint, int[] offsetXY) | . |
| getAllocationByteCount(): int | 获取bitmap的尺寸 |
| getByteCount() : int | 获取存储图片最少需要的空间 |
| getConfig(): Bitmap.Config | 获取图片配置 |
| getDensity() : int | 获取图片密度 |
| getGenerationId() : int | 返回generationId |
| getHeight() | 位图高度 |
| getNinePatchChunck() : byte[] | 返回一个数组,为.9.png使用 |
| getPixel(int x, int y): int | 获取具体位置的颜色值 |
| getRowBytes() | 图片中一行像素占多少空间 |
| getScaledHeight(int targetDensity) : int, getScaledWidth(int targetDenisty) : int | 特定目标屏幕密度下的高度 |
| hasAlpha() : boolean | 如果每个像素都支持透明效果的话就返回true |
| hasMipMap(): boolean | ... |
| isMutable() : boolean | 是否可以更改 |
| isPremultiplied() : boolean | 像素点是否是premulitplied格式存储 |
| isRecycled() : boolean | 图片是否已经被回收 |
| prepareToDraw() | 为绘制做缓存 |
| reconfigure(int width, int height, Bitmap.Config config) : void | 更改Bitmap的配置属性,但是不会影响底层的存储 |
| recycle() : void | 释放native层的对象,并释放对像素矩阵的引用 |
| sameAs(Bitmap other) | 如果另一个Bitmap拥有同样的尺寸,配置,像素值就返回true |
| setConfig(Bitmap.Config config): void | reconfig方法的一种捷径 |
| setDensity(int density) : void | . |
| setHasAlpha(boolean hasAlpha) : void | . |
| setHasMipMap(boolean hasMipMap) : void | . |
| set Height(int height):void | . |
| setPixel(int x, int y):void | . |
| setPremultiplied(boolean premultiplied) : void | . |
| setWidth(int width):void | . |
| writeToParcel(Parcel p, int flags): void | . |
Bitmap.Config
这个类是用来配置像素格式的。它决定了像素的大小,图像的质量
| 变量名 | 大小(B) | 补充说明 |
|---|---|---|
| ALPHA_8 | 1 | 只有黑白灰,就像黑白电视,最节省空间 |
| ARGB_4444 | 2 | 由于图像质量问题,建议使用ARGB_8888。deprecated since api 14 |
| ARGB_8888 | 4 | 最高画质,建议使用,空间使用最多 |
| RGB_565 | 2 | 颜色相对丰富,适合不做透明处理的图像 |
Bitmap.CompressFormat
| 变量名 | 说明 |
|---|---|
| JPEG | 有损压缩,画质不稳定,存储传输效率高 |
| PNG | 无损压缩,画质很好,存储传输效率低 |
| WEBP | api 14 以后才提供使用,效果未知 |
这里对压缩做一下说明。compress方法有三个参数:
第一个是格式,PNG格式是无损的,所以后面的第二个参数对它没有影响。另外两种格式都有影响。
第二个是压缩比,取值在0~100之间。数字越大图片质量越高,体积越大。100 代表不压缩,0代表尽全力压缩。
第三个是输出流。
reconfigure方法,它是不更改底层像素值的。调用这个方法之后只是“看起来”变了,不会影响内存。
getAllocationByteCount()与getByteCount()分别需要API-19和API-12,api版本相对较高。事实上源码的计算很简单,如果app使用的时候受到api限制的话,完全可以自己计算:
public final int getBytesCount() {
return getRowBytes() * getHeight();
}
LruCache
基于LinkedHashMap的一种经典的内存缓存模型。它是用强引用控制的缓存。可以设置缓存的大小,个数。可以统计命中率,读写次数。它是线程安全的。从做缓存的角度来说,要比WeakHashMap要好很多。
api 12 以上可以直接使用。api 12 以下可以通过support v4包 使用。
它的接口十分简单明了
具体的接口参见:LruCache
DiskLruCache
DiskLruCache并不是谷歌官方的API。它是推荐给开发者使用的文件缓存的类。从名称上很好理解,文件系统中的Lru缓存。它的源码地址。
它的原理 利用LinkedHashMap在内存中记录文件缓存的最近访问顺序。磁盘中利用了journal文件作为日志文件,记录文件读写操作。每次创建DiskLruCache的时候都会通过journal日志重建LinkedHashMap的对象,这样在每次重新创建的时候也可以保持之前LRU的效果。源码不长,有兴趣的同学自行研究。网络上也有比较详细的介绍。
可以控制的变量:
- 缓存路径。建议选择App的cache目录下;
- cache版本。cache版本升级的时候会把旧的缓存全部清除;
- cache大小。cache的大小要小于缓存路径下的可有
- 日志条数。默认2000条。