一.前言
在 Android 开发中经常会遇到的一个小功能就是加载图片,然后显示在我们指定的 View 上,我们知道对于 Android 应用程序,显示图片的 View 不会太大,而图片则有不同的大小,况且应用程序的也都有一定的内存限制,如果不对图片进行一定的处理,当加载大量图片的时候很容易就 产生 OOM,因此对图片进行压缩达到需要显示的大小左右,在视觉上的差别不会太大,但是可以节约大量的内存空间。
二.图片加载
1.加载大图片
BitmapFactory 提供了多个对图片的解析方法解析方法,
这些方法会参与已经构建的 bitmap 对象分配内存,如果没有对图片进行处理,Bitmap 占用的内存就会非常大,
这时就容易导致 OOM。
压缩方法:
通过将 BitmapFactory.Options 参数的 inJustDecodeBounds 属性设置为 true 就可以让解析方法禁止为 bitmap 分配内存,这是 bitmap 对象就为 null, 但是 Option 对象中的 Width Height ,outMineType 属性都会被赋值,这就可以在加载图片的时候就获取图片的长宽,和 MIME 类型
根据显示大小和 图片大小计算 压缩比例 inSampleSIze
压缩步骤:
- inJustDecodeBounds 属性设为 false 计算图片大小
- 根据显示大小和 图片大小计算 压缩比例 inSampleSIze
- 设为 true 解析图片 ,返回 Bitmap 。
2.图片的存储
在 Android 手机中图片的存储一般为有两种方式,内存和外存 (比如 SD 卡)
- 内存:每个应用程序在运行的时候会申请属于自己一段内存区域,对内存区域的读写相对的比外存要快得多,应用程序在退出后就会回收内存空间,即在内存中保存 Bitmap 并不是一种持久的保存。
- 外存:图片的外存一般是以文件的形式存在,因此读取和写入就涉及一些 IO 操作,速度没有内存块,但是是一种持久的数据保存。
3.图片缓存
为了避免重复的去从网络,文件加载图片,内存缓存可以对图片进行缓存,从内存缓存中读取图片要快得多,核心类是 LruCache ,内存缓存支持一种最少使用算法,算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,
并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。通过这样就可以提高内存缓存的效率。
4.基本流程
实际上对于所有的图片加载的框架都有一个固定的流程,只不过在具体的模块上不同的框架有不同的处理或者优化过程而已,下面就是图片加载框架一个十分抽象的流程模型。
二.Glide
1.简介
Glide 是 Bump Technologies 在 2013 年创建的开源项目,目前已经更新到 4.x 版本,实际上在 3.x 版本的时候就已经很成熟了,本篇基于3.x 版本的源码。Glide 是 Google 基于 Picasso 开发,优化的,因此这两个框架有很多相识的地方。因为 Glide 具有加载速度快和内存开销小等优点,更加适合移动系统, 所以这也是 Google 推荐使用的图片处理框架。
2.原理
关于 Glide 的使用因其易用性,且已经有很多参考资料这里就不再赘述,下面着重于 Glide 的几个优点并结合源码进行说明。
(1)生命周期的管理
Glide 和其他图片加载框架一个不同的地方就是具有对加载生命周期的管理,通常是结合 Activity 和 Fragment 的生命周期,实现加载过程跟随其生命周期的开始而开始,销毁而销毁,做到对资源的及时释放同时也降低的对内存的占用,避免和内存泄露的风险。
具体从 with 方法开始:
1 | public static RequestManager with(Context context) { |
这里有很多重载的方法,主要是对两类 Context 进行不同的处理
- Application Context,如果是 Application 那么 图片加载的生命周期和应用程序一样,所以不推荐直接使用 Application Context 。
- 非 Application Context,向当前的 Activity 创建一个 隐藏的 Fragment ,绑定 图片加载的生命周期,这样和在 Activity 中 Fragments的生命周期是一样的。
下面以 Activity 为例1
2
3
4
5
6
7
8
9
10
11
12
13public RequestManager get(Activity activity) {
if (Util.isOnBackgroundThread() || Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
return get(activity.getApplicationContext());
} else {
//判断activity是否已经是销毁状态
assertNotDestroyed(activity);
//获取FragmentManager 对象
android.app.FragmentManager fm = activity.getFragmentManager();
//进行生命周期的绑定
return fragmentGet(activity, fm);
}
}
具体接着看 fragmentGet
1 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) |
在 RequestManagerFragment 的具体实现中就是
1 | public class RequestManagerFragment extends Fragment { |
总结:可以看到对于 Glide 生命周期的管理,就是在 with 方法将 Context 类型的对象传进来的时候,根据不同的 Context 绑定其生命周期,比如 Activity 的就是在 Activity 上 创建一个隐形的 Fragment ,因为 Fragment 的生命周期是由 Activity 管理的,所以将 Glide 的回调接口与 Fragment 的进行绑定,也就间接形成了 Activity 的绑定。
(2)高效的缓存策略
Glide 有着高效的缓存策略,这使得 Glide 在运行起来相对的流畅,而且不会占用大量的内存。图片加载一般会有两种缓存,一是内存缓存,二是本地缓存,而 Glide 还对增加了一个 Bitmap 对象缓存。
1.内存缓存
内存缓存的主要作用是防止应用重复将图片数据读取到内存当中。内存缓存的核心类就是 LruCache,而且使用近期最少使用算法,这个算法的核心就是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。
1 | // load 方法 |
我们都知道 Glide 的图片加载是从 load 方法开始的,最终会到 Engine 的 load 去实现真正的内存缓存。可以看到关于内存缓存的方法有两个就是
- loadFromCache,保存图片的是 LruCache 内部就是一个前引用的
- loadFromActiveResources, 保存图片的是 activeResources ,一个弱引用的 HashMap 。
下面看具体的方法和两者的关系
1 | private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) { |
可以看到在 loadFromCache 中首先会去从 LruResourceCache 中获取到缓存图片之后会将它从缓存中移除,然后添加到 activeResources 当中一个弱引用的 HashMap,
用来缓存正在使用中的图片。可能你会觉得直接取出来不久行了,为什么要多此一举?实际上要采用的这样的方式是因为 LruCache 内部是一个强引用的,而对于 activeResources 是一个弱引用的队列。一张图片在使用的时候被回收的概率非常小,因此使用弱引用可以避免正在使用的图片过多而造成内存溢出。
上面是读取的操作,写入的时候就把在使用的图片就放到 activeResources 弱引用缓存当中,如果图片不再使用就将缓存图片从 activeResources 中移除,
然后再将它 put 到 LruResourceCache 当中。
2.本地缓存
本地缓存也叫硬盘缓存,主要作用是防止应用重复从网络或其他地方重复下载和读取数据。在 3.x 版本的 Glide 中,本地缓存可以有四个设置参数
- NONE,表示不缓存任何内容
- SOURCE,表示只缓存原始图片
- RESULT,表示只缓存转换过后的图片(默认选项)
- ALL, 表示既缓存原始图片,也缓存转换过后的图片
而在 4.x 版本中又增加了一个可以根据图片的资源智能地选择一个加载的策略。
在硬盘缓存读取的过程中,如果是缓存原始的图片,也就是没有经过目标
View .就先进行转换解码再返回,如果是已经根据目标 View 转换过的就直接将数据解码返回。
本地缓存实际上就是将图片进行压缩然后以文件的形式存储,Glide 默认使用应用的私有路径,当然也可以更改为其他指定的路径。
3.Bitmap 对象池
Bitmap 对象是在图片处理的过程中经常见到的对象,之前也说过这个对象十分的消耗内存,而且相对于图片加载这个场景需要对图片频繁的使用/收回(就是显示与不显示),就需要对这些对象进行一个缓存。Glide 对 Bitmap 也 使用对象池的机制,对这种频繁需要创建和销毁的对象保存在一个对象池中。每次用到该对象时,就取对象池空闲的对象,并对它进行初始化操作,从而提高框架的性能。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public interface BitmapPool {
long getMaxSize();
void setSizeMultiplier(float sizeMultiplier);
void put(Bitmap bitmap);
Bitmap get(int width, int height, Bitmap.Config config);
void clearMemory();
void trimMemory(int level);
}
上面就是一个 Bitmap 对象池定义的接口1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25@Override
@NonNull
public Bitmap get(int width, int height, Bitmap.Config config) {
Bitmap result = getDirtyOrNull(width, height, config);
if (result != null) {
// Bitmaps in the pool contain random data that in some cases must be cleared for an image
// to be rendered correctly. we shouldn't force all consumers to independently erase the
// contents individually, so we do so here. See issue #131.
result.eraseColor(Color.TRANSPARENT);
} else {
result = createBitmap(width, height, config);
}
return result;
}
@NonNull
@Override
public Bitmap getDirty(int width, int height, Bitmap.Config config) {
Bitmap result = getDirtyOrNull(width, height, config);
if (result == null) {
result = createBitmap(width, height, config);
}
return result;
}
可以看到当需要一个 Bitmap 对象的时候,会根据图片的这些参数,从 Bitmap 池里找到一个合适的 Bitmap 对象,如果没有就重新创建一个。BitmapPool 也会根据 LRU 算法和缓存池的尺寸来释放一些老旧资源。从而达到性能的最优。
通过这种方式可以以最小的内存开销达到较高的性能,而且有效的降低的内存抖动,即在短时间内频繁的分配大量的内存。因此也降低了系统回收的频率。