图片作为内存消耗大户,一直是开发人员尝试优化的重点对象。Bitmap的内存从3.0以前的位于native,到后来改成jvm,再到8.0又改回到native。jvm每个进程都有内存上限,而native则没有限制(不是没有影响,至少不会oom),所以把内存大户Bitmap挪到native可能是很多人的梦想,但native的管理和实现明显比jvm更为复杂,除非有现成实现,很少有人去动这一块。
二、初识BitmapBitmap是一个final类,因此不能被继承。Bitmap只有一个构造方法,且该构造方法是没有任何访问权限修饰符修饰,也就是说该构造方法是friendly,但是谷歌称Bitmap的构造方法是private(私有的),感觉有点不严谨。不管怎样,一般情况下,我们不能通过构造方法直接新建一个Bitmap对象。
Bitmap是Android系统中的图像处理中最重要类之一。Bitmap可以获取图像文件信息,对图像进行剪切、旋转、缩放,压缩等 *** 作,并可以以指定格式保存图像文件。
三、创建Bitmap对象既然不能直接通过构造方法创建Bitmap,那怎样才能创建Bitmap对象。通常我们可以利用Bitmap的静态方法createBitmap()和BitmapFactory的decode系列静态方法创建Bitmap对象。
Bitmap的静态方法createBitmap()BitmapFactory的decode系列静态方法
四、Bitmap的颜色配置信息与压缩方式信息
Bitmap中有两个内部枚举类:Config和CompressFormat,Config是用来设置颜色配置信息的,CompressFormat是用来设置压缩方式的。
通常我们优化Bitmap时,当需要做性能优化或者防止OOM(Out Of Memory),我们通常会使用Bitmap.Config.RGB_565这个配置,因为Bitmap.Config.ALPHA_8只有透明度,显示一般图片没有意义,Bitmap.Config.ARGB_4444显示图片不清楚,Bitmap.Config.ARGB_8888占用内存最多。
CompressFormat解析: Bitmap.CompressFormat.JPEG:表示以JPEG压缩算法进行图像压缩,压缩后的格式可以是".jpg"或者".jpeg",是一种有损压缩。Bitmap.CompressFormat.PNG:表示以PNG压缩算法进行图像压缩,压缩后的格式可以是".png",是一种无损压缩。Bitmap.CompressFormat.WEBP:表示以WebP压缩算法进行图像压缩,压缩后的格式可以是".webp",是一种有损压缩,质量相同的情况下,WebP格式图像的体积要比JPEG格式图像小40%。美中不足的是,WebP格式图像的编码时间“比JPEG格式图像长8倍”。 五、Bitmap 图片性能优化 方式一:使用inSampleSize采样率压缩,尺寸压缩:BitmapFactory是通过BitmapFactory.Options对图像进行 *** 作的,然后将 *** 作后的图像生成Bitmap对象或者将 *** 作后的图像用已经存在的Bitmap保存,当不能用之保存时会返回null。
BitmapFactory.Options中常用的字段有:
使用options.inJustDecodeBounds来获取原始尺寸,然后按需使用options.inSampleSize来采样图片到接近view尺寸。
BitmapFactory 在解码图片时,可以带一个Options,有一些比较有用的功能,比如:
inTargetDensity:绘制到目标Bitmap上的像素密度。inSampleSize:对图像进行压缩,这个值是一个int,当它小于1的时候,将会被当做1处理,如果大于1,那么就会按照比例(1 / inSampleSize)缩小bitmap的宽和高、降低分辨率,大于1时这个值将会被处置为2的整数次幂或者接近2的整数次幂。例如,width=100,height=100,inSampleSize=2(设置为2时),那么就会将bitmap处理为,width=50,height=50,宽高降为1 / 2,像素数降为1 / 4inJustDecodeBounds:如果设置成true,表示获取Bitmap对象信息,但是不将其像素加载到内存。有时如果只是为了获取图片的大小就可以用这个,而不必直接加载整张图片。inPreferredConfig:Bitmap对象颜色配置信息,默认会使用Bitmap.Config.ARGB_8888,在这个模式下一个像素点将会占用4个byte(8+8+8+8=32位(4字节)),而对一些没有透明度要求或者图片质量要求不高的图片,可以使用RGB_565,一个像素只会占用2个byte(5+6+5=16位(2字节)),一下可以省下50%内存。inPurgeable和inInputShareable这两个需要一起使用,BitmapFactory.java的源码里面有注释,大致意思是表示在系统内存不足时是否可以回收这个bitmap,有点类似软引用,但是实际在5.0以后这两个属性已经被忽略,因为系统认为回收后再解码实际会反而可能导致性能问题inBitmap:官方推荐使用的参数,表示重复利用图片内存,减少内存分配,在4.4以前只有相同大小的图片内存区域可以复用,4.4以后只要原有的图片比将要解码的图片大既可以复用了。inDensity:给Bitmap对象设置的密度,如果inScaled为true(这是默认的),而若inDensity与inTargetDensity不匹配,那么就会在Bitmap对象返回前将其缩放到匹配inTargetDensity。inDither:是否对图像进行抖动处理,默认值是false。inScaled:设置是否缩放。outHeight:Bitmap对象的高度。outWidth:Bitmap对象的宽度。 方式二:合理选择Bitmap的像素格式ARGB8888格式的图片,每像素占用 4 Byte(8+8+8+8=32位(4字节)),而 RGB565则是 2 Byte(5+6+5=16位(2字节))。如果是 ARGB8888 那么就是一个像素4个字节,如果是 RGB565 那就是2个字节。
格式 | 描述 |
---|---|
ALPHA_8 | 只有一个alpha通道,每个像素用一个字节(8位)存储 |
ARGB_4444 | 这个从API 13开始不建议使用,因为质量太差 |
ARGB_8888 | ARGB四个通道,每个像素用四个字节(32位)存储 |
RGB_565 | 每个像素占2字节,其中红色占5bit,绿色占6bit,蓝色占5bit |
ALPHA8 没必要用,因为我们随便用个颜色就可以搞定的。
ARGB4444 虽然占用内存只有 ARGB8888 的一半,不过已经被官方嫌弃。
ARGB8888、RGB565:默认会使用ARGB_8888,在这个模式下一个像素点将会占用4个byte,而对一些没有透明度要求或者图片质量要求不高的图片,可以使用RGB_565,一个像素只会占用2个byte,一下可以省下50%内存。
方式三:将图片放到合适的文件夹下:同一张图片,放在不同目录下,会生成不同大小的bitmap,因为它的宽高都被缩放了,所以图片资源应该尽可能放在高密度的资源文件夹下,这样可以节省图片的内存开支,一般建议放在xxhdpi下,目前主流手机都是这个dpi,并且UI给我们提供切图时也应该尽量面向高密度的屏幕设备来提供
jpg 是一种有损压缩的图片存储格式,而 png 则是无损压缩的图片存储格式,显而易见,jpg 会比 png 小.
Bitmap 在内存当中占用的大小其实取决于:
色彩格式,前面我们已经提到,如果是 ARGB8888 那么就是一个像素4个字节(8+8+8+8=32位(4字节)),如果是 RGB565 那就是2个字节(5+6+5=16位(2字节));
原始文件存放的资源目录(是hdpi还是 xhdpi 还是 xxhdpi 可不能傻傻分不清楚);
目标屏幕的密度(所以同等条件下,红米在资源方面消耗的内存肯定是要小于三星S6的);
方式四:设计合理的缓存机制(内存缓存,复用池,硬盘缓存)内存缓存:可以使用官方已经帮我们实现好的LurCache;
复用池:用来装载内存缓存LRU中被抛弃掉的,但是可能又会马上使用到的那些图片,我们在复用池中缓存下来,防止立马被GC回收掉;
硬盘缓存:有 JakeWharton 大神实现的 DiskLruCache ,下载地址:https://github.com/JakeWharton/DiskLruCache
为什么要设置复用池呢?
六、Bitmap 图片性能优化代码 图片优化需要做三件事情: 像素点的压缩(格式转换)内存复用三级缓存设计(内存、磁盘、网络) 像素点的压缩因为在官方8.0Android系统中,将图片放在native层中进行了处理,例如回收机制,在java层我们无法进行干预,为了能够不立即将图片交给native层处理,我们利用复用池中的弱引用暂时保存在java层中,以方便我们需要使用刚刚被内存缓存LRU放弃掉的图片,而不是重复decode图片再放入到LRU队列中,降低性能消耗。
// 用来优化图片(像素点的压缩(格式转换))
public class ImageResize {
// int maxW,int maxH ———— 限定像素点的宽和高
public static Bitmap resizeBitmap(Context context, int id, int maxW, int maxH, boolean hasAlpha) {
Resources resources = context.getResources();
BitmapFactory.Options options = new BitmapFactory.Options();
// Bitmap bitmap = BitmapFactory.decodeResource(resources,R.drawable.logo);
// 只解码出相关参数信息(宽、高信息等),不会真实产生图片
options.inJustDecodeBounds = true; // 解码动作开关(开)
BitmapFactory.decodeResource(resources, id, options);
int w = options.outWidth; // 图片真实输出的宽
int h = options.outHeight; // 图片真实输出的高
// 设置缩放系数(见图片)
options.inSampleSize = calcuteInSampleSize(w, h, maxW, maxH);
if (!hasAlpha) {
// 不需要透明度,使用 RGB_565(5+6+5=16位)
options.inPreferredConfig = Bitmap.Config.RGB_565;
}
options.inJustDecodeBounds = false; // 处理完进行关闭
// 返回设置过后的真实图片
return BitmapFactory.decodeResource(resources, id, options);
}
// 计算缩放系数
// w,h:原图的宽和高
// maxW,maxH:缩放后的宽和高
private static int calcuteInSampleSize(int w, int h, int maxW, int maxH) {
int inSampleSize = 1;
if (w > maxW && h > maxH) {
inSampleSize = 2;
while ((w / inSampleSize > maxW) && (h / inSampleSize > maxH)) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
}
像素点的压缩+内存复用
public class ImageResize {
// int maxW,int maxH ———— 限定像素点的宽和高
// reusable:内存复用位置
public static Bitmap resizeBitmap(Context context, int id, int maxW, int maxH, boolean hasAlpha,Bitmap reusable) {
Resources resources = context.getResources();
BitmapFactory.Options options = new BitmapFactory.Options();
// Bitmap bitmap = BitmapFactory.decodeResource(resources,R.drawable.logo);
// 只解码出相关参数信息(宽、高信息等),不会真实产生图片
options.inJustDecodeBounds = true; // 解码动作开关(开)
BitmapFactory.decodeResource(resources, id, options);
int w = options.outWidth; // 图片真实输出的宽
int h = options.outHeight; // 图片真实输出的高
// 设置缩放系数(见图片)
options.inSampleSize = calcuteInSampleSize(w, h, maxW, maxH);
if (!hasAlpha) {
// 不需要透明度,使用 RGB_565(5+6+5=16位)
options.inPreferredConfig = Bitmap.Config.RGB_565;
}
options.inJustDecodeBounds = false; // 处理完进行关闭
// 设置成能复用
options.inMutable=true;
// 设置复用需要的内存位置
options.inBitmap = reusable;
// 返回设置过后的真实图片
return BitmapFactory.decodeResource(resources, id, options);
}
// 计算缩放系数
// w,h:原图的宽和高
// maxW,maxH:缩放后的宽和高
private static int calcuteInSampleSize(int w, int h, int maxW, int maxH) {
int inSampleSize = 1;
if (w > maxW && h > maxH) {
inSampleSize = 2;
while ((w / inSampleSize > maxW) && (h / inSampleSize > maxH)) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
}
bitmap 对象在内存中申请一块内存,bitmap2 需要使用时直接复用 bitmap 申请的内存(也可以直接覆盖掉bitmap的内容)
public class ImageCache {
public static final String TAG = "ImageCache";
private static ImageCache instance;
private Context context;
public static ImageCache getInstance() {
if (null == instance) {
synchronized (ImageCache.class) {
if (null == instance) {
instance = new ImageCache();
}
}
}
return instance;
}
// 内存缓存
private LruCache<String, Bitmap> memoryCache;
// 磁盘缓存
private DiskLruCache diskLruCache;
BitmapFactory.Options options = new BitmapFactory.Options();
// 复用池
public static Set<WeakReference<Bitmap>> reuseablePool;
// 参数 dir 就是最后存储的磁盘缓存的路径
public void init(Context context, String dir) {
this.context = context.getApplicationContext();
// synchronizedSet:带锁的集合
reuseablePool = Collections.synchronizedSet(new HashSet<WeakReference<Bitmap>>());
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
int memoryClass = am.getMemoryClass();// 虚拟机提供的可用内存(APP可用的内存)
memoryCache = new LruCache<String, Bitmap>(memoryClass / 8 * 1024 * 1024) {// 1/8的可用内存用于图片的缓存(APP可用的内存里取1/8)
/*
* return value占用的内存大小
*/
@Override
// sizeOf方法用于计算图片大小
protected int sizeOf(String key, Bitmap value) {
// 为了兼容 Android 3.0以前(19以前和以后)
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
// 大于 19 以后(可以压缩复用)
return value.getAllocationByteCount();
}
// 19 以前复用内存只能使用同样大小的图片才能复用内存
return value.getByteCount();
}
@Override
// LruCache放满后一些数据会挤出(在 oldValue 挤出)
// newValue:队头 放入的对象
// oldValue:队尾 挤出的对象
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
// oldValue需要放入到复用池
if (oldValue.isMutable()) {
// 用 WeakReference 与 引用队列(ReferenceQueue)进行关联
reuseablePool.add(new WeakReference<Bitmap>(oldValue, getReferenceQueue()));
} else {
oldValue.recycle();
}
}
};
try {
// 手机上打开一个目录
// 参数三:1 个文件
diskLruCache = DiskLruCache.open(new File(dir), 1, 1, 10 * 1024 * 1024);
} catch (IOException e) {
e.printStackTrace();
}
}
// 引用队列
ReferenceQueue referenceQueue;
Thread clearReferenceQueue;
boolean shutDown;
// 用于主动监听GC的API,加快回收(可以兼容不同的Android版本)
private ReferenceQueue<Bitmap> getReferenceQueue() {
if (null == referenceQueue){
referenceQueue = new ReferenceQueue<Bitmap>();
// 单开一个线程,去扫描引用队列中GC扫到的内容,交到native层去释放
clearReferenceQueue = new Thread(new Runnable() {
@Override
public void run() {
while (!shutDown){
try {
// remove带阻塞功能的
Reference<Bitmap> reference = referenceQueue.remove();
Bitmap bitmap = reference.get();
if (null!=bitmap && !bitmap.isRecycled()){
bitmap.recycle(); // 提高加快回收动作(转到 native 层进行回收)
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
clearReferenceQueue.start();
}
return referenceQueue;
}
/**
* 加入内存缓存
*/
public void putBitmapToMemeory(String key,Bitmap bitmap){
memoryCache.put(key,bitmap);
}
public Bitmap getBitmapFromMemory(String key){
return memoryCache.get(key);
}
public void clearMemoryCache(){
memoryCache.evictAll();
}
// 获取复用池中的内容
public Bitmap getReuseable(int w,int h,int inSampleSize){
// 3.0以下不理会
if (Build.VERSION.SDK_INT<Build.VERSION_CODES.HONEYCOMB){
return null;
}
Bitmap reuseable = null;
// 通过迭代器
Iterator<WeakReference<Bitmap>> iterator = reuseablePool.iterator();
while (iterator.hasNext()){
Bitmap bitmap = iterator.next().get();
if (null!=bitmap){
// 可以复用
if (checkInBitmap(bitmap,w,h,inSampleSize)){
reuseable = bitmap;
iterator.remove();
Log.i(TAG, "复用池中找到了");
break;
}else {
iterator.remove();
}
}
}
return reuseable;
}
// 检测能不能复用
private boolean checkInBitmap(Bitmap bitmap, int w, int h, int inSampleSize) {
if (Build.VERSION.SDK_INT<Build.VERSION_CODES.KITKAT){
return bitmap.getWidth()==w && bitmap.getHeight()==h && inSampleSize == 1;
}
// 缩放系数大于 1 的就是可以复用
if (inSampleSize>=1){
w/=inSampleSize;
h/=inSampleSize;
}
int byteCount = w*h*getPixelsCount(bitmap.getConfig());
return byteCount<=bitmap.getAllocationByteCount();
}
/*
* 用于获取像素点的不同的格式所需要的字节数
*/
private int getPixelsCount(Bitmap.Config config) {
if (config == Bitmap.Config.ARGB_8888){
return 4;
}
return 2;
}
// 磁盘缓存的处理
/*
* 加入磁盘缓存
*/
public void putBitMapToDisk(String key,Bitmap bitmap){
DiskLruCache.Snapshot snapshot = null;
OutputStream os = null;
try {
snapshot = diskLruCache.get(key);
// 如果缓存中已经有这个文件 不理他
if (null==snapshot){
// 如果没有这个文件,就生成这个文件
DiskLruCache.Editor editor = diskLruCache.edit(key);
if (null!=editor){
os = editor.newOutputStream(0);
bitmap.compress(Bitmap.CompressFormat.JPEG,50,os);
editor.commit();
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if (null!=snapshot){
snapshot.close();
}
if (null!=os){
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/*
* 从磁盘缓存中取
*/
public Bitmap getBitmapFromDisk(String key,Bitmap reuseable){
DiskLruCache.Snapshot snapshot = null;
Bitmap bitmap = null;
try {
snapshot = diskLruCache.get(key);
if (null==snapshot){
return null;
}
// 获取文件输入流,读取 bitmap
InputStream is = snapshot.getInputStream(0);
// 解码个图片,写入
options.inMutable = true;
options.inBitmap = reuseable;
bitmap = BitmapFactory.decodeStream(is,null,options);
if (null != bitmap){
memoryCache.put(key,bitmap);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if (null!=snapshot){
snapshot.close();
}
}
return bitmap;
}
}
完整代码
BitMapAppDemo
参考性能优化专题三–内存优化(图片三级缓存)
玩转Android Bitmap
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)