介绍一下比较不好理解的属性:
1inJustDecodeBounds: 这个属性表示是否只扫描轮廓, 默认为 false. 如果该属性为 true,decodeXXXX 方法不会返回一个 Bitmap 对象 (即不会为 Bitmap 分配内存) 而是返回 null. 那如果 decodeXXXX 方法不再分配内存以创建一个 Bitmap 对象, 那么还有什么用呢? 答案就是: 扫描轮廓.
BitmapFactory.Options 对象的 outWidth 和 outHeight 属性分别代表 Bitmap 对象的宽和高, 但是这两个属性在 Bitmap 对象未创建之前显然默认为 0, 默认只有在 Bitmap 对象创建后才能被赋予正确的值. 而当 inJustDecodeBounds 属性为 true, 虽然不会分配内存创建 Bitmap 对象, 但是会扫描轮廓来给 outWidth 和 outHeight 属性赋值, 就相当于绕过了 Bitmap 对象创建的这一步提前获取到 Bitmap 对象的宽高值. 那这个属性到底有啥用呢? 具体用处体现在 Bitmap 的采样率计算中, 后面会详细介绍.
2inSample: 这个表示 Bitmap 的采样率, 默认为 1. 比如说有一张图片是 2048 像素 X1024 像素, 那么默认情况下该图片加载到内存中的 Bitmap 对象尺寸也是 2048 像素 X1024 像素. 如果采用的是 ARGB_8888 方式, 那么该 Bitmap 对象加载所消耗的内存为 2048X1024X4/1024/1024=8M. 这只是一张图片消耗的内存, 如果当前活动需要加载几张甚至几十张图片, 那么会导致严重的 OOM 错误.
OOM 错误: 尽管 Android 设备内存大小可能达到好几个 G(比如 4G), 但是 Andorid 中每个应用其运行内存都有一个阈值, 超过这个阈值就会引发 out of memory 即 OOM 错误(内存溢出错误). 因为现在市场上流行的手机设备其操作系统都是在 Andori 原生操作系统基础上的拓展, 所以不同的设备环境中这个内存阈值不一样. 可以通过以下方法获取到当前应用所分配的内存阈值大小, 单位为字节: Runtime.getRuntime().maxMemory();
尽管我们确实可以通过设置来修改这个阈值大小以提高应用的最大分配内存(具体方式是在在 Manifest 中设置 Android.largeHeap="true"), 但是需要注意的是: 内存是一种很宝贵的资源, 不加考虑地无脑给每个应用提高最大分配内存是一个糟糕的选择. 因为手机总内存相比较每个应用默认的最大分配内存虽然高很多, 但是手机中的应用数量是非常多的, 每个应用都修改其运行内存阈值为几百 MB 甚至一个 G, 这很严重影响手机性能! 另外, 如果应用的最大分配内存很高, 这意味着其垃圾回收工作也会变得更加耗时, 这也会影响应用和手机的性能. 所以, 这个方案需要慎重考虑不能滥用.
关于这个方案的理解可以参考一位大神的解释:"在一些特殊的情景下, 你可以通过在 manifest 的 application 标签下添加 largeHeap=true 的属性来为应用声明一个更大的 heap 空间. 然后, 你可以通过 getLargeMemoryClass()来获取到这个更大的 heap size 阈值. 然而, 声明得到更大 Heap 阈值的本意是为了一小部分会消耗大量 RAM 的应用 (例如一个大图片的编辑应用). 不要轻易的因为你需要使用更多的内存而去请求一个大的 Heap Size. 只有当你清楚的知道哪里会使用大量的内存并且知道为什么这些内存必须被保留时才去使用 large heap. 因此请谨慎使用 large heap 属性. 使用额外的内存空间会影响系统整体的用户体验, 并且会使得每次 gc 的运行时间更长. 在任务切换时, 系统的性能会大打折扣. 另外, large heap 并不一定能够获取到更大的 heap. 在某些有严格限制的机器上, large heap 的大小和通常的 heap size 是一样的. 因此即使你申请了 large heap, 你还是应该通过执行 getMemoryClass() 来检查实际获取到的 heap 大小."
综上, 我们已经知道了 Bitmap 的加载是一个很耗内存的操作, 特别是在大位图的情况下. 这很容易引发 OOM 错误, 而我们又不能轻易地通过修改或提供应用的内存阈值来避免这个错误. 那么我们该怎么做呢? 答案就是: 利用这里所说的采样率属性来创建一个原 Bitmap 的子采样版本. 这也是官方推荐的对于大位图加载的 OOM 问题的解决方案. 其具体思想为: 比如还是那张尺寸为 2048 像素 X1024 像素图片, 在 inSample 值默认为 1 的情况下, 我们现在已经知道它加载到内存中默认是一个 2048 像素 X1024 像素大位图了. 我们可以将 inSample 设置为 2, 那么该图片加载到内存中的位图宽高都会变成原宽高的 1/2, 即 1024 像素 X512 像素. 进一步, 如果 inSample 值设置为 4, 那么位图尺寸会变成 512 像素 X256 像素, 这个时候该位图所消耗的内存 (假设还是 ARGB_8888 方式) 为 512X256X4/1024/1024=0.5M, 可以看出从 8M 到 0.5M, 这极大的节省了内存资源从而避免了 OOM 错误.
切记: 官方对于 inSample 值的要求是, 必须为 2 的幂, 比如 2,4,8... 等整数值.
这里会有两个疑问: 第一: 通过设置 inSample 属性值来创建一个原大位图的子采样版本的方式来降低内存消耗, 听不上确实很不错. 但是这不会导致图片严重失真吗? 毕竟你丢失了那么多像素点, 这意味着你丢失了很多颜色信息. 对这个疑问的解释是: 尽管在采样的过程确实会丢失很多像素点, 但是原位图的尺寸也在减小, 其像素密度是不变的. 比如说如果 inSample 值为 2, 那么子采样版本的像素点数量是原来的 1/4, 但是子采样版本的显示尺寸 (区域面积) 也会变成原来的 1/4, 这样的话像素密码是不变的因此图片不用担心严重失真问题. 第二: inSample 值如何选取才是最佳? 这其实取决于 ImageView 的尺寸, 具体采样率的计算方式后面会详细介绍.
3inPreferredConfig: 该属性指定 Bitmap 的色深值, 该属性类型为 Bitmap.Config 值.
例如你可以指定某图片加载为 Bitmap 对象的色深模式为 ARGB_8888, 即: options.inPreferredConfig=Bitmap.Config.ARGB_8888;
4isMutable: 该属性表示通过 decodeXXXX 方法创建的 Bitmap 对象其代表的图片内容是否允许被外部修改, 比如利用 Canvas 重新绘制其内容等. 默认为 false, 即不允许被外部操作修改.
利用这些属性定制 BitmapFactory.Options 对象, 从而灵活地按照自己的需求配置创建的 Bitmap 对象.
五, Bitmap 的进阶使用
1, 高效地加载大位图
上面刚说了大位图加载时的 OOM 问题, 解决方式是通过 inSample 属性创建一个原位图的子采样版本以减低内存. 那么这里的采样率 inSample 值如何选取最好呢? 这里我们利用官方推荐的采样率最佳计算方式: 基本步骤就是:1获取位图原尺寸 2获取 ImageView 即最终图片显示的尺寸 3依据两种尺寸计算采样率(或缩放比例).
- public static int calculateInSampleSize(
- BitmapFactory.Options options, int reqWidth, int reqHeight) {
- // 位图的原宽高通过 options 对象获取
- final int height = options.outHeight;
- final int width = options.outWidth;
- int inSampleSize = 1;
- if (height> reqHeight || width> reqWidth) {
- final int halfHeight = height / 2;
- final int halfWidth = width / 2;
- // 当要显示的目标大小和图像的实际大小比较接近时, 会产生没必要的采样, 先除以 2 再判断以防止过度采样
- while ((halfHeight / inSampleSize)>= reqHeight
- && (halfWidth / inSampleSize)>= reqWidth) {
- inSampleSize *= 2;
- }
- }
- return inSampleSize;
- }
依据上面的最佳采样率计算方法, 进一步可以封装出利用最佳采样率创建子采样版本再创建位图对象的方法, 这里以从项目图片资源文件加载 Bitmap 对象为例:
- public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
- int reqWidth, int reqHeight) {
- final BitmapFactory.Options options = new BitmapFactory.Options();
- options.inJustDecodeBounds = true;
- // 因为 inJustDecodeBounds 为 true, 所以不会创建 Bitmap 对象只会扫描轮廓从而给 options 对象的宽高属性赋值
- BitmapFactory.decodeResource(res, resId, options);
- // 计算最佳采样率
- options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
- // 记得将 inJustDecodeBounds 属性设置回 false 值
- options.inJustDecodeBounds = false;
- return BitmapFactory.decodeResource(res, resId, options);
- }
2,Bitmap 加载时的异步问题
由于图片的来源有三种, 如果是项目图片资源文件的加载, 一般采取了子采样版本加载方案后不会导致 ANR 问题, 毕竟每张图加载消耗的内存不会很大了. 但是对于本地图片文件和网络图片资源, 由于分别涉及到文件读取和网络请求, 所以属于耗时操作. 为了避免 ANR 的产生, 必须将图片加载为 Bitmap 对象的过程放入工作线程中; 获取到 Bitmap 对象后再回到 UI 线程设置 ImageView 的显示. 举个例子, 如果采用 AsyncTask 作为我们的异步处理方案, 那么代码如下:
- class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
- private final ImageView iv;
- private int id = 0;
- public BitmapWorkerTask(ImageView imageView) {
- iv = imageView;
- }
- // Decode image in background.
- @Override
- protected Bitmap doInBackground(Integer... params) {
- id = params[0];
- // 假设 ImageView 尺寸为 500X500, 为了方便还是以项目资源文件的加载方式为例, 因为这可以复用上面封装的方法
- return decodeSampledBitmapFromResource(getResources(), id, 500, 500);
- }
- @Override
- protected void onPostExecute(Bitmap bitmap) {
- iv.setImageBitmap(bitmap);
- }
- }
该方案中, doInBackground 方法执行在子线程, 用来处理 "图片文件读取操作 + Bitmap 对象的高效加载操作" 或 "网络请求图片资源操作 + Bimap 对象的高效加载操作" 等两种情形下的耗时操作. onPostExecute 方法执行在 UI 线程, 用于设置 ImageView 的显示内容. 看上去这个方案很完美, 但是有一个很隐晦的严重问题:
由当前活动启动了 BitmapWorkerTask 任务后: 当我们退出当前活动时, 由于异步任务只依赖于 UI 线程所以 BitmapWorkerTask 任务会继续执行. 正常的操作是遍历当前活动实例的对象图来释放各对象的内存以销毁该活动, 但是由于当前活动实例的 ImageView 引用被 BitmapWorkerTask 对象持有, 而且还是强引用关系. 这会导致 Activity 实例无法被销毁, 引发内存泄露问题. 内存泄露问题会进一步导致内存溢出错误.
为了解决这个问题, 我们只需要让 BitmapWorkerTask 类持有 ImageView 的弱引用即可. 这样当活动退出时, BitmapWorkerTask 对象由于持有的是 ImageView 的弱引用, 所以 ImageView 对象会被回收, 继而 Activity 实例得到销毁, 从而避免了内存泄露问题. 具体修改后的代码如下:
- class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
- private final WeakReference<ImageView> imageViewReference;
- private int data = 0;
- public BitmapWorkerTask(ImageView imageView) {
- // 用弱引用来关联这个 imageview! 弱引用是避免 Android 在各种 callback 回调里发生内存泄露的最佳方法!
- // 而软引用则是做缓存的最佳方法 两者不要搞混了!
- imageViewReference = new WeakReference<ImageView>(imageView);
- }
- // Decode image in background.
- @Override
- protected Bitmap doInBackground(Integer... params) {
- data = params[0];
- return decodeSampledBitmapFromResource(getResources(), data, 100, 100);
- }
- @Override
- protected void onPostExecute(Bitmap bitmap) {
- // 当后台线程结束后 先看看 ImageView 对象是否被回收: 如果被回收就什么也不做, 等着系统回收他的资源
- // 如果 ImageView 对象没被回收的话, 设置其显示内容即可
- if (imageViewReference != null && bitmap != null) {
- final ImageView imageView = imageViewReference.get();
- if (imageView != null) {
- imageView.setImageBitmap(bitmap);
- }
- }
- }
- }
拓展:1WeakReference 是弱引用, 其中保存的对象实例可以被 GC 回收掉. 这个类通常用于在某处保存对象引用, 而又不干扰该对象被 GC 回收, 可以用于避免内存泄露.2SoftReference 是软引用, 它保存的对象实例, 不会被 GC 轻易回收, 除非 JVM 即将 OutOfMemory, 否则不会被 GC 回收. 这个特性使得它非常适合用于设计 Cache 缓存. 缓存可以省去重复加载的操作, 而且缓存属于内存因此读取数据非常快, 所以我们自然不希望缓存内容被 GC 轻易地回收掉; 但是因为缓存本质上就是一种内存资源, 所以在内存紧张时我们需要能释放一部分缓存空间来避免 OOM 错误. 综上, 软引用非常适合用于设计缓存 Cache. 但是, 这只是早些时候的缓存设计思想, 比如在 Android2.3 版本之前. 在 Android2.3 版本之后, JVM 的垃圾收集器开始更积极地回收软引用对象, 这使得原本的缓存设计思想失效了. 因为如果使用软引用来实现缓存, 那么动不动缓存对象就被 GC 回收掉实在是无法接受. 所以, Android2.3 之后对于缓存的设计使用的是强引用关系(也就是普通对象引用关系). 很多人会问这样不会由于强引用的缓存对象无法被回收从而导致 OOM 错误吗? 确实会这样, 但是我们只需要给缓存设置一个合理的阈值就好了. 将缓存大小控制在这个阈值范围内, 就不会引发 OOM 错误了.
3, 列表加载 Bitmap 时的图片显示错乱问题
我们已经知道了如何高效地加载位图以避免 OOM 错误, 还知道了如何合理地利用异步机制来避免 Bitmap 加载时的 ANR 问题和内存泄露问题. 现在考虑另一种常见的 Bitmap 加载问题: 当我们使用列表, 如 ListView,GridView 和 RecyclerView 等来加载多个 Bitmap 时, 可能会产生图片显示错乱的问题. 先看一下该问题产生的原因. 以 ListView 为例:
1ListView 为了提高列表展示内容在滚动时的流畅性, 使用了一种 item 复用机制, 即: 在屏幕中显示的每个 ListView 的 item 对应的布局只有在第一次的时候被加载, 然后缓存在 convertView 里面, 之后滑动改变 ListView 时调用的 getView 就会复用缓存在 converView 中的布局和控件, 所以可以使得 ListView 变得流畅(因为不用重复加载布局).
2每个 Item 中的 ImageView 加载图片时往往都是异步操作, 比如在子线程中进行图片资源的网络请求再加载为一个 Bitmap 对象最后回到 UI 线程设置该 item 的 ImageView 的显示内容.
3 听上去1是一种非常合理有效的提高列表展示流畅性的机制,2看起来也是图片加载时很常见的一个异步操作啊. 其实1和2本身都没有问题, 但是1+2+ 用户滑动列表 = 图片显示错乱! 具体而言: 当我们在其中一个 itemA 加载图片 A 的时候, 由于加载过程是异步操作需要耗费一定的时间, 那么有可能图片 A 未被加载完该 itemA 就 "滚出去了", 这个 itemA 可能被当做缓存应用到另一个列表项 itemB 中, 这个时候刚好图片 A 加载完成显示在 itemB 中(因为 ImageView 对象在缓存中被复用了), 原本 itemB 该显示图片 B, 现在显示图片 A. 这只是最简单的一种情况, 当滑动频繁时这种图片显示错乱问题会愈加严重, 甚至让人毫无头绪.
那么如何解决这种图片显示错乱问题呢? 解决思路其实非常简单: 在图片 A 被加载到 ImageView 之前做一个判断, 判断该 ImageView 对象是否还是对应的是 itemA, 如果是则将图片加载到 ImageView 当中; 如果不是则放弃加载(因为 itemB 已经启动了图片 B 的加载, 所以不用担心控件出现空白的情况).
那么新的问题出现了, 如何判断 ImageView 对象对应的 item 已经改变了? 我们可以采取下面的方式:
1在每次 getView 的复用布局控件时, 对会被复用的控件设置一个标签(在这里就是对 ImageView 设置标签). 标签内容必须可以标识不同的 item! 这里使用图片的 url 作为标签内容, 然后再异步加载图片.
2在图片下载完成后要加载到 ImageView 之前做判断, 判断该 ImageView 的标签内容是否和图片的 url 一样: 如果一样说明 ImageView 没有被复用, 可以将图片加载到 ImageView 当中; 如果不一样, 说明 ListView 发生了滑动, 导致其他 item 调用了 getView 从而将该 ImageView 的标签改变, 此时放弃图片的加载(尽管图片已经被下载成功了).
总结: 解决 ListView 异步加载 Bitmap 时的图片错乱问题的方式是: 为被复用的控件对象 (即 ImageView 对象) 设置标签来标识 item, 异步任务结束后要将图片加载到 ImageView 时取出标签值进行比对是否一致: 如果一致意味着没有发生滑动, 正常加载图片; 如果不一样意味着发生了滑动, 取消加载.
4,Android 中的 Bitmap 缓存策略
如果只是加载若干张图片, 上述的 Bitmap 使用方式已经绝对够用了; 但是如果在应用中需要频繁地加载大量的图片, 特别是有些图片会被重复加载时, 这个时候利用缓存策略可以很好地提高图片的加载速度. 比如说有几张图片被重复加载的频率很高, 那么可以在缓存中保留这几张图片的 Bitmap 对象; 后续如果需要加载这些图片, 则不需要花费很多时间去重新在网络上获取并加载这些图片的 Bitmap 对象, 只需要直接向缓存中获取之前保留下来的 Bitmap 对象即可.
Android 中对 Bitmap 的缓存策略分为两种:
内存缓存: 图像存储在设备内存中, 因此访问速度非常快. 事实上, 比图像解码过程要快得多, 所以将图像存储在这里是让 App 更快更稳定的一个好主意. 内存缓存的唯一缺点是: 它只存活于 App 的生命周期, 这意味着一旦 App 被 Android 操作系统内存管理器关闭或杀死(全部或部分), 那么储存在那里的所有图像都将丢失. 由于内存缓存本质上就是一种内存资源, 所以切记: 内存缓存必须设置一个最大可用的内存量. 否则可能会导致臭名昭著的 outOfMemoryError.
磁盘缓存: 图像存储在设备的物理存储器上(磁盘). 磁盘缓存本质上就是设备 SD 卡上的某个目录. 只要 App 不被卸载, 其磁盘缓存可以一直安全地存储图片, 只要有足够的磁盘空间即可. 缺点是, 磁盘读取和写入操作可能会很慢, 而且总是比访问内存缓存慢. 由于这个原因, 因此所有的磁盘操作必须在工作线程执行, UI 线程之外. 否则, App 会冻结, 并导致 ANR 警报.
在实际使用中, 我们不需要强行二选一, 可以二者都使用, 毕竟各有优势. 所以 Android 中完整的图片缓存策略为: 先尝试在内存缓存中查找 Bitmap 对象, 如果有直接加载使用; 如果没有, 再尝试在磁盘缓存中查找图片文件是否存在, 如果有将其加载至内存使用; 如果还是没有, 则老老实实发送网络请求获取图片资源并加载使用. 需要注意的是, 后面两种情况下的操作都必须使用异步机制以避免 ANR 的发生.
Android 中通过 LruCache 实现内存缓存, 通过 DiskLruCache 实现磁盘缓存, 它们采用的都是 LRU(Least Recently Used)最近最少使用算法来移除缓存中的最近不常访问的内容(变相地保留了最近经常访问的内容).
1内存缓存 LruCache
LruCache 原理: LruCache 底层是使用 LinkedHashMap 来实现的, 所以 LruCache 也是一个泛型类. 在图片缓存中, 其键类型是字符串, 值类型为 Bitmap. 利用 LinkedHashMap 的 accessOrder 属性可以实现 LRU 算法. accessOrder 属性决定了 LinkedHashMap 的链表顺序: accessOrder 为 true 则以访问顺序维护链表, 即被访问过的元素会安排到链表的尾部; accessorder 为 false 则以插入的顺序维护链表.
而 LruCache 利用的正是 accessOrder 为 true 的 LinkedHashMap 来实现 LRU 算法的. 具体表现为:
1° put: 通过 LinkedHashMap 的 put 方法来实现元素的插入, 插入的过程还是要先寻找有没有相同的 key 的数据, 如果有则替换掉旧值, 并且将该节点移到链表的尾部. 这可以保证最近经常访问的内容集中保存在链表尾部, 最近不常访问的内存集中保存在链表头部位置. 在插入后如果缓存大小超过了设定的最大缓存大小 (阈值), 则将 LinkedHashMap 头部的节点(最近不常访问的内容) 删除, 直到 size 小于 maxSize.
2° get: 通过 LinkedHashMap 的 get 方法来实现元素的访问, 由于 accessOrder 为 true, 因此被访问到的元素会被调整到链表的尾部, 因此不常被访问的元素就会留到链表的头部, 当触发清理缓存时不常被访问的元素就会被删除, 这里是实现 LRU 最关键的地方.
3° remove: 通过 LinkedHashMap 的 remove 方法来实现元素的移除.
3° size:LruCache 中很重要的两个成员变量 size 和 maxSize, 因为清理缓存的是在 size>maxSize 时触发的, 因此在初始化的时候要传入 maxSize 定义缓存的大小, 然后重写 sizeOf 方法, 因为 LruCache 是通过 sizeOf 方法来计算每个元素的大小. 这里我们是使用 LruCache 来缓存图片, 所以 sizeOf 方法需要计算 Bitmap 的大小并返回.
LruCache 对其缓存对象采用的是强引用关系, 采用 maxSize 来控制缓存空间大小以避免 OOM 错误. 而且 LruCache 类在 Android SDK 中已经提供了, 在实际使用中我们只需要完成以下几步即可:
设计 LruCache 的最大缓存大小: 一般是通过计算当前可用的内存大小继而来获取到应该设置的缓存大小
创建 LruCache 对象: 传入最大缓存大小的参数, 同时重写 sizeOf 方法来设置存在 LruCache 里的每个对象的大小
封装对 LruCache 的数据访问和添加操作并对外提供接口以供调用
具体代码参考如下:
- // 初始化 LruCache 对象
- public void initLruCache()
- {
- // 获取当前进程的可用内存, 转换成 KB 单位
- int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
- // 分配缓存的大小
- int maxSize = maxMemory / 8;
- // 创建 LruCache 对象并重写 sizeOf 方法
- lruCache = new LruCache<String, Bitmap>(maxSize)
- {
- @Override
- protected int sizeOf(String key, Bitmap value) {
- // TODO Auto-generated method stub
- return value.getWidth() * value.getHeight() / 1024;
- }
- };
- }
- /**
- * 封装将图片存入缓存的方法
- * @param key 图片的 url 转化成的 key
- * @param bitmap 对象
- */
- private void addBitmapToMemoryCache(String key, Bitmap bitmap)
- {
- if(getBitmapFromMemoryCache(key) == null)
- {
- mLruCache.put(key, bitmap);
- }
- }
- // 封装从 LruCache 中访问数据的方法
- private Bitmap getBitmapFromMemoryCache(String key)
- {
- return mLruCache.get(key);
- }
- /**
- * 因为外界一般获取到的是 url 而不是 key, 因此为了方便再做一层封装
- * @param url http url
- * @return bitmap
- */
- private Bitmap loadBitmapFromMemoryCache(String url)
- {
- final String key = hashKeyFromUrl(url);
- return getBitmapFromMemoryCache(key);
- }
2磁盘缓存 DiskLruCache
由于 DiskLruCache 并不属于 Android SDK 的一部分, 需要自行设计. 与 LruCache 实现 LRU 算法的思路基本上是一致的, 但是有很多不一样的地方: LruCache 是内存缓存, 其键对应的值类型直接为 Bitmap; 而 DiskLruCache 是磁盘缓存, 所以其键对应的值类型应该是一个代表图片文件的类. 其次, 前者访问或添加元素时, 查找成功可以直接使用该 Bitmap 对象; 后者访问或添加元素时, 查找到指定图片文件后还需要通过文件的读取和 Bitmap 的加载过程才能使用. 另外, 前者是在内存中的数据读写操作所以不需要异步; 后者涉及到文件操作必须开启子线程实现异步处理.
具体 DiskLruCache 的设计方案和使用方式可以参考这篇博客: https://www.jianshu.com/p/765640fe474a
有了 LruCache 类和 DiskLruCache 类, 可以实现完整的 Android 图片二级缓存策略: 在具体的图片加载时: 先尝试在 LruCache 中查找 Bitmap 对象, 如果有直接拿来使用. 如果没有再尝试在 DiskLruCache 中查找图片文件, 如果有将其加载为 Bitmap 对象再使用, 并将其添加至 LruCache 中; 如果没有查找到指定的图片文件, 则发送网络请求获取图片资源并加载为 Bitmap 对象再使用, 并将其添加 DiskLruCache 中.
5,Bitmap 内存管理
Android 设备的内存包括本机 Native 内存和 Dalvik(类似于 JVM 虚拟机)堆内存两部分. 在 Android 2.3.3(API 级别 10)及更低版本中, 位图的支持像素数据存储在 Native 内存中. 它与位图本身是分开的, Bitmap 对象本身存储在 Dalvik 堆中. Native 内存中的像素数据不会以可预测的方式释放, 可能导致应用程序短暂超出其内存限制并崩溃. 从 Android 3.0(API 级别 11)到 Android 7.1(API 级别 25), 像素数据与相关 Bitmap 对象一起存储在 Dalvik 堆上, 一起交由 Dalvik 虚拟机的垃圾收集器来进行回收, 因此比较安全.
1在 Android2.3.3 版本之前:
在 Bitmap 对象不再使用并希望将其销毁时, Bitmap 对象自身由于保存在 Dalvik 堆中, 所以其自身会由 GC 自动回收; 但是由于 Bitmap 的像素数据保存在 native 内存中, 所以必须由开发者手动调用 Bitmap 的 recycle()方法来回收这些像素数据占用的内存空间.
2在 Android2.3.3 版本之后:
由于 Bitmap 对象和其像素数据一起保存在 Dalvik 堆上, 所以在其需要回收时只要将 Bitmap 引用置为 null 就行了, 不需要如此麻烦的手动释放内存操作.
当然, 一般我们在实际开发中往往向下兼容到 Android4.0 版本, 所以你懂得.
3在 Android3.0 以后的版本, 还提供了一个很好用的参数, 叫 options.inBitmap. 如果你使用了这个属性, 那么在调用 decodeXXXX 方法时会直接复用 inBitmap 所引用的那块内存. 大家都知道, 很多时候 ui 卡顿是因为 gc 操作过多而造成的. 使用这个属性能避免频繁的内存的申请和释放. 带来的好处就是 gc 操作的数量减少, 这样 CPU 会有更多的时间执行 ui 线程, 界面会流畅很多, 同时还能节省大量内存. 简单地说, 就是内存空间被各个 Bitmap 对象复用以避免频繁的内存申请和释放操作.
需要注意的是, 如果要使用这个属性, 必须将 BitmapFactory.Options 的 isMutable 属性值设置为 true, 否则无法使用这个属性.
具体使用方式参考如下代码:
- final BitmapFactory.Options options = new BitmapFactory.Options();
- //size 必须为 1 否则是使用 inBitmap 属性会报异常
- options.inSampleSize = 1;
- // 这个属性一定要在用在 src Bitmap decode 的时候 不然你再使用哪个 inBitmap 属性去 decode 时候会在 c++ 层面报异常
- //BitmapFactory: Unable to reuse an immutable bitmap as an image decoder target.
- options.inMutable = true;
- inBitmap2 = BitmapFactory.decodeFile(path1,options);
- iv.setImageBitmap(inBitmap2);
- // 将 inBitmap 属性代表的引用指向 inBitmap2 对象所在的内存空间, 即可复用这块内存区域
- options.inBitmap = inBitmap2;
- // 由于启用了 inBitmap 属性, 所以后续的 Bitmap 加载不会申请新的内存空间而是直接复用 inBitmap 属性值指向的内存空间
- iv2.setImageBitmap(BitmapFactory.decodeFile(path2,options));
- iv3.setImageBitmap(BitmapFactory.decodeFile(path3,options));
- iv4.setImageBitmap(BitmapFactory.decodeFile(path4,options));
补充: Android4.4 以前, 你要使用这个属性, 那么要求复用内存空间的 Bitmap 对象大小必须一样; 但是 Android4.4 以后只要求后续复用内存空间的 Bitmap 对象大小比 inBitmap 指向的内存空间要小就可以使用这个属性了. 另外, 如果你不同的 imageview 使用的 scaletype 不同, 但是你这些不同的 imageview 的 bitmap 在加载是如果都是引用的同一个 inBitmap 的话,
这些图片会相互影响. 综上, 使用 inBitmap 这个属性的时候 一定要小心小心再小心.
六, 开源框架
我们现在已经知道了, Android 图片加载的知识点和注意事项实在太多了: 单个的位图加载我们要考虑 Bitmap 加载的 OOM 问题, 异步处理问题和内存泄露问题; 列表加载位图要考虑显示错乱问题; 频繁大量的位图加载时我们要考虑二级缓存策略; 我们还有考虑不同版本下的 Bitmap 内存管理问题, 在这部分最后我们介绍了 Bitmap 内存复用方式, 我们需要小心使用这种方式.
那么, 能不能有一种方式让我们省去这么多繁琐的细节, 方便我们对图片进行加载呢? 答案就是: 利用已有的成熟的图片加载和缓存开源框架! 比如 square 公司的 Picasso 框架, Google 公司的 Glide 框架和 Facebook 公司的 Fresco 框架等. 特别是 Fresco 框架, 提供了三级缓存策略, 非常的专业. 根据 App 对图片显示和缓存的需求从低到高排序, 我们可以采用的方案依次为: Bitmapfun,Picasso,Android-Universal-Image-Loader,Glide,Fresco.
这些框架可以方便我们实现对网络图片的加载和缓存操作. 具体不再赘述.
来源: https://www.cnblogs.com/shakinghead/p/11025805.html