通常情况下,图片的原始尺寸要比 ImageView 尺寸大,这时候把整张图片加载进 ImageView 会造成内存内存浪费,甚至造成 OOM (Out Of Memory Killer)。
在 Android App 中,为什么加载位图(Bitmap)是很棘手,是有很多原因的:
- Bitmap 可以很轻松的消耗 App 的内存预算(Android 分配给每个 APP 的最大内存)。例如,Pixel 手机上的相机可拍摄高达 4048x3036 像素(1200万像素)如果 bitmap 配置使用 ARGB_8888(每个像素占 4 字节)—— Android 2.3(API 9 版本)及以上版本的默认值,将这张照片加载到内存中需要大约48MB 的内存(4048 * 3036 * 4 字节)。如此大的内存能够立即耗尽 App 可用的所有的内存。
- 在 UI 线程中加载 bitmap 会降低你的 App 的性能,引发响应缓慢甚至发生 ANR (Application Not Responding) 的信息。因此,在使用 bitmap 时适当的管理线程是非常重要的。
- 如果你的 App 正在加载多张 bitmap 到内存中,你需要熟练的管理内存和磁盘缓存。否则,你的 App 可能要忍受响应性和滑动性差的体验
多数情况下,我们推荐你在 App 中,使用 Glide 框架获得,解析展示 bitmap。在 Android中,Glide 抽象出来很多复杂的 bitmap 和其他图像和其他任务的相关工作。
然而,在使用开源框架的同时,我们仍然要明白一个像 Glide 这样优秀的框架要做哪些事情。
图片采样率放缩
正如文章开头展示的情形,我们可以使用BitmapFactory.Options
按照一定的采样率加载缩小后的图片,将缩小后的图片在ImageView
中显示,这样就能降低内存占用,在一定程度上避免 OOM,提高 bitmap 加载时候的性能。
BitmapFactory
有一个参数:inSampleSize(采样率),这个值为 1 时,那么采样图片大小等于原始图片大小。为 2 时,那么采样后图片宽高均为原始图片的1/2,像素为原图的1/4,占有的内存大小为原图的1/4。
如前边的例子,放缩后,图片大约需要 12MB 的内存。
12MB 依然很大,我们还可以继续压缩,放缩规则是这样的:inSampleSize 是必须大于 1 的整数才有效果,小与 1 就相当于 1,并且同时作用于宽高,所以缩放后的图片大小以采样率的 2 次方形式递减。根据最新的官方文档,inSampleSize 的取值应该总是为 2 的指数,若给系统的 inSampleSize 不为 2 的指数,那么系统会向下取整并且选择一个最接近 2 的指数来代替,不过经过验证,这个结论并不是在所有的Android 版本上都成立。
如何使用采样率
核心代码:
|
|
打印结果如下:
|
|
由结果可知,放缩采样率后,图片的宽高都缩小了 2 倍。
在实际使用中,需要计算出合适的 inSampleSize 参数,以保证节省内存,这部分内容在 Android 开发艺术探索中 描述的很轻出,我就不班门弄斧了。
大图片展示
由上述结果我们可以发现,这幅图片的高度实在是太大,这样的图片加载到应用是这个样子的
这样的图片是根本看不清的,所以加载大图片的方式略有不同,需要借助一个类 BitmapRegionDecoder
。
初识 BitmapRegionDecoder
BitmapRegionDecoder
主要用于显示图片的某一块矩形区域,如果你需要显示某个图片的指定区域,那么这个类非常合适。
BitmapRegionDecoder
提供了一系列的newInstance
方法来构造对象,支持传入文件路径,文件描述符,文件的inputstrem
等。
|
|
- 上述解决了传入我们需要处理的图片,那么接下来就是显示指定的区域。
|
|
参数一很明显是一个rect,参数二是BitmapFactory.Options,你可以控制图片的inSampleSize
,inPreferredConfig
等。
下面是一个简单例子的核心代码:
|
|
以上代码是显示 big_picture 中间 400 * 400 的区域。
自定义显示大图控件
根据上面的分析,我们的自定义控件思路就非常清晰了:
- 提供一个设置图片的入口
- 重写 onTouchEvent,在里面根据用户移动的手势,去更新显示区域的参数
- 每次更新区域参数后,调用 invalidate,onDraw 里面去regionDecoder.decodeRegion 拿到 bitmap,去 draw
以下是自定义 View 的源码:
|
|
注意: 32 行的代码,如果它的位置放在了获取宽高之前,会导致宽高无法获取,结果会为 -1,将此行加粗希望读者不要在掉坑里了。
根据上述源码:
- setInputStream 里面去获得图片的真实的宽度和高度,以及初始化我们的 mDecoder
- onMeasure 里面为我们的显示区域的 rect 赋值,大小为 view 的尺寸
- onTouchEvent 里面我们监听 move 的手势,在监听的回调里面去改变 rect 的参数,以及做边界检查,最后 invalidate
- 在 onDraw 里面就是根据 rect 拿到 bitmap,然后 draw 了
ok,上面并不复杂,不过大家有没有注意到,这个监听用户move手势的代码写的有点奇怪,恩,这里模仿了系统的 ScaleGestureDetector
,编写了MoveGestureDetector
,代码如下:
|
|
|
|
不过值得一提的是:上面这个手势检测的写法,不是我想的,而是一个开源的项目https://github.com/rharter/android-gesture-detectors,里面包含很多的手势检测。对应的博文是:http://code.almeros.com/android-multitouch-gesture-detectors#.VibzzhArJXg 我是偷学鸿洋大神的。
测试
|
|
|
|
最后如上一张效果图:
结束,感谢您的阅读。