ImageLoader 笔记
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条。