有时候我们加载照片的时候,假如我们的 `ImageView`尺寸是`200*150`,但是我们要加载的图片是 `2400*1440` 的,那我们加载的时候,是不需要完全加载原图尺寸的,只需要加载合适的尺寸就可以,这个时候可以讲图片转换为对`Bitmap`,并进行一定程度的压缩,在加载即可,可以有效的防止`OOM`的出现。
在构造`Bitmap`的时候,我们通常是需要`BitmapFactory`这个工厂类来生成`Bitmap`对象的,比如`SD`卡中的图片可以使用`decodeFile`方法,网络上的图片可以使用`decodeStream`方法,资源文件中的图片可以使用`decodeResource`方法。这些方法会尝试为已经构建的`bitmap`分配内存,这时就会很容易导致`OOM`出现。
为此每一种解析方法都提供了一个可选的`BitmapFactory.Options`参数,将这个参数的`inJustDecodeBounds`属性设置为`true`就可以让解析方法禁止为`bitmap`分配内存,返回值也不再是一个`Bitmap`对象,而是`null`。虽然`Bitmap`是`null`了,但是`BitmapFactory.Options的outWidth、outHeight和outMimeType`属性都会被赋值。这个技巧让我们可以在加载图片之前就获取到图片的长宽值和`MIME`类型,从而根据情况对图片进行压缩,代码:
```java
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
// 这里会返回 null,所以不做接收,仅仅是为了拿到 options
BitmapFactory.decodeResource(getResources(),R.string.ic_bitmap,options);
//要加载图片的宽
int imageWidth = options.outWidth;
//要加载图片的高
int imageHeight = options.outHeight;
//要加载图片的 Mime 类型
String imageType = options.outMimeType;
```
这里看到了一个属性是`inJustDecodeBounds`,官方解释:
```java
/**
* If set to true, the decoder will return null (no bitmap), but
* the <code>out...</code> fields will still be set, allowing the caller to
* query the bitmap without having to allocate the memory for its pixels.
*/
// 如果设置为 `true`,那么解码器就会返回一个`null`,但是一些 `out..`字段会被赋值,允许调用者在不为`Bitmap`分配内存的时候查询这个`Bitmap`。
public boolean inJustDecodeBounds;
```
上面已经拿到了`Bitmap`的`width、height、mime`,这个时候,我们就可以决定是加载原图还是加载压缩后的`Bitmap`到内存中,可以从以下角度去考虑:
- 预估一下加载整张图片所需占用的内存。
- 为了加载这一张图片你所愿意提供多少内存。
- 用于展示这张图片的控件的实际大小。
- 当前设备的屏幕尺寸和分辨率。
以`ImageView`尺寸是`200*150`,图片尺寸为 `2400*1440` 为例。我们可以寻找一个合适的尺寸压缩原图,然后生成 `Bitmap`去加载。
这里涉及到一个属性是`inSimpleSize`。
官网解释:
```java
/**
* If set to a value > 1, requests the decoder to subsample the original
* image, returning a smaller image to save memory. The sample size is
* the number of pixels in either dimension that correspond to a single
* pixel in the decoded bitmap. For example, inSampleSize == 4 returns
* an image that is 1/4 the width/height of the original, and 1/16 the
* number of pixels. Any value <= 1 is treated the same as 1. Note: the
* decoder uses a final value based on powers of 2, any other value will
* be rounded down to the nearest power of 2.
*/
public int inSampleSize;
```
大概意思是讲:
如果这个值是大于 1 的,那么会请求解码器对原始图片进行二次采样,返回较小的图片以便节省内存,这个`inSimpleSize`是任一维度中对应于解码位图中的单个像素的像素数。例如:`inSimpleSize == 4`返回的图片是原图高度/宽度的1/4,像素数量为1/4 * 1/4 = 1/16,如果`inSimpleSize< 1`那么会被当做 1 来处理,即不做任何处理。注意:解码器使用基于2的幂的最终值,任何其他值将**向下**舍入到最接近的2的幂。
具体这样写,计算`inSimpleSize`:
```java
public static int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
// 源图片的高度和宽度
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
// 计算出实际宽高和目标宽高的比率
final int heightRatio = Math.round((float) height / (float) reqHeight);
final int widthRatio = Math.round((float) width / (float) reqWidth);
// 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高
// 一定都会大于等于目标的宽和高。
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
}
return inSampleSize;
}
```
得到`inSimpleSize`以后,结合上面的代码:
```java
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// 第一次解析将inJustDecodeBounds设置为true,来获取图片大小
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// 调用上面定义的方法计算inSampleSize值
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 使用获取到的inSampleSize值再次解析图片
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
```
使用:
```java
// ic_bitmap 就是我们要加载的图片,200*100 就是我们想要的图片最终需要的缩略图
mImageView.setImageBitmap(decodeSampledBitmapFromResource(getResources(), R.id.ic_bitmap, 200, 150));
```
#### LruCache 算法的原理
即:`Least recently used` 最近最少使用,会在每次 `put`的时候进行检查,超过设置的大小,就将最近最少使用的位于队尾的元素清除掉。
##### 为什么能够实现这个效果呢?或者问为什么会选择使用 LinkedHashMap 呢?
因为在 LruCache 初始化 LinkedHashMap 的时候,传入了一个变量叫 accessOrder 为 trye ,当 accessOrder 为 true 的时候,在每次从 LinkedHashMap 调用 get 方法的时候,都会对链表内的数据进行排序:
```java
// 获取一个存入在 LinkedHashMap 的对象
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
// 把获取的 LinkedHashMap 对象移动到队头,保证最后被删除
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMapEntry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMapEntry<K,V> p =
(LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
```
可以看到会把当前 get 获得的节点移动到队列尾部,保证了我们经常使用的值在队列尾部最后被移除。而最经常不使用的在队头,当超过设置的最大内存值得时候,优先被移除。
##### 那 LruCache 是怎么实现在一定内存缓存的呢?
在我们每次 put 的时候,都会调用 trimToSize() 去计算当前 LruCache 占用的内存大小,当超过我们设定的内存最大值的时候,就会把队尾的数据给移除掉,保证了占用内存的大小,当没超过我们设置的最大值的时候,就什么都不做。
#### 总结
## LruCache 中维护了一个集合 LinkedHashMap ,该 LinkedHashMap 是以访问顺序排序的。当调用 put() 方法时,就会在结合中添加元素,并调用 trimToSize() 判断缓存是否已满,如果满了就用 LinkedHashMap 的迭代器删除队尾元素,即最近最少访问的元素。当调用 get() 方法访问缓存对象时,就会调用 LinkedHashMap 的 get() 方法获得对应集合元素,同时会更新该元素到队头。
- Java 面试题
- String、StringBuffer、StringBuilder 的区别?
- Java 中的四种引用
- 接口和抽象类的本质区别
- 集合框架
- 集合概述
- ArrayList 源码分析
- LinkedList 源码分析
- HashMap 源码分析
- LinkedHashMap 源码分析
- Android提供的 LruCache 的分析
- LinkedList 和 ArrayList 的区别
- 多线程
- 实现多线程的几种方式
- 线程的几种状态
- Thread 的 start() 和 run() 的区别
- sleep() 、yield() 和 wait() 的区别 ?
- notify() 和 notifyAll() 的区别?
- 保证线程安全的方式有哪几种?
- Synchronized 关键字
- volatile 和 synchronized 的区别?
- 如何正确的终止一个线程?
- ThreadLocal 原理分析
- 线程池
- 多线程的三个特征
- 五种线程池,四种拒绝策略,三种阻塞队列
- 给定三个线程如何顺序执行完以后在主线程拿到执行结果
- Java 内存模型
- 判定可回收对象算法
- equals 与 == 操作符
- 类加载机制
- 类加载简单例子
- 算法
- 时间、空间复杂度
- 冒泡排序
- 快速排序
- 链表反转
- IO
- 泛型
- Kolin 面试题
- Android 面试题
- Handler 线程间通信
- Message、MessageQueue、Looper、Handler 的对象关系
- Handler 使用
- Handler 源码分析
- HandlerThread
- AsyncTask
- IntentService
- 三方框架
- Rxjava
- rxjava 操作符有哪些
- 如何解决 RxJava 内存泄漏
- Rxjava 线程切换原理
- map和 flatmap 的区别
- Databinding引起的 java方法大于 65535 的问题
- Glide
- Glide 的缓存原理
- Glide 是如何和生命周期绑定的?不同的Context 有什么区别?
- Glide 、Picasso 、的区别,优劣势,如何选择?
- Jetpack
- 源码分析
- EventBus
- EventBus 源码分析
- RxBus 替代 EventBus
- OkHttp
- OkHttp 源码分析
- OkHttp 缓存分析
- RxPermission
- RxPermission 源码分析
- Retrofit
- create
- Retrofit 源码分析
- 优化
- 启动优化
- 布局优化
- 绘制优化
- 内存优化
- 屏幕适配
- 组件
- Activity
- Frgment
- Service
- ContentProvider
- BroadcastReceiver
- 进程间通信
- Binder机制和AIDL
- AILD 中的接口和普通的接口有什么区别
- in、out、inout 的区别
- Binder 为什么只需要拷贝一次
- 在android中,请简述jni的调用过程
- 生命周期
- Activity 生命周期
- Fragment 生命周期
- Service 生命周期
- onSaveInstanceState() 与 onRestoreIntanceState()
- 前沿技术
- 组件化
- 模块化
- 插件化
- 热更新
- UI - View
- Android 动画
- 事件分发机制
- WebView
- 系统相关
- 谈谈对 Context 的理解
- Android 版本
- App应用启动流程
- App 的打包
- App 的加固
- App 的安装
- Activity 启动流程
- ClassLoader
- Lru 算法加载 Bitmap 三级缓存原理
- Parcelable 和 Serializable 的区别
- Activity的启动流程
- 相关概念
- 网络相关
- Http
- Https
- Http 和 Https 的区别
- 为什么要进行三次握手和四次挥手?
- OkHttp使用Https访问服务器时信任所有证书
- 设计模式
- 单例模式
- 构建者模式
- 工厂模式
- 外观模式
- 代理模式
