使用Glide迁移图片加载框架遇到的一些问题

背景

因为项目原有的图片加载框架已经不满足新的业务需求,而且改造成本较大,经过慎重的技术选型,将原有的图片加载框架整体迁移成Glide。在这个过程中我遇到了不少问题,跟大家分享一下。

遇到的问题和解决方案

1.如果ImageView不可见 或者 width为0 或者 height为0,glide是不会加载图像的

项目中遇到ImageView并没有add到UI上,只是用来加载图像,获取drawing cache作为Bitmap使用。在没有layout的时候width和height都是0,结果图片是空的,因为glide没有加载图像。

解决方案:layout结束才加载图像。

2.不支持多磁盘缓存目录

因为DiskCache的LRU机制,我们倾向于会把较小的头像和较大的会话图片放在不同的目录存储,以保证小头像有足够的磁盘缓存空间,小文件也不会因大文件的频繁变动而从缓存中移除。
但是Glide原生只支持一个磁盘缓存目录,很多人也提过这个issue。根据作者提供的思路,可以通过继承DiskLruCacheWrapper,依据不同请求的signature来分发不同的磁盘缓存目录,但是需要一些反射的手段。

MultiFolderDiskLruCacheWrapper.java

/**
 * 支持多个磁盘缓存目录
 */
public class MultiFolderDiskLruCacheWrapper extends DiskLruCacheWrapper {
  private static final String TAG = "MultiFolderDiskLruCacheWrapper";

  private static Field sFieldSignatureInResourceCacheKey;
  private static Field sFieldSignatureInDataCacheKey;
  private static MultiFolderDiskLruCacheWrapper wrapper;

  static {
    try {
      Class<?> resourceCacheKeyClass = Class.forName("com.bumptech.glide.load.engine.ResourceCacheKey");
      sFieldSignatureInResourceCacheKey = resourceCacheKeyClass.getDeclaredField("signature");
      sFieldSignatureInResourceCacheKey.setAccessible(true);

      Class<?> DataCacheKeyClass = Class.forName("com.bumptech.glide.load.engine.DataCacheKey");
      sFieldSignatureInDataCacheKey = DataCacheKeyClass.getDeclaredField("signature");
      sFieldSignatureInDataCacheKey.setAccessible(true);
    } catch (ClassNotFoundException e) {
      WwLog.log(Log.WARN, TAG, "find ResourceCacheKey failed", e);
    } catch (NoSuchFieldException e) {
      WwLog.log(Log.WARN, TAG, "reflect signature failed", e);
    } catch (Error error) {
      WwLog.log(Log.WARN, TAG, "reflect signature failed", error);
    }
  }

  private DiskCache[] diskCaches = new DiskCache[CacheFolder.values().length];

  public static synchronized DiskCache get() {
    if (wrapper == null) {
      wrapper = new MultiFolderDiskLruCacheWrapper(null, 0);
    }
    return wrapper;
  }

  protected MultiFolderDiskLruCacheWrapper(File directory, int maxSize) {
    super(directory, maxSize);
  }

  @Override
  public File get(Key key) {
    return getDiskCacheBySignature(key).get(key);
  }

  @Override
  public void put(Key key, Writer writer) {
    getDiskCacheBySignature(key).put(key, writer);
  }

  @Override
  public void delete(Key key) {
    getDiskCacheBySignature(key).delete(key);
  }

  @Override
  public synchronized void clear() {
    for (DiskCache diskCache : diskCaches) {
      if (diskCache != null) {
        diskCache.clear();
      }
    }
  }

  /**
   * 根据key选择不同的DiskCache实例,每个DiskCache实例对应不同缓存目录
   * @param key
   * @return
   */
  private DiskCache getDiskCacheBySignature(Key key) {
    Object signature = getSignature(key);
    CacheFolder cacheFolder;
    if (isContact(signature)) {
      cacheFolder = CacheFolder.CONTACHT;
    } else {
      cacheFolder = CacheFolder.OTHER;
    }

    return getDiskCache(cacheFolder);
  }

  @Nullable
  private Object getSignature(Key key) {
    Object signature = null;
    try { // 已有缓存的情况下一般是ResourceCacheKey,所以这种情况更常见
      signature = sFieldSignatureInResourceCacheKey.get(key);
    } catch (Exception e) {
      WwLog.log(Log.INFO, TAG, "getSignature: " + e.getMessage());
    }
    if (signature != null) return  signature;

    try {
      signature = sFieldSignatureInDataCacheKey.get(key);
    } catch (Exception e) {
      WwLog.log(Log.INFO, TAG, "getSignature", e);
    }
    return signature;
  }

  private boolean isContact(Object signature) {
    return signature instanceof ContactSignature;
  }

  @NonNull
  private DiskCache getDiskCache(CacheFolder cacheFolder) {
    DiskCache diskCache = diskCaches[cacheFolder.index];
    if (diskCache == null) {
      String cachePath = FileUtil.getImageDiskCacheDirPath(cacheFolder.cacheDirName);
      if (FileUtil.createDir(cachePath)) {
        try {
          diskCache = (DiskLruCacheWrapper) ReflecterHelper.newInstance(
                  "com.bumptech.glide.load.engine.cache.DiskLruCacheWrapper", new Object[]{new File(cachePath), cacheFolder.cacheSize});
          diskCaches[cacheFolder.index] = diskCache;
        } catch (Exception e) {
          WwLog.log(Log.ERROR, TAG, "getDiskCache", e);
        }

      }
    }
    return diskCache;
  }
}

CacheFolder.java

public enum CacheFolder {
    CONTACHT(0, 1024 * 1024 * 50, "glidecontactphoto"),

    IMAGE(1, 1024 * 1024 * 100, "glideimage"),

    OTHER(2, 1024 * 1024 * 50, "glideother"),
    ;

    public int index;
    public int cacheSize;
    public String cacheDirName;

    CacheFolder(int index, int cacheSize, String cacheDirName) {
        this.index = index;
        this.cacheSize = cacheSize;
        this.cacheDirName = cacheDirName;
    }
}

3.使用setTag会报错"You must not call setTag() on a view Glide is targeting"

先看下Glide中是怎样使用setTag的:

/**
  * Returns any stored request using {@link android.view.View#getTag()}.
  *
  * <p> For Glide to function correctly, Glide must be the only thing that calls {@link
  * View#setTag(Object)}. If the tag is cleared or put to another object type, Glide will not be
  * able to retrieve and cancel previous loads which will not only prevent Glide from reusing
  * resource, but will also result in incorrect images being loaded and lots of flashing of images
  * in lists. As a result, this will throw an {@link java.lang.IllegalArgumentException} if {@link
  * android.view.View#getTag()}} returns a non null object that is not an {@link
  * com.bumptech.glide.request.Request}. </p>
  */
 @Override
 @Nullable
 public Request getRequest() {
   Object tag = getTag();
   Request request = null;
   if (tag != null) {
     if (tag instanceof Request) {
       request = (Request) tag;
     } else {
       throw new IllegalArgumentException(
           "You must not call setTag() on a view Glide is targeting");
     }
   }
   return request;
 }

原来Glide把Request通过setTag的方式放在ImageView中,主要用于复用资源取消旧的加载请求 或者 在列表滑动图片时防止加载了错误的图片。
解决方案:改成使用setTag(int key, final Object tag)就不会和Glide冲突了。

4.需要清除某个key对应的图像缓存

解决方案:参考issue,使用作者建议的方式,通过更新signature来废弃原有的图像缓存。
还有另一种情况是,需要显示的图片只是一次性的临时图片,但是这个临时图片的路径是写死在代码中的,这时直接选择不使用磁盘缓存的方式就好了(DiskCacheStrategy.NONE)。

5.需要图片落地加密,读磁盘缓存时解密

由于我们项目对安全性要求较高,需要对所有的图片用aes加密后才落地,读缓存时要aes解密。那如何在Glide的下载图片和加载缓存的过程中插入加解密的流程呢?

一种直接的想法就是去修改Glide的源码,在加载流程中加入我们的加解密函数。但是侵入式修改源码的做法,不利于跟进官方对Glide的升级。

参考issue,其实根据Glide Resource Flow,Glide是可以通过插入Encoder,ResourceDecoder,ResourceEncoder来自定义加载的流程。后来我看到另一个项目的需求,要在Glide的基础上支持对sharpP格式(腾讯音视频实验室推出的一种图片压缩技术)的支持,也是可以自定义ModelLoader来实现。

强烈建议认真研读Glide Resource Flow这张图,对于我们理解Glide数据流的转换和理解源码的帮助很大。
Glide Resource Flow
为了实现对所有图片的加解密,我加入了如下的Decoder和Encoder。其实源码里有大把的ResourceDecoder,基本上复制过来添加加解密的函数的就可以了。

@GlideModule(glideName = "WwGlide")
public class WwGllideMoudle extends AppGlideModule {

    @Override
    public void registerComponents(Context context, Glide glide, Registry registry) {
        // 对于ModelLoader,Encoder,ResourceDecoder,ResourceEncoder的理解,参考
        // https://docs.google.com/drawings/d/1KyOJkNd5Dlm8_awZpftzW7KtqgNR6GURvuF6RfB210g/edit

        /* FileId */
        registry.append(ImageFileId.class, InputStream.class, new FileIdLoader.Factory());
        /* File */
        registry.append(File.class, new FileEncoder())
                .prepend(File.class, File.class, new FileDecoder());
        /* 加解密相关 */
        BitmapPool bitmapPool = glide.getBitmapPool();
        ArrayPool arrayPool = glide.getArrayPool();
        Downsampler downsampler = new Downsampler(glide.getRegistry().getImageHeaderParsers(),
                glide.getContext().getResources().getDisplayMetrics(), bitmapPool, arrayPool);
        registry.prepend(InputStream.class, new StreamEncryptEncoder(glide.getArrayPool()));
        registry.prepend(Bitmap.class, new BitmapEncyptEncoder());
        registry.prepend(BitmapDrawable.class, new BitmapDrawableEncryptEncoder(bitmapPool, new BitmapEncyptEncoder()));
        registry.prepend(File.class, Bitmap.class, new BitmapDecryptDecoder(downsampler, arrayPool));

        glide.getRegistry().replace(GlideUrl.class, InputStream.class, new HttpGlideUrlLoader.Factory());
    }

...

}

在使用开源项目时,如果需要根据业务自定义开源项目中的某些流程,最优方案是寻找开源项目中,可以提供定制能力的接口或可供继承的类;次优方案是使用反射的手段hack掉开源项目中类;最后才是修改源码。因为如果和源码形成了不同的分支,后面原作者对项目的更新,我们就只能通过merge代码的方式跟进。

预热缓存
增加了图片加解密后,我们发现头像首次加载速度比之前慢了很多,原因是从磁盘中解密图片文件时耗费了很多时间。考虑到App启动后闪屏页大概要花费1.5s的时间,我在这段时间内预热头像的内存缓存,闪屏结束后首页加载头像就很快了。

private void preloadConversationContact() {
	List<ConversationItem> conversationItems = ConversationEngine.getInstance().getSortedConversationList();
	for (final ConversationItem conv : conversationItems) {
           List<String> photoUrlList = conv.getPhotoUrlList();
           boolean isSinglePhoto = photoUrlList.size() == 1;
           for (int i = 0; i < MultiPhotoImageView.MAX_PHOTO_COUNT && i < photoUrlList.size(); i++) {
			if (!StringUtil.isHttpSchema(photoUrlList.get(i))) break;
               ((GlideImgLoader) WwImgLoaderManager.getInstance()).loadContactImage(WwUtil.APPLICATION_CONTEXT, photoUrlList.get(i),
					GlideImgLoader.NO_DEFAULT_RES, isSinglePhoto ? ImageSizeType.TYPE_MIDDLE : ImageSizeType.TYPE_SMALL, null);
           }
       }
}

6.需要一个同步接口判断某个url的图片是否在磁盘缓存中

Glide从磁盘缓存中加载图像的流程是放在异步IO线程中进行的,所以只提供了异步判断的接口。但是项目有几个地方是用了同步方式判断缓存是否存在,如果改成异步接口需要修改大量代码。通读了Glide中构造key加载图像的代码,发现如果是按原大小加载图像,也是可以通过反射出几个关键类判断的。

 public static String getPathFromGlideCacheByFileId(String fileId) {
     try {
         Key sourceId = new ObjectKey( new ImageFileId(fileId));
         Key originalKey = (Key) ReflecterHelper.newInstance(
                 "com.bumptech.glide.load.engine.DataCacheKey", new Object[]{sourceId, EmptySignature.obtain()}, new Class[]{Key.class, Key.class});
         File cacheFile = MultiFolderDiskLruCacheWrapper.get().get(originalKey);
         if (cacheFile != null) {
             return cacheFile.getAbsolutePath();
         }
     } catch (Exception e) {
         WwLog.log(Log.INFO, TAG, "getAttachPath", e);
     }
     return null;
 }

}

但是如果按照实际显示大小加载图像,因为ResourceCacheKey的构造涉及了实际大小等其他因素,就没什么办法了。

currentKey = new ResourceCacheKey(sourceId, helper.getSignature(), helper.getWidth(),
          helper.getHeight(), transformation, resourceClass, helper.getOptions());

题后语

经过了这次对开源项目的技术选型、迁移旧代码,感触还是比较多的。

关于技术选型

对于某个业务需求,类似的开源项目很多,首先要选跟自身业务所需要的功能和接口都最契合的,否则迁移旧代码时就要做很多改造。我选择Glide主要是因为Glide可以支持gif和视频的加载,并且提供了扩展所支持的图片格式、定制加载流程的方式,可以满足我们丰富多样的业务需求。
其次是选社区比较活跃,作者积极发布新版本和处理issue的开源项目。我之前在Glide的项目中提了几个issue,基本上作者都会当天回复,并且也可以找到很多相似的案例。在使用开源项目遇到问题时,这样可以大大提高处理问题的速度。

关于迁移框架

迁移旧代码是一个漫长的过程,不能步子迈太大。我首先将头像加载部分改造成使用Glide,经过两个迭代的上线验证之后,再迁移主业务的图片加载接口。整个过程中旧框架和新框架并存,并可以自由切换,以防出现了严重问题需要切回原框架。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陆业聪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值