Glide 无法加载细长图片解决方法

今天在开发过程中,看到测试的 bug 反馈中有一条挺有趣的,就是当用户发出很细很长的图片时,无法加载出那个细长图片,为什么说细长图片,而不直接说长图呢,接下来分享下这个 bug 的解决流程

以下就是今天看到的诡异 bug

可以看到,这个用户发了一条很长很长的图片,在安卓端显示的时候是空白的,但是在 iOS 端则是正常的,由此可以推断,后台返回给我们的数据应该是正常的,我验证了一下,在 postman 中发起请求,得到缩略图和原图后保存了下来,可以看到两个图片的信息如下:

明明就是实实在在存在的图像,怎么显示空白呢,同事之前遇到过一个无法显示长图的情况,那是因为图片的宽或者高超过了手机的限制,所以导致无法正常显示图片,解决的方案是载入图像时分段载入,就不会出现不显示图片的情况。但是我这里的两张图,一张顶多也就 5000,而缩略图更加诡异,明明只有 408 的高度,却显示不出来了,这就奇怪得很了,继续看看是什么原因

我依稀记得这个不是普通的写死宽高的 ImageView,也不是普通的 match_parent,wrap_content 之类的,于是去看了一下,果然,用的是一个重写的自适应高宽高比的自定义View,逻辑如下:

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
public AdaptiveImageView(Context context, AttributeSet attrs) {
super(context, attrs);
maxSize = (int) context.obtainStyledAttributes(attrs, R.styleable.AdaptiveImageView).
getDimension(R.styleable.AdaptiveImageView_maxSize, 720);
}

/**
* 通过maxSize 的最大宽高,自动适应高宽高
* @param uri 图片的uri
*/
public void loadUri(String uri, LoadOptions loadOptions) {
ImageLoaderManager.INSTANCE.loadUri(this, uri, loadOptions, new ImageInfoCallback() {
@Override
public void onFailed() {}

@Override
public void onSuccess(ImageSize imageSize) {
float originHeight = imageSize.getHeight();
float originWidth = imageSize.getWidth();
float adaptiveHeight, adaptiveWidth;
if (originWidth > originHeight) {
adaptiveWidth = maxSize;
adaptiveHeight = originHeight * ((float) maxSize / originWidth);

} else {
adaptiveHeight = maxSize;
adaptiveWidth = originWidth * ((float) maxSize / originHeight);
}

ImageTools.INSTANCE.resizeView(AdaptiveImageView.this, (int)adaptiveWidth, (int)adaptiveHeight);
}
});
}

那么可以看到,这个 View 是经过 resizeView,按照赋予它的最大宽高计算过后,再重新设定宽高的,我把这个控件重新设置后的宽高输出如下:

明明 width 就是 7.35,应该可以正常显示才是,我打开手机的开发者选项中的显示布局和边距模式,也可以看到确实是存在一个很细的 ImageView 的,但是图片却不显示出来,所以说问题也不出现在这个自适应高宽高的 View 上

那就可能是加载图片的时候出了问题了,项目是用 Glide 来加载图片的。我想要看到 Glide 的输出日志,但是项目内的 Glide 调用是在封装好的库中进行的,而且封装的时候并没有去监听 Glide 的异常输出,所以看不到异常信息,我又没办法修改这个库,那就简单写个 demo 去加载这个图片试试

结果发现,把 Glide 的 into 中的 Target 改为 ImageView 的时候,图片正常地显示出来了,那么这个 into 到底是什么的,可以参考官方的文档:

目标

Glide中的目标作为请求跟请求者之间的传递者。目标负责显示占位符,加载的资源以及为每个请求确定合适的尺寸。最常用目标是使用ImageView显示占位符,Drawable和Bitmap的ImageViewTarget。用户还可以实现自己的目标,或者对任何可用的基类进行子类化。

指定目标

into(Target) 方法不仅用于启动每个请求,同时也可以指定将要接收请求结果的目标。Glide 提供了一个辅助方法给into(ImageView),其采用 ImageView 并把它包装在目标于适合所请求的资源类型。
为了方便使用自定义目标,这些 into() 方法返回提供给他们的目标

郭霖在公众号中也提及了关于这个 into 以及 Target 的一些内容,节选一部分如下:

使用了这么久的 Glide,我们都知道 into() 方法中是可以传入 ImageView 的。那么 into() 方法还可以传入别的参数吗?我可以让 Glide 加载出来的图片不显示到 ImageView 上吗?答案是肯定的,这就需要用到自定义 Target 功能。

其实通过上面的分析,我们已经知道了,into() 方法还有一个接收 Target 参数的重载。即使我们传入的参数是 ImageView,Glide 也会在内部自动构建一个Target对象。而如果我们能够掌握自定义Target技术的话,就可以更加随心所欲地控制Glide的回调了。

我们创建了一个 SimpleTarget 的实例,并且指定它的泛型是 GlideDrawable,然后重写了 onResourceReady() 方法。在 onResourceReady() 方法中,我们就可以获取到 Glide 加载出来的图片对象了,也就是方法参数中传过来的 GlideDrawable 对象。有了这个对象之后你可以使用它进行任意的逻辑操作,这里我只是简单地把它显示到了 ImageView 上

SimpleTarget 中的泛型并不一定只能是 GlideDrawable,如果你能确定你正在加载的是一张静态图而不是 GIF 图的话,我们还能直接拿到这张图的 Bitmap 对象

Glide 在内部自动帮我们创建的 GlideDrawableImageViewTarget 就是 ViewTarget 的子类。只不过 GlideDrawableImageViewTarget 被限定只能作用在 ImageView 上,而 ViewTarget 的功能更加广泛,它可以作用在任意的 View 上

所以我的大致理解如下:给这个 Target 设置一个泛型,然后它会返回给你这个泛型的对象,你就可以拿这个返回的对象去处理一些事情了,而项目中返回的是 ImageView,返回的 resource 是 Drawable,我们就可以将 Drawable 给 set 到 ImageView 中了

然而就是在这里,使用细长图片的时候,调用的回调并不是 onResourceLoading,而是 onLoadFailed,说明图片加载出了错误,而 onLoadFailed 中并不会取得抛出的错误,那么就在 Glide 的 builder 中再加一条 .listener() 方法,去获取它的详细的报错信息

整个完整的小 demo 如下所示

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
52
53
54
55
56
57
58
59
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

ImageView imageView = findViewById(R.id.iv_test);
RequestOptions requestOptions = new RequestOptions();

//requestOptions.override(80, 80);//设置width和height后可以正常显示

//requestOptions.signature(new ObjectKey(System.currentTimeMillis())); //网上说的缓存问题解决方案,并没有作用

//自己制作的细长图片https://github.com/EndeRHoshI/Photo/blob/master/blog/Glide/%E7%BB%86%E9%95%BF%E5%9B%BE%E6%A1%88.png?raw=true
//原图 https://static.aicoinstorge.com/talk/18-11-07/100518-cdc-166.jpeg
//缩略图 https://static.aicoinstorge.com/talk/18-11-07/100518-cdc-166.jpeg?x-oss-process=image/resize,m_mfit,h_408,w_4
//平时的普通图片 https://static.aicoinstorge.com/talk/18-11-08/082142-569-179.jpeg?x-oss-process=image/resize,m_mfit,h_408,w_176
//平时的普通图片 http://oz5ak1kvp.bkt.clouddn.com/cms_liuhua/user/img1516646371.jpg

//into的参数使用imageView可以正常使用,问题出在不设置宽高的 target —— CustomViewTarget 上
Glide.with(imageView)
.load("https://static.aicoinstorge.com/talk/18-11-07/100518-cdc-166.jpeg")
.apply(requestOptions)
.listener(new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
if (e != null) {
e.printStackTrace();
e.logRootCauses("root-lvqx");
Log.d("lvqx-causes","\nMessage---->" + e.getMessage() + "\nCauses---->" + e.getCauses());
}
return false;
}

@Override
public boolean onResourceReady(Drawable resource, Object model,
Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
return false;
}
})
.into(new CustomViewTarget<ImageView, Drawable>(imageView) {
@Override
public void onLoadFailed(@Nullable Drawable errorDrawable) {
Log.d("glide-test", "fail");//Options不设置宽高,设置CustomViewTarget,且图片过于细长的时候,会加载失败回调该方法
}

@Override
public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
view.setImageDrawable(resource);
}

@Override
protected void onResourceCleared(@Nullable Drawable placeholder) {
Log.d("glide-test", "cleared");
}
});
}
}

获得的完整报错信息如下:

接下来我在 e.logRootCauses 方法输出的信息中找到了抛出这个 GlideException 的地方,就是 setDataSource 方法,这个 setDataSource 方法在 Glide 的 VideoDecoder 类中的 decode 方法中调用,具体抛出异常的地方如下:

在其中的 296 行中的 setDataSource 方法抛出

错误的描述是

1
Caused by: java.lang.RuntimeException: setDataSource failed: status = 0x80000000

查到的结果是说,这个错误的意思是因为文件不完整,已损坏等等原因而读取错误

好了,那现在项目框架选择了使用 Glide 来加载图片,又封装好了特定的库去调用 Glide,无法修改其中内容,每当加载到这种奇葩细长图片的时候就无法显示,而且也不是之前同事遇到的超长图片的问题(图片的宽高远远没有达到手机限制的最大宽高,所以这个根本不是尺寸长短的问题,而是宽高比的问题,不能称之为长图加载失败,而是细长图加载失败….),那么到底要怎样解决呢

我再去看了下官方文档,看到了以下这条

override方法的官方api文档解释如下

这下可以改变 Target 的宽高来达到目的了,而且文档中也说了,应该只用于你需要一个很特别的图像尺寸的时候,这种细长图,够奇葩了,那就试着设置了一下,发现只要是设置了这个,图像都可以显示了,无论是 override(1, 1) 还是 override(80, 80),都可以显示出来,只是如果设置宽高为1,会把图像处理成宽度为1个像素,然后拉伸填满 View,这样就会让图像面目全非,宽度 80 会好一点,那这个数当然不能写死,如果图像比较大的时候就会模糊不清,刚好看到接口中后台为了让 iOS 端正确显示图片而有传图像的宽高过来,把这个设置进去,就刚好可以设置 Target 的高度让其符合这张奇葩图片了,图像完美地显示了出来,bug 至此就修复了

总结

bug 已经修复,但是问题还是很多,总结起来就是封装第三方库的时候最好把输出异常信息的操作也封装进去,这样会比较方便排查故障

然后还有几点没有弄懂的:

  1. 为什么普通的图片没有设置宽高,可以正常显示,而这个细长图片就不可以呢,是因为不能自适应?
  2. 如果不能自适应,那么为什么 Target 设置成 ImageView 的时候又可以显示了呢?
  3. setDataSource 方法只有在使用细长图案的时候才会调用到,然后抛出异常,我设置了断点,但是在使用正常图案时并没有跑到 setDataSource 方法中去,为什么呢?
  4. 这个 CustomViewTarget 到底干了啥,它在处理细长图案时是怎样的?现在看源码还是看不太懂

如果以后技术深入到了这个程度,也有这样的闲情逸致再去研究,再接着更新


Glide 无法加载细长图片解决方法
https://enderhoshi.github.io/2018/11/08/Glide 无法加载细长图片解决方法/
作者
HoshIlIlI
发布于
2018年11月8日
许可协议