Bitmap的加载离不开BitmapFactory类,关于Bitmap官方介绍:
Creates Bitmap objects from various sources, including files, streams, and byte-arrays.
BitmapFactory类提供了四类方法用来加载Bitmap:
注意:查看源码可以发现,decodeFile()和decodeResource()间接调用decodeStream()。
在Android3.0之前:Bitmap的像素数据存放在Native内存,而Bitmap对象本身则存放在Dalvik Heap中。
从Android3.0开始:Bitmap的内存就全部在Dalvik Heap里了 。
在Android3.0之前,需要使用Bitmap.recycle()进行Bitmap的内存回收。
从Android3.0开始,不需要手动回收Bitmap了。
从Android3.0开始,在Bitmap中引入了一个新的字段BitmapFactory.Options.inBitmap,设置此字段为true后,解码方法会尝试复用一张存在的Bitmap。这意味着Bitmap的内存被复用,避免了内存的回收及申请过程,显然性能表现更佳。
Android4.4(API 19)之前只有格式为jpg、png,同等宽高(要求苛刻),inSampleSize为1的Bitmap才可以复用。
从Android4.4(API 19)开始被复用的Bitmap的内存大于需要新申请内存的Bitmap的内存就可以了。
出于对性能和app的考虑,我们肯定是想着第一次从网络中加载到图片之后,能够将图片缓存在内存和sd卡中,这样,我们就不用频繁的去网络中加载图片,可以很好地控制内存问题。
一般都会考虑使用LruCache+DiskLruCache,LruCache作为Bitmap在内存中的存放容器,在sd卡则使用DiskLruCache来统一管理磁盘上的图片缓存。
之前提到,可以采用LruCache作为存放Bitmap的容器,而在LruCache中有一个方法值得留意,那就是entryRemoved(),按照文档给出的说法,在LruCache容器满了需要淘汰存放其中的对象腾出空间的时候会调用此方法。
此时可以在此方法中将Bitmap使用SoftReference包裹起来,并用事先准备好的一个HashSet容器来存放这些即将被回收的Bitmap,有人会问,这样存放有什么意义?
之前我们提到将inmutable设置为true,Bitmap的内存可以被复用,当然肯定要满足之前所说的条件。
解码方法对图片进行decode的时候会检查内存中是否有可复用的Bitmap,避免我们频繁地去SD卡上加载图片而造成系统性能的下降,毕竟从直接从内存中复用要比在SD卡上进行IO操作的效率要高很多。
1.ALPHA_8:颜色信息只由透明度组成,占8位。
2.ARGB_4444:颜色信息由透明度与R(Red),G(Green),B(Blue)四部分组成,每个部分都占4位,总共占16位。
3.ARGB_8888:颜色信息由透明度与R(Red),G(Green),B(Blue)四部分组成,每个部分都占8位,总共占32位。是Bitmap默认的颜色配置信息,也是最占空间的一种配置。
4.RGB_565:颜色信息由R(Red),G(Green),B(Blue)三部分组成,R占5位,G占6位,B占5位,总共占16位。
通常我们优化Bitmap时,当需要做性能优化或者防止OOM,我们通常会使用RGB_565,因为ALPHA_8只有透明度,显示一般图片没有意义,Bitmap.Config.ARGB_4444显示图片不清楚,Bitmap.Config.ARGB_8888占用内存最多。
Bitmap类中有一个方法getByteCount():
/**
* Returns the minimum number of bytes that can be used to store this bitmap's pixels.
*
* As of {@link android.os.Build.VERSION_CODES#KITKAT}, the result of this method can
* no longer be used to determine memory usage of a bitmap. See {@link
* #getAllocationByteCount()}.
*/
public final int getByteCount() {
// int result permits bitmaps up to 46,340 x 46,340
return getRowBytes() * getHeight();
}
还有一个方法getAllocationByteCount():
/**
* Returns the size of the allocated memory used to store this bitmap's pixels.
*
* This can be larger than the result of {@link #getByteCount()} if a bitmap is reused to
* decode other bitmaps of smaller size, or by manual reconfiguration. See {@link
* #reconfigure(int, int, Config)}, {@link #setWidth(int)}, {@link #setHeight(int)}, {@link
* #setConfig(Bitmap.Config)}, and {@link BitmapFactory.Options#inBitmap
* BitmapFactory.Options.inBitmap}. If a bitmap is not modified in this way, this value will be
* the same as that returned by {@link #getByteCount()}.
*
* This value will not change over the lifetime of a Bitmap.
*
* @see #reconfigure(int, int, Config)
*/
public final int getAllocationByteCount() {
if (mBuffer == null) {
// native backed bitmaps don't support reconfiguration,
// so alloc size is always content size
return getByteCount();
}
return mBuffer.length;
}
通过方法注释我们可以了解到,getByteCount()代表存储Bitmap的色素需要的最少内存,而getAllocationByteCount()代表在内存中为Bitmap分配的内存大小。
其实getByteCount()方法是在API12加入的,代表存储Bitmap的色素需要的最少内存。从API19开始getAllocationByteCount()方法代替了getByteCount()。
一般情况下getByteCount()和getAllocationByteCount()是相等的。但是Bitmap内存如果复用之后,两者就不一样了。
通过复用Bitmap来解码图片,如果被复用的Bitmap的内存比待分配内存的Bitmap大,那么getByteCount()表示新解码图片占用内存的大小(并非实际内存大小,实际大小是复用的那个Bitmap的大小),getAllocationByteCount()表示被复用Bitmap真实占用的内存大小。
那getByteCount()和getAllocationByteCount()值是怎么计算出来的呢?
下面就来举个例子计算理论上Bitmap加载一张图片时,所占内存的大小,和getByteCount()的结果比较一下。
一张像素为522*686的PNG图片,把它放到drawable-xxhdpi目录下,在三星s6上加载,getByteCount()的结果是2547360B。
默认的像素格式是ARGB_8888,之前已经说到了ARGB_8888格式下的一个像素点占用32位内存即4个字节。
所以结果是:int res = 522*686*4
,1432368。
显然和答案不一样啊。
假设中说把图片放到drawable-xxhdpi目录下,在三星s6上加载。并不是随口一说的,它们也是影响Bitmap所占内存大小的重要因素。
我们读取的是drawable目录下面的图片,用的是decodeResource方法,该方法本质上就两步:
读取原始资源,这个调用了Resource.openRawResource方法,这个方法调用完成之后会对TypedValue进行赋值,其中包含了原始资源的density等信息。
调用decodeResourceStream对原始资源进行解码和适配。这个过程实际上就是原始资源的density到屏幕density的一个映射。
原始资源的density其实取决于资源存放的目录(比如xxhdpi对应的是480),而屏幕density的值是和设备的硬件有关的,三星s6的值为640。加载时,原始的资源会自动进行缩放。
所以结果是:int res = (522 * 640 / 480) * (686 * 640 / 480) * 4
,2546432。
好像还是差那么一点,其实系统是进行了精度处理。
所以最终结果是:int res = (522 * 640 / 480f + 0.5) * (686 * 640 / 480f + 0.5) * 4
,2547360。
上面说的缩放和一个参数inScaled有关:
public static class Options {
/**
* Create a default Options object, which if left unchanged will give
* the same result from the decoder as if null were passed.
*/
public Options() {
inDither = false;
inScaled = true;
inPremultiplied = true;
}
……
}
如果inScaled设置为true,就缩放。设置为false,则不进行缩放。默认的值从上面代码可以看到,就是true。
缩放的比例为inTargetDensity / inDensity。
但是缩放也只针对资源文件有效,对于其他来源的图片不起效果,我们可以从源码中参数上的解释得知:
/**
* The pixel density to use for the bitmap. This will always result
* in the returned bitmap having a density set for it (see
* {@link Bitmap#setDensity(int) Bitmap.setDensity(int)}). In addition,
* if {@link #inScaled} is set (which it is by default} and this
* density does not match {@link #inTargetDensity}, then the bitmap
* will be scaled to the target density before being returned.
*
* If this is 0,
* {@link BitmapFactory#decodeResource(Resources, int)},
* {@link BitmapFactory#decodeResource(Resources, int, android.graphics.BitmapFactory.Options)},
* and {@link BitmapFactory#decodeResourceStream}
* will fill in the density associated with the resource. The other
* functions will leave it as-is and no density will be applied.
*
* @see #inTargetDensity
* @see #inScreenDensity
* @see #inScaled
* @see Bitmap#setDensity(int)
* @see android.util.DisplayMetrics#densityDpi
*/
public int inDensity;
其中有一句The other functions will leave it as-is and no density will be applied
就是这个意思。
以上所说inDensity和inTargetDensity其实是DPI(dots per inch),关于DPI的概念请移步 全面理解Android中的Px,DPI,DIP,Density,Sp等概念。
Bitmap加载资源文件在内存当中占用的大小取决于以下三点:
Bitmap加载其他来源的图片,就和像素格式有关。
不需要透明度的情况下,我们通常使用RGB_565。
inSampleSize的值必须大于1时才会有效果,且采样率同时作用于宽和高。当inSampleSize=1时,采样后的图片为图片的原始大小。
当inSampleSize=n时,采样后的图片的宽高均为原始图片宽高的1/n,这时像素为原始图片的1/(n*n),占用内存也为原始图片的1/(n*n)。
inSampleSize的取值应该总为2的整数倍,否则会向下取整,取一个最接近2的整数倍,比如inSampleSize=3时,系统会取inSampleSize=2。
假设一张1024*1024,模式为ARGB_8888的图片,inSampleSize=2,原始占用内存大小是4MB,采样后的图片占用内存大小就是(1024/2) * (1024/2 )* 4 = 1MB。
具体用法请参考Bitmap的高效加载(Android开发艺术探索学习笔记)。
我们之前使用inSampleSize对图片进行采样,采样之后内存是小了,可是图的尺寸也小了,我们要用Canvas绘制原始大小的图片该怎么办?就可以使用矩阵:
Matrix matrix = new Matrix();
matrix.preScale(2, 2, 0, 0);
canvas.drawBitmap(bitmap, matrix, paint);
这样,绘制出来的图就是放大以后的效果了,不过占用的内存却仍然是我们采样出来的大小。
如果我要把图片放到ImageView当中呢?一样可以,请看:
Matrix matrix = new Matrix();
matrix.postScale(2, 2, 0, 0);
imageView.setImageMatrix(matrix);
imageView.setScaleType(ScaleType.MATRIX);
imageView.setImageBitmap(bitmap);
参考:
1.Android坑档案:你的Bitmap究竟占多大内存?
2.Android性能优化:谈谈Bitmap的内存管理与优化
3.Android 之Bitmap
4.Android性能优化(五)之细说Bitmap
5.softReference+LruCache优化Android缓存
6.玩转Android Bitmap