其他框架的优缺点
UniversalImageLoader
优点
- 1、多线程异步加载;
- 2、AbsListView 滑动监听;
- 3、内存缓存、磁盘缓存;
- 4、列表滑动监听;
缺点
- 1、使用弱引用做内存缓存,容易被回收;
Picasso
优点
- 1、默认使用 ARGB_8888 格式,图片显示质量较高,但内存占用较大;
- 2、集成后 apk 增加较小 118k;
- 3、网络层默认使用 okhttp 实现,可利用其缓存机制;
- 4、结合 LifecycleObserver 自动暂停/恢复图片请求、监听网络变化
- 5、可加载 webp 格式图片;
缺点
- 1、全尺寸缓存,导致显示速度慢;
- 2、不能加载 gif;
Fresco
优点
- 1、更专业的图片加载框架,图片显示需求量大的应用;
- 2、5.0 以下的设备设备表现较好,bitmap 存入了 native 层;
- 3、可加载本地图片、资源图片、gif、webp;
- 4、图片渐进式显示,类似 web 页面图片显示;
缺点
- 1、侵入性较强,DraweeView;
- 2、涉及到 jni 层面技术,学习难度大;
Volley
优点
- 1、图片加载与网络请求一体化;
- 2、请求结果转换成 java bean;
缺点
- 1、大文件下载时对内存压力比较大;
- 2、不能加载本地图片;
Glide 的优点
优点
- 1、流式 API、使用简单、集成入侵性低、加载流畅(缓存);
- 2、生命周期集成,解决内存泄露及 OOM 问题;
- 3、网络变化监听,自动暂停/恢复加载;
- 4、内存管理机制优秀,自动计算内存、监听内存使用并释放内存;
- 5、使用弱引用实现活动资源缓存,缓解内存缓存压力;
- 6、bitmap、byte[] 缓存、各种 LRU 缓存;
- 7、能够加载多种资源类型的图片:gif、webp、base64、video
- 8、可定制性高,提供接口能够替换核心实现:网络、缓存、图片编解码等;
缺点
- 1、集成后导致 apk 增大 430k;
- 2、学习路线陡峭,理解起来难度较大;
总结
Picasso 所能实现的功能 Glide 都能做到,只是所需设置不同。两者的区别是 Picasso 比 Glide 体积小很多且图像质量比 Glide 高,但Glide 的速度比 Picasso 更快,Glide 的长处是处理大型的图片流,如 gif、video,如果要制作视频类应用,Glide 当为首选。
Fresco 可以说是综合了之前图片加载库的优点,其在5.0以下的内存优化非常好,但它的不足是体积太大,按体积进行比较:Fresco>Glide>Picasso,所以 Fresco 在图片较多的应用中更能凸显其价值,如果应用没有太多图片需求,还是不推荐使用 Fresco。
流程梳理
Glide#with() -> RequestManager
1、初始化 Glide 单例: Glide#get() -> checkAndInitializeGlide() -> initializeGlide(Context, GlideBuilder) 1.1、getAnnotationGeneratedGlideModules() -> Class.forName("com.bumptech.glide.GeneratedAppGlideModuleImpl") 通过 @GlideModule 注解的类或者 com.github.bumptech.glide:compiler 依赖动态编译生成 1.2、ManifestParser#parse() 在 AndroidManifest.xml 文件中注册自定义的 GlideModule 类,用于替换默认组件或定义默认 加载参数、自定义线程池等1.3、移除排除的组件 1.4、应用自定义默认设置 1.5、Glide glide = builder.build(); sourceExecutor: 用于获取 http 资源 核心&最大线程数为 CPU 个数、存活 0s、PriorityBlockingQueue sourceUnlimitedExecutor: 用于获取 http 资源 核心线程数为 0、最大线程数无上限、存活 10s、SynchronousQueue 吞吐量高,与 OkHttp 中 Dispatcher 设计一致 disCackeExecutor: 用于磁盘读写 核心线程数 0、最大线程数 1、存活 0s、PriorityBlockingQueue animationExecutor: 用于执行动画 核心&最大线程数为 1 或 2、存活 10s、PriorityBlockingQueue memorySizeCalculator: 内存缓存计算:低内存设备、屏幕像素密度、ARGB_8888、堆大小 根据 ActivityManager#isLowMemoryDevice() 判读是否是低内存设备 最大: heapSize * (0.33 或 0.4) 通过 `adb shell setprop log.tag.MemorySizeCalculator DEBUG` 可观察到以下日志 MemorySizeCalculator: Calculation complete, Calculated memory cache size: 31.24 MB, (bitmap)pool size: 15.62 MB, byte array size: 4.19 MB, memory class limited? false, max size: 80.53 MB, memoryClass: 192, isLowMemoryDevice: false connectivityMonitoryFactory: 网络变化监听 bitmapPool: bitmap 缓存池 arrayPool: byte[] 缓存池 memoryCache: 内存缓存 discacheFactory: 生成磁盘缓存对象 engine: 核心引擎,把各个组件串联起来 -> new Glide() -> Register#register().... ImageHeaderParser/Downsampler/xxxDecoder/xxxLoader/xxxEncoder/xxxTranscoder 1.6、调用 registerComponents() 注册或替换自定义组件 1.7、注册 ComponentCallbacks,内存吃紧时调用 onLowMemory() 时释放内存
Glide#getRetriever() -> RequestManagerRetriever()
Glide 构造器中传入,由 GlideBuilder 注入 3、RequestManagerRetriever#get() -> RequestManager 管理图片请求 3.1、Application: getApplicationManager() -> RequestManagerFactory#build(glide, ApplicationLifecycle) 3.2、Activity: -> fragmentGet() 3.3、Fragment: -> fragmentGet() 3.4、View: -> findActivity() -> fragmentGet() 3.5、fragmentGet() & supportFragmentGet() 3.6、RequestManagerFactory#build(glide, ActivityFragmentLifecycle) 4、Lifecycle & LifecycleListener 利用空白 Fragment 监听生命周期反馈给整个框架 addListener(LifecycleListener) & removeListener(LifecycleListener) 4.1、ApplicationLifecycle: 子线程或者 Application 类型的 Context 4.2、ActivityFragmentLifecycle: Activity/Fragment/View 监听 Fragment 生命周期
RequestManager#load(String) -> RequestBuilder
asDrawable().load(String) asDrawable() -> as(Drawable.class) -> new RequestBuilder<>(glide, this, klass, context) load(String) -> loadGeneric() -> model = str
RequestBuilder#into() -> Target
into(ImageView) -> into(GlideContext#buildImageViewTarget(view)) -> into(Target) 1、buildRequest(target) -> Request 2、Target#setRequest(request) 3、RequestManager#track(target) 4、RequestTracker#runRequest() 5、Request#begin() -> SingleRequest#begin() -> 6、Target#onLoadStarted() -> 计算 View 尺寸 -> onSizeReady() -> 7、Engine#load() -> 开始加载图片 8、 图片格式: Android 中常用的 png、jpeg、webp 质量压缩:根据宽高及像素点所占用的字节数,本身宽高不变 png 无损压缩,设置 quality 无效,不适合做缩略图 采样压缩: 缩小图片分辨率,降低磁盘和内存占用 缩放压缩: 减少图片像素,降低磁盘和内存占用 可用于做缩略图 jni 调用 jpeg 库: Android 图片引擎是阉割版 skia 库,去掉了图片压缩中的哈夫曼算法 java 版本: github: https://github.com/Curzibn/Luban
图片加载流程
com.bumptech.glide.load.engine.Engine
1、调用 Engine#load() 方法加载图片:
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public <T, Z, R> LoadStatus load(Key signature, int width, int height,
DataFetcher<T> fetcher, DataLoadProvider<T, Z> loadProvider,
Transformation<Z> transformation, ResourceTranscoder<Z, R> transcoder,
Priority priority, boolean isMemoryCacheable,
DiskCacheStrategy diskCacheStrategy, ResourceCallback cb) {
// 1、工厂模式生成缓存 key
final String id = fetcher.getId();
EngineKey key = keyFactory.buildKey(id, signature, width, height,
loadProvider.getCacheDecoder(), loadProvider.getSourceDecoder(),
transformation, loadProvider.getEncoder(), transcoder,
loadProvider.getSourceEncoder());
// 2、根据 key 从内存中获取
EngineResource<?> cached = getFromCache(key);
if (cached != null) {
cached.acquire(); // 引用计数+1
// 放到活动资源中
activeResources.put(key, new ResourceWeakReference(key, cached,
resourceReferenceQueue));
cb.onResourceReady(cached); // 回调给调用者
return null;
}
// 3、从活动资源中获取(弱引用)
WeakReference<EngineResource<?>> activeRef = activeResources.get(key);
if (activeRef != null) {
EngineResource<?> active = activeRef.get();
if (active != null) { // 引用对象存在
active.acquire(); // 引用计数+1
cb.onResourceReady(active); // 回调给调用者
return null;
} else { // 引用对象不存在,从活动资源中移除
activeResources.remove(key);
}
}
// 4、从磁盘或网络或其他源头中获取
EngineJob current = jobs.get(key);
if (current != null) {
current.addCallback(cb);
return new LoadStatus(cb, current);
}
EngineJob engineJob = engineJobFactory.build(key, isMemoryCacheable);
// 解码
DecodeJob<T, Z, R> decodeJob = new DecodeJob<T, Z, R>(key, width, height,
fetcher, loadProvider, transformation, transcoder, diskCache,
diskCacheStrategy, priority);
// 把 engineJob 和 decodeJob 封装到 EngingRunnable 中
EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority);
jobs.put(key, engineJob);
engineJob.addCallback(cb);
engineJob.start(runnable); // 开始加载(线程池 sourceService)
return new LoadStatus(cb, engineJob);
}
com.bumptech.glide.load.engine.EngineRunnable
2、EngineRunnable#run() 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
public void run() {
if (isCancelled) return; // 任务已取消,返回
Exception exception = null;
Resource<?> resource = null;
try {
resource = decode(); // 解码,会分别磁盘和源头加载
} catch (Exception e) {
exception = e;
}
if (isCancelled) {
if (resource != null) resource.recycle(); // 任务已取消,回收资源
return;
}
if (resource == null) {
onLoadFailed(exception); // 加载失败
} else {
onLoadComplete(resource); // 加载完成,通过 Handler 回调到主线程
}
}
3、EngineRunnable#decodeFromCache() 方法
1
2
3
4
5
6
7
8
9
10
private Resource<?> decodeFromCache() throws Exception {
Resource<?> result = null;
try {
// 尝试从磁盘加载已转换过的资源
result = decodeJob.decodeResultFromCache();
} catch (Exception e) {}
// 尝试从磁盘加载并转化、再次缓存到磁盘
if (result == null) result = decodeJob.decodeSourceFromCache();
return result;
}
4、EngineRunnable#decodeFromSource() 方法
1
2
3
4
private Resource<?> decodeFromSource() throws Exception {
// 从源头获取、转码、转换并写入磁盘
return decodeJob.decodeFromSource();
}
磁盘缓存
com.bumptech.glide.load.engine.cache.DiskLruCacheWrapper
com.bumptech.glide.disklrucache.DiskLruCache
LRU 算法
Latest Recently Used 最近最少使用算法,要求查询快(数组)且修改快(链表),LinkedHashMap 内部使用数组+双链表的数据结构,正好满足以上两个要求。
1
LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<>(0, 0.75f, true);
第三个参数 accessOrder = true
表示被访问的元素,访问完之后放到链表的尾部,尾部的数据是最近最 新被访问的;而链表头部的数据则是最近最少被访问的,也就是下次调用 trimToSize()
方法时最容易被淘 汰的数据。
日志文件 journal
文件结构:
magic: 魔数 libcore.io.DisckLruCache
disk cache version: 缓存版本号
app version: 应用版本
count: 缓存计数
Clean xxx 2 9 新入的缓存,可能下次被访问到;两个数字表示文件的长度
Dirty abcd 创建或者更新的缓存,其后必然是一行 Clean 或 Remove;若无则表示下次重建缓存时要删除临时文件
Clean abcd 3 5
Remove abcd 缓存被删除
Dirty fffcc
Clean fffcc 7 3
Read qwert 缓存被读
作用: 1、记录访问顺序,应用程序运行时据此重建缓存; 2、清除损坏的文件;
edit() 方法
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
26
27
28
29
30
31
public Editor edit(String key) throws IOException {
return edit(key, ANY_SEQUENCE_NUMBER);
}
private synchronized Editor edit(String key, long expectedSequenceNumber)
throws IOException {
checkNotClosed();
// 根据 key 获取 entry
Entry entry = lruEntries.get(key);
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
|| entry.sequenceNumber != expectedSequenceNumber)) {
return null; // Value is stale.
}
if (entry == null) {
// 若 entry 为 null 则创建并加入缓存
entry = new Entry(key);
lruEntries.put(key, entry);
} else if (entry.currentEditor != null) {
return null; // Another edit is in progress.
}
Editor editor = new Editor(entry);
entry.currentEditor = editor;
// 记录日志文件
// Flush the journal before creating files to prevent file leaks.
journalWriter.append(DIRTY);
journalWriter.append(' ');
journalWriter.append(key);
journalWriter.append('\n');
journalWriter.flush();
return editor;
}
get() 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public synchronized Value get(String key) throws IOException {
checkNotClosed();
Entry entry = lruEntries.get(key);
if (entry == null) return null;
if (!entry.readable) return null;
for (File file : entry.cleanFiles) {
// A file must have been deleted manually!
if (!file.exists()) {
return null;
}
}
// 在日志中记录
redundantOpCount++;
journalWriter.append(READ);
journalWriter.append(' ');
journalWriter.append(key);
journalWriter.append('\n');
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return new Value(key, entry.sequenceNumber, entry.cleanFiles, entry.lengths);
}
trimToSize() 方法
1
2
3
4
5
6
7
8
private void trimToSize() throws IOException {
Map.Entry<String, Entry> toEvict;
while (size > maxSize) {
// 从链表头部开始移除,即移除最久未被使用的元素
toEvict = lruEntries.entrySet().iterator().next();
remove(toEvict.getKey());
}
}
remove() 方法
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
26
27
28
public synchronized boolean remove(String key) throws IOException {
checkNotClosed();
Entry entry = lruEntries.get(key);
if (entry == null || entry.currentEditor != null) {
return false;
}
// 删除 key 对应的所有文件
for (int i = 0; i < valueCount; i++) {
File file = entry.getCleanFile(i);
if (file.exists() && !file.delete()) {
throw new IOException("failed to delete " + file);
}
size -= entry.lengths[i];
entry.lengths[i] = 0;
}
// 在日志中记录
redundantOpCount++;
journalWriter.append(REMOVE);
journalWriter.append(' ');
journalWriter.append(key);
journalWriter.append('\n');
// 从缓存中移除
lruEntries.remove(key);
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return true;
}
常见问题
1、Glide 缓存失效问题
原因
:由于图片 url 携带了 token,而每次登录 token 是不同的从而导致缓存 key 失效
解决
:自定义 GlideUrl 并重写 getCacheKey() 方法擦除 token 并使用自定义 GlideUrl 替换原有 Glide.get().register(MyGlideUrl.class, InputStream.class, HttpUrlFetcher.Factory())
2、调用 View#setTag() 报错:You must not call setTag() on a view Glide is targeting
原因
:ViewTarget#setRequest(request) 时调用了 View#setTag(),而 ViewTarget#getRequest() 方法调用 View#getTag() 来取回 request,此时会判断 tag 类型是否为 Request,如果不是说明用户也 调用了 View#setTag() 方法,抛出 IllegalArgumentException()
解决一
:使用自定义 tag,给 View 设置 tag 时调用 View#setTag(key, tag) 方法 解决二
:调用 ViewTarget#setTagIt() 设置 tagId 后可以使用 View#setTag()
3、替换网络层 利用 Glide 提供的注册机制,在 GlideModule#registerComponents() 方法中替换
4、获取图片缓存
FutureTarget
5、加载 gif 时内存飙升 解决
:实现 GifDecoder
接口自定义 gif 解析,可选择性引入 libgif
jni 库