在上一篇文章《Android性能调优(5)—Bitmap内存模型》一篇我们介绍了Bitmap在Android系统中内存模型、内存复用、占用内存大小以及Android特殊场景的计算方式;对Bitmap有了一个更为全面的认识。
由于Bitmap非常消耗内存,使用不当极易引起OOM,故接下来我们一起讨论下该如何优化Bitmap的使用。
一、说在前面
1、缩放你的位图非常重要
对于现在的应用来讲到处都是位图。但是如果内存里的图片大小大于屏幕显示出的图片大小、这些高分辨率的图片会导致大量性能问题。
假如现在我们有一个图片库应用,现在这些图片大部分都是超高清的,但是在图库模式下,他们显示为小图标:
这意味着内存里的图片已经非常大,但是屏幕上它们只有实际的十分之一。
这里的问题在于:
占据了内存中你实际未使用的内存区块;这些大图片占用了内存堆中大量空间。你的应用剩余部分可用的空间变少。
然而重置这些图片大小让他们符合屏幕展示大小会减少不必要的内存膨胀,所以我们应该减轻这部分带来的性能压力。
二、内存压缩优化
我们应该要做的是让载入内存的图片规格符合设计显示规格;而不是加载完整的分辨率。
之后如果用户想看完整分辨率图片时,你可以在用户确定时再显示并载入内存:
缩放你的位图是Android上非常重要的操作;
1、crateScaledBitmap
幸运的是Android平台为我们提供了一整套API:craeteScaledBitmap()函数,它将现有的位图按照你选择大小准确的创建一个新的位图。
优点是:你可以获得想要的图片大小
缺点时:它需要现有的位图来工作,这就意味着在创建小的位图之前,这个图片需要被加载、解码。
2、inSampleSize
对于这种我们大多数时候是不能接受的,另一种更好的方式是可以在加载时重设位图大小的方法。
BitmapFactory.Options的inSampleSize,可以帮助我们实现这个功能。将这个属性设置为非1值,可以在不加载完整大小图片的前提下生成一张只有原始图片部分大小的新图片。
比如设置inSampleSize为2,获得只有原图二分之一大小的图片。
基本上,图片大小总是会比原始图片小于一倍
inSampleSize是个很快的操作,实际上它根据这个值会每隔几个像素读入一次数据
所以如果inSimpleSize值为1意味着它会隔1个像素读入一次。
3、inScaled
但是如果你想缩放一张图片是2的幂规格图片,就需要借助位图功能的inScaled,inDensity和inTargetDensity功能
当inScaled设置好时,系统会根据现有密度来划分目标密度,通过派生缩放数值来应用你的位图,这个方法会重设图片大小,并对它应用一个新的过滤:
但是多余的过滤步骤需要额外的处理时间;加载大图片的时间会很快增加,导致大小重置速度变慢。所以还要结合2个工具:
inSampleSize会先运用于图片,将他转换成接近目标大小的2次幂,然后用inDensity和inTargetDensity将缩放为你想要的准确大小。
这两个方法结合是非常快的操作,因为inSampleSize会减少像素的数量,而基于输出密度的步骤需要对这些像素大小重置过滤。
4、inJustDecodeBounds
另外一个特别注意的问题是:你如何获取图片资源大小?
inJustDecodeBounds = true值,然会会继续解码你的位图图片(注意此时它不会将位图载入内存)。这会生成图片的宽度和高度,并允许你继续进行实际的图片解码,最后将大小重置为你需要的规格。
三、Bitmap的复用
Bitmap复用在多级缓存中的应用:
我们可以看到:当Bitmap被移除时,判断是否可以被复用,如果允许复用将其加入到一个set中(等待复用)。
由于Bitmap在添加进入set中的是以弱引用的方式,然后如果在较低版本中Bitmap是要显示的调用recycler()释放所占用的Native内存,所以通过ReferenceQueue来监听被弱引用持有时对象回收情况:
开启一个线程,不断在RefereceQueue中获取(只有当Bitmap引用对象被回收),reference.get != null,此时调用Bitmap的recycle()释放所占用的Native内存。
这种方式在多级缓存的前提下,对Bitmap进行复用,这样对于内存平滑过渡是非常有益的。其实Glide早已经运用了该手段。感兴趣可以去看下其源码。
Google提供的sample:https://developer.android.google.cn/topic/performance/graphics/manage-memory.html
四、多级缓存管理
关于Android的多级缓存,其中主要的就是内存缓存和硬盘缓存。
它的核心思想:在获取一张图片时,首先到内存缓存(LRUCache)中去加载,如果未加载到,则到硬盘缓存(DiskLruCache)中加载,如果加载到将其返回并添加进内存缓存,否则通过网络加载一张新的图片,并将新加载的图片添加进入内存缓存和硬盘缓存。
有关内存LRUCache的资料实在太多,故不在此展开叙述。
DiskLruCache大家可以参考:https://github.com/JakeWharton/DiskLruCache
五、LRUCache原理分析
关于多级缓存,这种缓存机制的实现都应用到了LruCache算法,我们今天不打算展开详细讨论,感兴趣的可以看下JDK的LinkedHashMap源码。
其主要思想:
1、新数据插入到链表头部;
2、每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
3、当链表满的时候,将链表尾部的数据丢弃。
从图也可以看出每个元素都指向它的上一个元素和下一个元素,第一元素的上一个元素就是最后一个元素,最后一个元素的下一个元素就是第一个元素;这样实际构成了一个双向循环链表的结构。
其实Android也为我们提供了LRUCache工具来实现LRU缓存管理,它实际也是借助JDK的LinkedHashMap(当然你也可以自己实现)。
六、更多的选择
最后:不得不说现在有稳定的函数库可以为我们处理这部分复杂的工作。这两个工具库都是对位图大小做了重置,还有其他的功能:如异步加载和缓存。
这些都对Android处理图片变得更容易,依靠久经考验的函数库是个不错的主意。性能优化的关键在于理解这类的权衡取舍问题。
七、总结
1、移动设备对于资源非常敏感的平台,Bitmap就像是一个大胖子在内存中占据着大量的堆空间,这也是导致Bitmap的处理一直都是Android中的一个难点,如果处理不当非常容易造成OOM的发生。
2、在Android中缩放你的位图非常重要。
3、考虑使用久经考验的函数库通常是一个不错的主意。这些函数库都是经历大量实践积累,所以是比较可靠的。