Glide 核心原理速读¶
不陷于茫茫源码之中,只取其核心
1. 总览¶
一般来说,我们使用如下代码加载一张网络图片:
流程大概如下:
Glide 特点¶
一般来说,一个优秀的图片加载库,都会有以下几个特点:
- 支持多种图片格式,如:
jpg、png、webp、gif等 - 支持多种数据源,如:
网络、本地、assets等 - 支持多种缓存策略,如:
内存、磁盘、网络等
而Glide处理具备以上特点外,还有以下特点:
- 生命周期感知(根据Activity或者Fragment的生命周期管理图片加载请求)
- 高效处理Bitmap(bitmap的复用和主动回收,减少系统回收压力)
- 更高效的缓存策略,加载速度快且内存开销小
2. 核心原理¶
2.1 Glide是如何感知生命周期的¶
- 传入Activity/Fragment/Context
Glide#with
public static RequestManager with(@NonNull Activity activity) {
return getRetriever(activity).get(activity); #1
}
RequestManagerRetriever#get
public RequestManager get(@NonNull Activity activity) {
if (Util.isOnBackgroundThread()) {
return get(activity.getApplicationContext());
} else if (activity instanceof FragmentActivity) {
return get((FragmentActivity) activity); #2
} else {
....
}
}
@NonNull
public RequestManager get(@NonNull FragmentActivity activity) {
...
//Glide 终于支持官方的lyfecycle方案了,虽然是实验性的,但不影响分析,就以这个为例吧
if (useLifecycleInsteadOfInjectingFragments()) {
Context context = activity.getApplicationContext();
Glide glide = Glide.get(context);
#4
return lifecycleRequestManagerRetriever.getOrCreate(
context,
glide
activity.getLifecycle(),
activity.getSupportFragmentManager(),
isActivityVisible);
} else {
...
}
}
LifecycleRequestManagerRetriever#getOrCreate
RequestManager getOrCreate(
Context context,
Glide glide,
final Lifecycle lifecycle,
FragmentManager childFragmentManager,
boolean isParentVisible) {
Util.assertMainThread();
RequestManager result = getOnly(lifecycle);
if (result == null) {
//LifecycleLifecycle 是关联Activity生命周期和RequestManager的桥梁
#5
LifecycleLifecycle glideLifecycle = new LifecycleLifecycle(lifecycle);
// 这里会构建RequestManager并且和Lifecycle关联
#6
result =
factory.build(
glide,
glideLifecycle,
new SupportRequestManagerTreeNode(childFragmentManager),
context);
...
}
return result;
}
RequestManager
RequestManager(
Glide glide,
Lifecycle lifecycle,
RequestManagerTreeNode treeNode,
RequestTracker requestTracker,
ConnectivityMonitorFactory factory,
Context context) {
...
// 关联生命周期
#7
lifecycle.addListener(this);
...
}
// 感知生命周期处理请求.
@Override
public synchronized void onStart() {
resumeRequests();
targetTracker.onStart();
}
// 感知生命周期处理请求.
@Override
public synchronized void onStop() {
pauseRequests();
targetTracker.onStop();
}
// 感知生命周期处理请求.
@Override
public synchronized void onDestroy() {
targetTracker.onDestroy();
for (Target<?> target : targetTracker.getAll()) {
clear(target);
}
targetTracker.clear();
requestTracker.clearRequests();
lifecycle.removeListener(this);
lifecycle.removeListener(connectivityMonitor);
Util.removeCallbacksOnUiThread(addSelfToLifecycle);
glide.unregisterRequestManager(this);
}
LifecycleLifecycle 是关联Activity生命周期和RequestManager的桥梁
final class LifecycleLifecycle implements Lifecycle, LifecycleObserver {
@NonNull
private final Set<LifecycleListener> lifecycleListeners = new HashSet<LifecycleListener>();
@NonNull private final androidx.lifecycle.Lifecycle lifecycle;
//这里是#4的 activity.getLifecycle()
LifecycleLifecycle(androidx.lifecycle.Lifecycle lifecycle) {
this.lifecycle = lifecycle;
// LifecycleLifecycle 作为 LifecycleObserver 注册到 Activity Lifecycle
lifecycle.addObserver(this);
}
@OnLifecycleEvent(Event.ON_START)
public void onStart(@NonNull LifecycleOwner owner) {
for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) {
lifecycleListener.onStart();
}
}
@OnLifecycleEvent(Event.ON_STOP)
public void onStop(@NonNull LifecycleOwner owner) {
for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) {
lifecycleListener.onStop();
}
}
@OnLifecycleEvent(Event.ON_DESTROY)
public void onDestroy(@NonNull LifecycleOwner owner) {
for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) {
lifecycleListener.onDestroy();
}
owner.getLifecycle().removeObserver(this);
}
// RequestManager 作为 LifecycleListener 注册到 LifecycleLifecycle,从而间接注册到 Activity Lifecycle
@Override
public void addListener(@NonNull LifecycleListener listener) {
lifecycleListeners.add(listener);
if (lifecycle.getCurrentState() == State.DESTROYED) {
listener.onDestroy();
} else if (lifecycle.getCurrentState().isAtLeast(State.STARTED)) {
listener.onStart();
} else {
listener.onStop();
}
}
@Override
public void removeListener(@NonNull LifecycleListener listener) {
lifecycleListeners.remove(listener);
}
}
小结¶
- 获取对应Activity的
Lifecycle, 把Lifecycle作为 LifecycleObserver 注册到Activity Lifecycle; - 在创建
RequestManager时,RequestManager 作为LifecycleListener注册到 Lifecycle,从而间接注册到 Activity Lifecycle; - 这样当Activity生命周期变化的时候,就能通过接口回调去通知RequestManager处理请求.
以上是Glide支持官方的lyfecycle方案的处理流程,其实默认Glide是自己实现了一套生命周期感知方案,这里不做分析,感兴趣的可以自己去看源码或者查看网上相关文章。
2.2 缓存策略¶
Glide的缓存分为两种,一种是内存缓存,一种是磁盘缓存。其中内存缓存又分为活动资源和LRU内存缓存,磁盘缓存又分为原始数据缓存和转换后的数据缓存。默认情况下,Glide 会在开始一个新的图片请求之前检查以下多级的缓存:
- 活动资源 (
Active Resources) - 现在是否有另一个 View 正在展示这张图片? - 内存缓存 (
Memory cache) - 该图片是否最近被加载过并仍存在于内存中? - 资源类型(
Resource) - 该图片是否之前曾被解码、转换并写入过磁盘缓存? - 数据来源 (
Data) - 构建这个图片的资源是否之前曾被写入过文件缓存?
前两步检查图片是否在内存中,如果是则直接返回图片。后两步则检查图片是否在磁盘上,以便快速但异步地返回图片。
如果四个步骤都未能找到图片,则Glide会返回到原始资源以取回数据(原始文件,Uri, Url等)。
内存缓存¶
- 先从活动资源缓存中获取,如果获取到则直接返回
- 不在活动资源缓存,则尝试从LRU内存缓中获取,如果获取到则返回,从Lru缓存删除并将其加入活动资源缓存
具体代码如下
Engine#loadFromMemory
private EngineResource<?> loadFromMemory(
EngineKey key, boolean isMemoryCacheable, long startTime) {
if (!isMemoryCacheable) {
return null;
}
#1 从活动资源缓存中获取
EngineResource<?> active = loadFromActiveResources(key);
if (active != null) {
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from active resources", startTime, key);
}
return active;
}
#2 从LRU内存缓中获取
EngineResource<?> cached = loadFromCache(key);
if (cached != null) {
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from cache", startTime, key);
}
return cached;
}
return null;
}
private EngineResource<?> loadFromActiveResources(Key key) {
// 从 ActiveResources 中加载缓存数据
EngineResource<?> active = activeResources.get(key);
if (active != null) {
//活动引用计数+1
active.acquire();
}
return active;
}
private EngineResource<?> loadFromCache(Key key) {
EngineResource<?> cached = getEngineResourceFromCache(key);
if (cached != null) {
//活动引用计数+1
cached.acquire();
// 加入活动缓存,注意,这里已经从 LRU 缓存中移除了
activeResources.activate(key, cached);
}
return cached;
}
private EngineResource<?> getEngineResourceFromCache(Key key) {
// cache是LruResourceCache的实现
// 从 LruResourceCache 中加载缓存数据,并移除
Resource<?> cached = cache.remove(key);
final EngineResource<?> result;
if (cached == null) {
result = null;
} else if (cached instanceof EngineResource) {
// Save an object allocation if we've cached an EngineResource (the typical case).
result = (EngineResource<?>) cached;
} else {
result =
new EngineResource<>(
cached, /*isMemoryCacheable=*/ true, /*isRecyclable=*/ true, key, /*listener=*/ this);
}
return result;
}
SingleRequest#clear
Engine#release
EngineResource#release
void release() {
...
if (--acquired == 0) {
// 引用计数归0
release = true;
}
if (release) {
//Engine 实现了ResourceListener
listener.onResourceReleased(key, this);
}
}
Engine#onResourceReleased
public void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
// 从活动缓存中移除
activeResources.deactivate(cacheKey);
if (resource.isMemoryCacheable()) {
// 加入 LRU 缓存
cache.put(cacheKey, resource);
} else {
resourceRecycler.recycle(resource, /*forceNextFrame=*/ false);
}
}
为什么要有活动资源缓存?
- 降低内存压力,防止内存泄漏 ActiveResources 中的 HashMap 是弱引用维护的,而 LruCache 中的 LinkedHashMap 用的是强引用。因为弱引用对象会随时被 gc 回收,所以可以防止内存泄漏
- 提高缓存效率 如果没有活动资源缓存,每一次使用的资源都加入内存缓存,极有可能因为放入Lru缓存的数据过多,导致正在使用资源从Lru缓存中移除,等到下次来进行加载的时候因为没有对应的引用关系,找不到原来内存中正在使用的那个资源,从而需要再次从文件或者网络进行数据加载。
磁盘缓存¶
首先了解一下磁盘缓存策略
DiskCacheStrategy.NONE: 表示不缓存任何内容。DiskCacheStrategy.RESOURCE: 只缓存转换过后的图片。DiskCacheStrategy.DATA: 只缓存原始图片。DiskCacheStrategy.ALL: 既缓存原始图片,也缓存转换过后的图片。DiskCacheStrategy.AUTOMATIC:默认策略,它会尝试对本地和远程图片使用最佳的策略。如果是远程图片,则只缓存原始图片;因为下载远程数据相比调整磁盘上已经存在的数据要昂贵得多. 如果是本地图片,那么只缓存转换过后的图片, 因为即使你需要再次生成另一个尺寸或类型的图片,取回原始数据也很容易。
以RESOURCE为例,核心代码如下
ResourceCacheGenerator#startNext
public boolean startNext() {
...
currentKey =
new ResourceCacheKey( // NOPMD AvoidInstantiatingObjectsInLoops
helper.getArrayPool(),
sourceId,
helper.getSignature(),
helper.getWidth(),
helper.getHeight(),
transformation,
resourceClass,
helper.getOptions());
// 从磁盘缓存中获取 ,DiskCache是DiskLruCacheWrapper的实例,对应的是DiskLruCache的封装
cacheFile = helper.getDiskCache().get(currentKey);
if (cacheFile != null) {
sourceKey = sourceId;
modelLoaders = helper.getModelLoaders(cacheFile);
modelLoaderIndex = 0;
}
...
loadData =
modelLoader.buildLoadData(
cacheFile, helper.getWidth(), helper.getHeight(), helper.getOptions());
if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) {
started = true;
loadData.fetcher.loadData(helper.getPriority(), this);
}
...
}
EngineJobListener#onEngineJobComplete回调完成
public synchronized void onEngineJobComplete(
EngineJob<?> engineJob, Key key, EngineResource<?> resource) {
if (resource != null && resource.isMemoryCacheable()) {
// 加入活动资源缓存
activeResources.activate(key, resource);
}
...
}
为什么需要两种磁盘缓存
- 可以根据不同来源在占用空间和效率上选择最佳方案 如果是远程图片,则只缓存原始图片;因为下载远程数据相比调整磁盘上已经存在的数据要昂贵得多. 如果是本地图片,那么只缓存转换过后的图片, 因为即使你需要再次生成另一个尺寸或类型的图片,取回原始数据也很容易。
- 提高缓存效率
举个例子,同一张图片,我们先在
100*100的View是展示,再在200*200的View上展示, 如果不缓存变换后的类型相当于每次都要进行一次变换操作,如果不缓存原始数据则每次都要去重新下载数据
2.3 内存优化¶
我们先回顾下Bitamp 内存占用分析, 我们知道bitmap计算公式为:
bitmapInRam = bitmapWidth*bitmapHeight*bytesPerPixel
bitmapWidth = 图片宽度/inSampleSize*inTargetDensity/inDensity
bitmapHeight = 图片高度/inSampleSize*inTargetDensity/inDensity
显而易见,假设图片分辨率不变的情况下,那我们可以从inSampleSize和bytesPerPixel两个方面进行优化,另外我们也可以通过inBitmap来复用bitmap,减少bitmap的内存分配,下面看下Glide是如何做的。
尺寸优化(inSampleSize)¶
当图片大小与View大小不一致时,可以用inSampleSize进行尺寸优化, 例如ImageView只有100*100,而图片的分辨率为800 * 800,我们可以通过使用inSampleSize对Bitmap进行尺寸缩放,采样率越高,图片越小。
Downsampler#calculateScaling
private static void calculateScaling(
ImageType imageType,
ImageReader imageReader,
DecodeCallbacks decodeCallbacks,
BitmapPool bitmapPool,
DownsampleStrategy downsampleStrategy,
int degreesToRotate,
int sourceWidth,
int sourceHeight,
int targetWidth,
int targetHeight,
BitmapFactory.Options options)
throws IOException {
...
//获取exactScaleFactor, downsampleStrategy:采样策略,默认为DownsampleStrategy#FIT_CENTER,取决于你ImageView设置的scaleType
final float exactScaleFactor =
downsampleStrategy.getScaleFactor(
orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight);
SampleSizeRounding rounding =
downsampleStrategy.getSampleSizeRounding(
orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight);
if (rounding == null) {
throw new IllegalArgumentException("Cannot round with null rounding");
}
//获取Bitmap输出宽高
int outWidth = round(exactScaleFactor * orientedSourceWidth);
int outHeight = round(exactScaleFactor * orientedSourceHeight);
int widthScaleFactor = orientedSourceWidth / outWidth;
int heightScaleFactor = orientedSourceHeight / outHeight;
// 根据缩放策略是省内存还是高品质,决定取宽高比的最大值还是最小值
int scaleFactor =
rounding == SampleSizeRounding.MEMORY
? Math.max(widthScaleFactor, heightScaleFactor)
: Math.min(widthScaleFactor, heightScaleFactor);
int powerOfTwoSampleSize;
// 在Android M及以下版本,BitmapFactory不支持wbmp格式的图片进行缩放
if (Build.VERSION.SDK_INT <= 23
&& NO_DOWNSAMPLE_PRE_N_MIME_TYPES.contains(options.outMimeType)) {
powerOfTwoSampleSize = 1;
} else {
//对scaleFactor再进行处理,通过highestOneBit()把计算的比例四舍五入到最接近2的幂
powerOfTwoSampleSize = Math.max(1, Integer.highestOneBit(scaleFactor));
if (rounding == SampleSizeRounding.MEMORY
&& powerOfTwoSampleSize < (1.f / exactScaleFactor)) {
//如果缩放策略为省内存,并且计算得出的sampleSize < exactScaleFactor,则sampleSize再增加一倍
powerOfTwoSampleSize = powerOfTwoSampleSize << 1;
}
}
//设置inSampleSize
options.inSampleSize = powerOfTwoSampleSize;
...
}
图片格式优化(bytesPerPixel)¶
从上面的分析可以看出,Bitmap占用的内存主要是由width * height * bytesPerPixel决定的,其中bytesPerPixel是由Bitmap.Config决定的,如下表所示
| Config | bytesPerPixel |
|---|---|
| ALPHA_8 | 1 |
| ARGB_4444 | 2 |
| ARGB_8888 | 4 |
| RGB_565 | 2 |
不同格式对应每个像素所占用的字节不一样,所存储的色彩信息也不同。同一张100像素的图片,ARGB_8888就占了400字节,RGB_565才占200字节
值得注意的是在Glide4.0之前,Glide默认使用RGB565格式,这样比较省内存,但对于特定的图片有明显的画质问题,譬如条纹(banding)和着色(tinting),
所以Glide4.0之后,默认格式已经变成了ARGB_8888格式了, 图片质量变高了,但内存使用也增加了。
这本身也就是质量与内存之间的取舍,如果应用所需图片的质量要求不高,也可以修改默认格式,通过在 AppGlideModule 中应用一个 RequestOption:
@GlideModule
public final class YourAppGlideModule extends GlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) {
builder.setDefaultRequestOptions(new RequestOptions().format(DecodeFormat.PREFER_RGB_565));
}
}
内存复用优化(inBitmap)¶
在 Android 3.0(API 级别 11)开始,系统引入了 BitmapFactory.Options.inBitmap 字段。如果设置了此选项,那么采用 Options 对象的解码方法会在生成目标 Bitmap 时尝试复用 inBitmap,这意味着 inBitmap 的内存得到了重复使用,从而提高了性能,同时移除了内存分配和取消分配。不过 inBitmap 的使用方式存在某些限制,在 Android 4.4(API 级别 19)之前系统仅支持复用大小相同的位图,4.4 之后只要 inBitmap 的大小比目标 Bitmap 大即可
既然想复用Bitmap,就需要有集合来存储这些Bitmap,在Glide中,BitmapPool就是干这事的。Glide 通过 SDK 版本不同创建不同的 BitmapPool 实例,版本低于 Build.VERSION_CODES.HONEYCOMB(11) 实例为 BitmapPoolAdapter,是个空实现。Glide 真正有效的BitmapPool 实例是LruBitmapPool。
LruBitmapPool
public class LruBitmapPool implements BitmapPool {
LruBitmapPool(long maxSize, LruPoolStrategy strategy, Set<Bitmap.Config> allowedConfigs) {
this.initialMaxSize = maxSize;
this.maxSize = maxSize;
this.strategy = strategy;
this.allowedConfigs = allowedConfigs;
this.tracker = new NullBitmapTracker();
}
@Override
public synchronized void put(Bitmap bitmap) {
...
if (!bitmap.isMutable()
|| strategy.getSize(bitmap) > maxSize
|| !allowedConfigs.contains(bitmap.getConfig())) {
return;
}
final int size = strategy.getSize(bitmap);
strategy.put(bitmap);
···
}
@Override
@NonNull
public Bitmap get(int width, int height, Bitmap.Config config) {
Bitmap result = getDirtyOrNull(width, height, config);
if (result != null) {
result.eraseColor(Color.TRANSPARENT);
} else {
result = createBitmap(width, height, config);
}
return result;
}
@Nullable
private synchronized Bitmap getDirtyOrNull(
int width, int height, @Nullable Bitmap.Config config) {
final Bitmap result = strategy.get(width, height, config != null ? config : DEFAULT_CONFIG);
if (result == null) {
misses++;
} else {
hits++;
currentSize -= strategy.getSize(result);
tracker.remove(result);
normalize(result);
}
...
return result;
}
...
}
LruBitmapPool 中没有做太多的事,只是做一些缓存大小管理、封装、日志记录等等操作, 主要任务都交给了 LruPoolStrategy,这里采用的是策略模式。
LruPoolStrategy 有两个实现类:SizeConfigStrategy 以及 AttributeStrategy,其中,AttributeStrategy 适用于 Android 4.4 以下的版本,SizeConfigStrategy 适用于 Android 4.4 及以上的版本。
AttributeStrategy 通过 Bitmap 的 width(图片宽度)、height(图片高度) 和 config(图片颜色空间,比如 ARGB_8888 等) 三个参数作为 Bitmap 的唯一标识。当获取 Bitmap 的时候只有这三个条件完全匹配才行。而 SizeConfigStrategy 使用 size(图片的像素总数) 和 config 作为唯一标识。当获取的时候会先找出 cofig 匹配的 Bitmap(一般就是 config 相同),然后保证该 Bitmap 的 size 大于我们期望的 size 并且小于期望 size 的 8 倍即可复用。
得到可复用Bitmap之后,Glide就设置到对应BitmapFactory.Options:
Downsampler#setInBitmap
private static void setInBitmap(
BitmapFactory.Options options, BitmapPool bitmapPool, int width, int height) {
@Nullable Bitmap.Config expectedConfig = null;
...
options.inBitmap = bitmapPool.getDirty(width, height, expectedConfig);
}
池化 (Pooling)
Glide中除了bitmap之外,对于byte[] 数组, int[] 数组也做了复用,这个体现在LruArrayPool中,譬如用于BitmapFactory.Options.inTempStorage的值,这个作用是指定解码过程的缓冲区大小。
“池化”以及对象复用这些资源,可以减少应用中的内存抖动数量。如果每次使用的时候直接创建一个对象,那么可能会因频繁创建和销毁导致虚拟机 GC,从而造成页面卡顿现象
在5.0之前,Dalvik 有两种基本的 GC 模式, GC_CONCURRENT和 GC_FOR_ALLOC 。
-
GC_CONCURRENT:对于每次收集将阻塞主线程大约 5ms 。因为每个操作都比一帧(16ms)要小,GC_CONCURRENT 通常不会造成你的应用丢帧。 -
GC_FOR_ALLOC:是一种stop-the-world收集,可能会阻塞主线程达到 125ms 以上。GC_FOR_ALLOC几乎每次都会造成你的应用丢失多个帧,导致视觉卡顿,特别是在滑动的时候。
不幸的是,Dalvik 似乎甚至连适度的分配(例如一个 16kb 的缓冲区)都处理得不是很好。重复的中等分配,或即使单次大的分配(比如说一个 Bitmap ),将会导致 GC_FOR_ALLOC。因此,你分配的内存越多,就会招来越多 stop-the-world 的 GC,而你的应用将有更多的丢帧。
通过复用中到大尺寸的资源, Glide 可以帮你尽可能地减少这种 GC,以保持应用的流畅。
虽然 Android 5.0之后使用 ART, 运行时的 GC 惩罚比 Dalvik 运行时要低,但无论使用什么设备,过多内存分配都会降低应用的性能。