一、引入

图片作为内存消耗大户,一直是开发人员尝试优化的重点对象。Bitmap的内存从3.0以前的位于native,到后来改成jvm,再到8.0又改回到native。jvm每个进程都有内存上限,而native则没有限制(不是没有影响,至少不会oom),所以把内存大户Bitmap挪到native可能是很多人的梦想,但native的管理和实现明显比jvm更为复杂,除非有现成实现,很少有人去动这一块。

二、初识Bitmap

Bitmap是一个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是用来设置压缩方式的。

Config解析:

  • Bitmap.Config.ALPHA_8:颜色信息只由透明度组成,占8位。
  • Bitmap.Config.ARGB_4444:颜色信息由透明度与R(Red),G(Green),B(Blue)四部分组成,每个部分都占4位,总共占16位。
  • Bitmap.Config.ARGB_8888:颜色信息由透明度与R(Red),G(Green),B(Blue)四部分组成,每个部分都占8位,总共占32位。是Bitmap默认的颜色配置信息,也是最占空间的一种配置。
  • Bitmap.Config.RGB_565:颜色信息由R(Red),G(Green),B(Blue)三部分组成,R占5位,G占6位,B占5位,总共占16位。

通常我们优化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 / 4
  • inJustDecodeBounds:如果设置成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

为什么要设置复用池呢?

因为在官方8.0Android系统中,将图片放在native层中进行了处理,例如回收机制,在java层我们无法进行干预,为了能够不立即将图片交给native层处理,我们利用复用池中的弱引用暂时保存在java层中,以方便我们需要使用刚刚被内存缓存LRU放弃掉的图片,而不是重复decode图片再放入到LRU队列中,降低性能消耗。

六、Bitmap 图片性能优化代码

图片优化需要做三件事情:

  1. 像素点的压缩(格式转换)
  2. 内存复用
  3. 三级缓存设计(内存、磁盘、网络)

像素点的压缩

// 用来优化图片(像素点的压缩(格式转换))
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() {@Overridepublic 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;}// 获取文件输入流,读取 bitmapInputStream 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

Android 图片性能优化:Bitmap相关推荐

  1. android 应用性能优化1

    1 背景 其实有点不想写这篇文章的,但是又想写,有些矛盾.不想写的原因是随便上网一搜一堆关于性能的建议,感觉大家你一总结.我一总结的都说到了很多优化注意事项,但是看过这些文章后大多数存在一个问题就是只 ...

  2. Android应用性能优化之优化列表头像过度绘制[一]

    为什么80%的码农都做不了架构师?>>>    操作的是否顺畅.卡顿,决定着整体的流畅程度. 事实上android跟iphone的差别,个人觉得很大程度上决定于流畅程度,无论是动画, ...

  3. Android客户端性能优化(魅族资深工程师毫无保留奉献)

    Android客户端性能优化(魅族资深工程师毫无保留奉献) 转载学习:http://blog.tingyun.com/web/article/detail/155?from=groupmessage& ...

  4. Android应用性能优化最佳实践.

    移动开发 Android应用性能优化最佳实践 罗彧成 著 图书在版编目(CIP)数据 Android应用性能优化最佳实践 / 罗彧成著. -北京:机械工业出版社,2017.1 (移动开发) ISBN ...

  5. Android应用性能优化——学习心得

    Android应用性能优化--学习心得 Android应用性能优化这门课分为内存优化.视图优化.电量优化.Bitmap优化.其他优化等五大部分,下面这对这五大部分的学习能容做一下总结: 一. 内存优化 ...

  6. Android APP性能优化

    转载自:https://www.cnblogs.com/qwangxiao/p/8727229.html Android APP性能优化(最新总结) 导语 安卓大军浩浩荡荡,发展已近十个年头,技术优化 ...

  7. Android WebView 性能优化

    原文出处:http://motalks.cn/2016/09/11/Android-WebView-JavaScript-3/ WebView相关阅读 Android WebView 和 javaSc ...

  8. Android APP性能优化(一)

    Android APP性能优化(最新总结) 安卓大军浩浩荡荡,发展已近十个年头,技术优化日异月新,如今Android 8.0 Oreo 都发布了,Android系统性能已经非常流畅了.但是,到了各大厂 ...

  9. Android的性能优化,全方面给你讲明白

    前言: 作为Android系统的使用者,我们经常会遇到图片加载速度慢.甚至长时间无响应,软件使用不流畅.甚至经常性卡退等问题,这些都是Android开发师需要进一步改进的地方,正是如此,公司对安卓项目 ...

最新文章

  1. 【Pushgateway】正则匹配,分隔逗号成数组
  2. MySQL ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: NO)的真正原因...
  3. java中日期格式转换
  4. android播放flv,Android:从url播放flv视频流
  5. 笔记,Vector类模板的基本功能
  6. CSS中的IFC和BFC入门
  7. mac securecrt无法记住密码的解决方法
  8. Modelsim 教程
  9. usbcan、can分析仪的产品特点和功能特点
  10. 阿里云部署flask项目
  11. 搞渗透!还不会信息收集的看这里(大佬的秘籍)
  12. android-ultra-pull-to-refresh list,Android下拉刷新控件android-Ultra-Pull-To-Refresh 使用
  13. 希尔贝壳参展世界人工智能大会 | WAIC 2021
  14. TOOD: Task-aligned One-stage Object Detection 原理与代码解析
  15. 自己搭建php主机绑定域名,只需5步,教你用虚拟主机搭建出属于自己的网站
  16. el-select选中变色及百分比怎么做
  17. 自考深圳大学计算机专业难吗,深圳大学全日制自考本科有用吗?计算机与软件学院自考办通过率...
  18. zabbix===》微信报警、聚合图形、自动发现自动注册、监控NFS、MySQL、web服务、URL地址
  19. 用cv2实现小图片边缘扩充
  20. 【51nod_3202】子集和判断

热门文章

  1. 【热门主题:比加猪爱无止尽xp主题】
  2. VR制作中必须踩的坑365之044(oculus2、UE4、UE5、VR记录一年的踩坑之旅)拳击VR小游戏红绿灯
  3. 计算机专业英语单词mp3,[听单词] 计算机专业英语词汇音频62,计算机英语单词MP3...
  4. selenum登录网页版有道词典
  5. 通过时间戳计算天数结果为负值
  6. Android 动画之补间动画实战(飞机起飞)
  7. 价格行为与量价关系(威科夫交易体系的建立与探索)
  8. Ubuntu16.04安装Windows可执行文件(QQ.exe)
  9. vue3小野森森-05-createApp,component,mount,unmount,directive指令,use,plugin,推荐一个好的vue3系列教程
  10. 加薪申请表要填这四个方面