需求
使用过Glide这个框架的同学大概知道,Glide从网络上加载图片时,只能加载一张图片,失败之后,可以选择显示占位图,如下面这段代码:
Glide.with(context).load(url)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.DATA)
.apply(new RequestOptions().dontAnimate())
.placeholder(placeholderRes)
.into(imageView);
但不知道大家有没有这样的需求:
如果我们有多张候选的图片url,需要在加载出错之后,尝试从下一个url中去进行加载,直到完成所有url的尝试。
所以我们希望在使用Glide加载图片时,大概的使用方式能如下面代码所示:
ImgUrlWrapper.java
/*
* ImgUrlWrapper类,将所有的url打包到一起
*/
public class ImgUrlWrapper {
private List<String> urls;
public ImgUrlWrapper(List<String> urls) {
this.urls = urls;
}
public void setUrls(List<String> urls) {
this.urls = urls;
}
public List<String> getUrls() {
return this.urls;
}
}
/*
* 一次图片加载失败之后,尝试加载其他的候选图片
*/
public static void displayWithCustomModule(Context context, ImageWrapper imageWrapper, int placeholderRes,
ImageView imageView) {
GlideApp.with(context)
.load(imageWrapper)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.DATA).apply(new RequestOptions().dontAnimate())
.placeholder(placeholderRes)
.into(imageView);
}
解题思路
读过Glide(version 4.11.0)源码的同学,大概能知道,Glide是通过ModelLoader<Model, Data>
完成图片的加载过程的封装的,由DataFetcher<T>
负责完成具体的数据加载,AppGlideModule
负责注册一组组件,以在使用Glide的注释处理器时在应用程序中初始化Glide时使用。
/*
* @param <Model> 资源的解码类型(转换前),比如:File, url/InputStream
* @param <Data> 可用于ResourceDecoder解码资源的数据类型(转换后),比如:Bitmap, Drawable等等
*/
public interface ModelLoader<Model, Data> {
/**
* Contains a set of {@link com.bumptech.glide.load.Key Keys} identifying the source of the load,
* alternate cache keys pointing to equivalent data, and a {@link
* com.bumptech.glide.load.data.DataFetcher} that can be used to fetch data not found in cache.
*
* @param <Data> The type of data that well be loaded.
*/
class LoadData<Data> {
public final Key sourceKey;
public final List<Key> alternateKeys;
public final DataFetcher<Data> fetcher;
public LoadData(@NonNull Key sourceKey, @NonNull DataFetcher<Data> fetcher) {
this(sourceKey, Collections.<Key>emptyList(), fetcher);
}
public LoadData(
@NonNull Key sourceKey,
@NonNull List<Key> alternateKeys,
@NonNull DataFetcher<Data> fetcher) {
this.sourceKey = Preconditions.checkNotNull(sourceKey);
this.alternateKeys = Preconditions.checkNotNull(alternateKeys);
this.fetcher = Preconditions.checkNotNull(fetcher);
}
}
/**
* 返回一个包裹了DataFetcher的LoadData, 即创建加载数据
*/
@Nullable
LoadData<Data> buildLoadData(
@NonNull Model model, int width, int height, @NonNull Options options);
/**
* 返回次Loader是否能够处理对应Model的数据
* @return true if this loader can handle the model and false otherwise
*/
boolean handles(@NonNull Model model);
}
上面源码中通过buildLoadData方法创建LoadData,而LoadData中的DataFetcher如下:
/**
* 负载加载数据的接口
* @param <T> The type of data to be loaded (InputStream, byte[], File etc).
*/
public interface DataFetcher<T> {
interface DataCallback<T> {
/**
* Called with the loaded data if the load succeeded, or with {@code null} if the load failed.
*/
void onDataReady(@Nullable T data);
/**
* Called when the load fails.
*/
void onLoadFailed(@NonNull Exception e);
}
/**
* 从一个可以被解码的资源拉取数据
*/
void loadData(@NonNull Priority priority, @NonNull DataCallback<? super T> callback);
/**
* 清理或回收资源
*/
void cleanup();
/**
* 退出加载
*/
void cancel();
/** Returns the class of the data this fetcher will attempt to obtain. */
@NonNull
Class<T> getDataClass();
/** Returns the {@link com.bumptech.glide.load.DataSource} this fetcher will return data from. */
@NonNull
DataSource getDataSource();
}
public abstract class AppGlideModule extends LibraryGlideModule implements AppliesOptions {
public boolean isManifestParsingEnabled() {
return true;
}
@Override
public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
// Default empty impl.
}
}
public abstract class LibraryGlideModule implements RegistersComponents {
@Override
public void registerComponents(
@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
// Default empty impl.
}
}
实现过程
先定义一个Model(即ModelLoader<Model, Data>
中的Model),我们把所有候选url都打包进去
public class ImgUrlWrapper {
private List<String> mUrls;
public ImgUrlWrapper(List<String> urls) {
mUrls = urls;
}
public List<String> getUrls() {
return mUrls;
}
public void setUrls(List<String> urls) {
mUrls = urls;
}
}
定义我们自己的DataFetcher,并参考Glide官方的HttpUrlFetcher
来实现我们的需求(候选图片的加载)。我们在上面提到过,DataFetcher
负责实现具体的数据加载过程。
下面代码中省略部分都是从HttpUrlFetcher拷贝过来的,我们主要关注loadData方法
public class UrlsFetcher implements DataFetcher<InputStream> {
private static final String TAG = "HttpUrlFetcher";
private static final int MAXIMUM_REDIRECTS = 5;
private static final int INVALID_STATUS_CODE = -1;
private final int timeout = 2500;
private HttpUrlConnectionFactory connectionFactory;
static HttpUrlConnectionFactory DEFAULT_CONNECTION_FACTORY =
new DefaultHttpUrlConnectionFactory();
private HttpURLConnection urlConnection;
private InputStream stream;
private volatile boolean isCancelled;
private final List<String> mUrls;
private int urlCount = 0;
public UrlsFetcher(List<String> urls) {
this.mUrls = urls;
connectionFactory = DEFAULT_CONNECTION_FACTORY;
urlCount = urls.size();
}
@Override
public void loadData(@NonNull Priority priority,
@NonNull DataCallback<? super InputStream> callback) {
// 从最后一个图片url开始加载图片
urlCount--;
try {
GlideUrl glideUrl = new GlideUrl(mUrls.get(urlCount));
InputStream result = loadDataWithRedirects(glideUrl.toURL(), 0, null,
glideUrl.getHeaders());
// 图片加载完成,直接回调
callback.onDataReady(result);
} catch (IOException e) {
// 如果加载失败,尝试加载下一张图
if (urlCount >= 0) {
// 打印日志来验证是否尝试从其他地址加载图片
Log.d(TAG, "retry next url " + mUrls.get(urlCount));
loadData(priority, callback);
} else {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Failed to load data for url", e);
}
// 所有图片加载失败的回调
callback.onLoadFailed(e);
}
}
}
...
}
完成自定义的DataFetcher,我们定义一个ModelLoader来封装这个加载过程:
public class UrlsLoader implements ModelLoader<ImgUrlWrapper, InputStream> {
@Nullable
@Override
public LoadData<InputStream> buildLoadData(@NonNull ImgUrlWrapper wrapper, int width, int height,
@NonNull Options options) {
// 返回LoadData
return new LoadData<>(new ObjectKey(wrapper), new UrlsFetcher(wrapper.getUrls()));
}
@Override
public boolean handles(@NonNull ImgUrlWrapper wrapper) {
// 只允许带有http和https的链接地址
for (String url : wrapper.getUrls()) {
if (!url.startsWith("http") || !url.startsWith("https")) {
return false;
}
}
return true;
}
public static class Factory implements ModelLoaderFactory<ImgUrlWrapper, InputStream> {
@NonNull
@Override
public ModelLoader<ImgUrlWrapper, InputStream> build(
@NonNull MultiModelLoaderFactory multiFactory) {
return new UrlsLoader();
}
@Override
public void teardown() {
}
}
}
完成下面最后一步,我们就可以验证我们的思路是否正确了
/*
* 注解@GlideModule不能缺,不然无法编译生成我们想要的GlideApp
* AppGlideModule中有个注释,如下:
* Classes that extend {@link AppGlideModule} must be annotated with {@link
* com.bumptech.glide.annotation.GlideModule} to be processed correctly.
*/
@GlideModule
public class UrlsModule extends AppGlideModule {
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide,
@NonNull Registry registry) {
super.registerComponents(context, glide, registry);
// 完成注册
glide.getRegistry().append(ImgUrlWrapper.class, InputStream.class, new UrlsLoader.Factory());
}
@Override
public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
super.applyOptions(context, builder);
}
}
OK,我们使用写一个Demo来验证我们的想法是否正确:
public class MainActivity extends AppCompatActivity {
private ImageView mImageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mImageView = findViewById(R.id.img);
// 以百度搜索的Logo地址为例,下面所有地址中只有
// https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png是正确的,
// 其他地址都无法获取到图片
List<String> urls = new ArrayList() {
{
add("https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png");
add("https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d");
add("https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6");
add("https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6c");
add("https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf");
add("https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.p");
add("https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.pn");
}
};
GlideApp.with(this.getApplicationContext())
.load(new ImgUrlWrapper(urls))
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.apply(new RequestOptions().dontAnimate())
.placeholder(R.mipmap.ic_launcher)
.into(mImageView);
}
}
2021-03-31 20:15:33.639 12972-12997/com.example.glidex D/HttpUrlFetcher: retry next url https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.pn
2021-03-31 20:15:33.843 12972-12997/com.example.glidex D/HttpUrlFetcher: retry next url https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.p
2021-03-31 20:15:34.051 12972-12997/com.example.glidex D/HttpUrlFetcher: retry next url https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf
2021-03-31 20:15:34.226 12972-12997/com.example.glidex D/HttpUrlFetcher: retry next url https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6c
2021-03-31 20:15:34.433 12972-12997/com.example.glidex D/HttpUrlFetcher: retry next url https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6
2021-03-31 20:15:34.642 12972-12997/com.example.glidex D/HttpUrlFetcher: retry next url https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d
从上面的日志中,我们可以验证我们的思路和实现方式都是OK的。如果各位同学还有其他的想法,也可以借鉴这种思路来实现。