Posts Glide 图片加载框架
Post
Cancel

Glide 图片加载框架

其他框架的优缺点

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

图片加载流程

Engine-load

  • 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 库

This post is licensed under CC BY 4.0 by the author.