改不完的 Bug,写不完的矫情。公众号 杨正友 现在专注音视频和 APM ,涵盖各个知识领域;只做全网最 Geek 的公众号,欢迎您的关注!
在内存优化中,优化 Bitmap 占用的内存效果最为明显,在 Android 里面,大部分 OOM,都是 bitmap 占用资源过大导致的,那么问题来了
- 如何防止 bitmap 占用资源过大导致 OOM?
- Android 系统何时会发生 OOM?
- 怎样搭建线上线下一体化内存监控体系?
- Drump 文件过大,我们线上如何查看?
- 线下监控那些工具你会用吗?
- 关于 Native 层的内存泄漏该如何解决?
- 图片监控你做过哪些努力?
- 内存抖动为什么会引起 OOM?
- 内存监控里面采集方式有哪些?
看完本文,希望可以以本文为索引,然后依次排雷解决上述问题,这样你也是这个领域的专家了
一. 如何得到 bitmap 对象?
Bitmap 是 Android 系统中的图像处理中最重要类之一。Bitmap 可以获取图像文件信息,对图像进行剪切、旋转、缩放,压缩等操作,并可以以指定格式保存图像文件。
如何创建 Bitmap 对象 创建 Bitmap 对象有两种方式,分别为:
Bitmap.createBitmap() 和 BitmapFactory 的 decode 系列静态方法创建 Bitmap 对象。
下面我们主要介绍 BitmapFactory 的 decode 方式创建 Bitmap 对象。
方式 1. decodeFile 从文件系统中加载
- 通过 Intent 打开本地图片或照片
- 根据 uri 获取图片的路径
- 根据路径解析
Bitmap bm = BitmapFactory.decodeFile(path);
方式 2. decodeResource 以 R.drawable.xxx 的形式从本地资源中加载
Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.icon);
方式 3 decodeStream 从输入流加载
Bitmap bm = BitmapFactory.decodeStream(stream);
方式 4 decodeByteArray 从字节数组中加载
Bitmap bm = BitmapFactory.decodeByteArray (myByte,0,myByte.length);
二. BitmapFactory.Options
合理的配置 BitmapFactory.Options 参数对防止内存溢出,节省内存开销,系统更流畅至关重要,下面我们就来聊聊 BitmapFactory.Options 参数介绍吧
2.1 BitmapFactory.Options 参数介绍
字段 | 参数名称 | 备注 |
inSampleSize | 采样大小 | 用于将图片缩小加载出来的,以免占用太大内存,适合缩略图 |
inJustDecodeBounds | 是否解析图片的原始宽高信息 | 当 inJustDecodeBounds 为 true 时,执行 decodexxx 方法时, BitmapFactory 只会解析图片的原始宽高信息,并不会真正的加载图片 |
inPreferredConfig | 配置图片解码方式 | 对应的类型 Bitmap.Config。如果非 null, 则会使用它来解码图片。默认值为是 Bitmap.Config.ARGB_8888 |
inBitmap | 在图片加载的时候可以使用之前已经创建了的 Bitmap,以便节省内存 | 在 Android 3.0 开始引入了 inBitmap 设置,通过设置这个参数,在图片加载的时候可以使用之前已经创建了的 Bitmap,以便节省内存,避免再次创建一个 Bitmap。 在 Android4.4,新增了允许 inBitmap 设置的图片与需要加载的图片的大小不同的情况,只要 inBitmap 的图片比当前需要加载的图片大就好了。 |
2.2 如何进行图片尺寸压缩
通过 BitmapFactory.Options 的这些参数,我们就可以按一定的采样率来加载缩小后的图片,然后在 ImageView 中使用缩小的图片这样就会降低内存占用避免【OOM】,提高了 Bitamp 加载时的性能。
这其实就是我们常说的图片尺寸压缩。尺寸压缩是压缩图片的像素
2.3 一张图片所占内存大小的计算方式
一张图片所占内存大小的计算方式: 图片类型*宽*高,通过改变三个值减小图片所占的内存,防止 OOM,当然这种方式可能会使图片失真 。
三. 如何计算 Bitmap 所占内存大小
计算 Bitmap 所占内存大小 有两种,一种是 获取可用 bitmap 字节大小 bitmap.getByteCount(),另外一种是 获取系统分配的 Bitmap 内存大小 bitmap.getAllocationByteCount()
四. Bitmap 在内存中的缓存管理
4.1 构建单例的缓存工具类 ImageCache
public class ImageCache { private static ImageCache instance; /** * 单例类 * * @return */ public static ImageCache getInstance() { if (instance == null) { synchronized (ImageCache.class) { if (instance == null) { instance = new ImageCache(); } } } return instance; } }
4.2 利用软引用队列存储 Bitmap,磁盘缓存和内存缓存用的是最少使用算法
private LruCache<String, Bitmap> lruCache; private Set<WeakReference<Bitmap>> reusablePool; private DiskLruCache diskLruCache;
4.3 LruCache 算法存储 Bitmap
首先我们需要计算我们 Android 手机可用内存大小,然后将内存分配给我们的 LruCache 最大缓存空间 LruCache 算法不同版本间做了一系列的变更
- Android 3.0 bitmap 缓存在 native 层
- Android 8.0 bitmap 缓存在 java 层
- Android 8.0 bitmap 缓存在 native 层
我们统计当前 Bitmap 使用了多少内存大小是使用 sizeOf,将 getAllocationByteCount 返回给 lruCache 即可
如果最少使用的缓存被清除时,我们要把可用缓存塞到我们的可重用缓存池
队列的获取是通过线程池来操作的,当前线程如果没有被打断要从软引用队列里面移除并且回收 Bitmap,这样能保证队列里面的 Bitmap 永远是最新的 Bitmap 对象
4.4 获取采样大小 inSampleSize
获取采样大小相对比较简单,我们只要迭代我们的可重用缓存
然后检查 Bitmap 是否满足以下条件
- 3.0 之前不能复用
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { return bitmap.getWidth() == w && bitmap.getHeight() == h && inSampleSize == 1; }
- 3.0-4.4 宽高一样,inSampleSize = 1
if (inSampleSize > 1) { w /= inSampleSize; h /= inSampleSize; }
- 4.4 只要小于等于就行了
int byteCount = w * h * getBytesPerPixel(bitmap.getConfig()); // 图片内存 系统分配内存 return byteCount <= bitmap.getAllocationByteCount();
4.5 内存缓存存放
public void putBitmap2Memory(String key, Bitmap bitmap) { lruCache.put(key, bitmap); }
4.6 内存缓存获取
public Bitmap getBitmapFromMemory(String key) { return lruCache.get(key); }
4.7 磁盘缓存存放
4.8 磁盘缓存获取
4.9 像素格式计算每一个像素占用多少字节
之前说过了 ARGB_8888 其实是占 4 个字节的,其余都是占 2 个字节,所以通过像素格式计算每一个像素占用多少字节是这么计算的
五. Bitmap 的缓存
5.1 LRUCache 算法
六. Bitmap 内存优化
7.1 磁盘缓存
概念梳理
缓存的内容直接写到磁盘中,存放的时间久,不会随着 APP 进程死亡而消失。如果磁盘缓存中没有,就会从网络中或者 SD 卡中获取资源了。同样使用的是 Lru 算法。
封装 DiskLruCache
因为 DiskLruCache 没有提供熟悉的 get、put 方法,所以我们对 DiskLruCache 进行二次封装,提供 get、put 方法给外界调用
7.2 长图加载
7.2.1 长图加载中需要注意的地方
长图加载,我们实现的是 GestureDetector.OnGestureListener 和 View.OnTouchListener 手势监听
然后将 局部 View 以 Rect 方式塞到我们的 BitmapRegionDecoder 参数里面
下面我们就实现一下我们的长图加载的自定义 View 吧
7.2.1 实现 GestureDetector.OnGestureListener, View.OnTouchListener 接口
7.2.2 onMeasure
在 onMeasure 通过缩放因子确定矩阵的宽高
7.2.3 onScroll
我们再手势滑动过程中,要不断的确定滑动区域的宽高,然后进行重新绘制工作
View 的 onTouch 直接交给 长按滑动事件 mGestureDetector 处理
7.2.4 onDraw
在 onDraw 方法里面,我们进行 inBitmap 初始化配置,再将 mRect 设置在 BitmapRegionDecoder 即可 ,然后使用矩阵进行缩放,最后调用 canvas.drawBitmap 进行 View 的绘制,这样就能完美实现局部刷新效果
7.2.5 computeScroll
完成滑动,我们就可以确定最终 React 矩阵参数了,然后进行相应重绘即可
八. Bitmap.Config 配置
九. Bitmap 大小缩放
不同的项目 Bitmap 缩放大小算法可能不太一样,这里我介绍一下通用的缩放算法,以便于对 Bitmap 知识点扫盲,大型项目一般不这样做,感兴趣可以看一下常用的图片压缩框架源码
- 方式一
- 方式二