查看原文
其他

坏了,Glide的超时居然失控了!这可如何是好

newki 鸿洋 2023-09-13

本文作者


作者:newki

链接:

https://juejin.cn/post/7265332526544814119

本文由作者授权发布。


前言

Glide 相信大家都不陌生,各种源码分析,使用介绍大家应该都是烂熟于心。但是设置 Glide 的超时问题大家遇到过没有。
我遇到了,并且掉坑里了,情况是这样的。
  1. 调用接口从网络拉取用户头像,目前数据量不大,大致1000多个人。(用了自定义队列)
  2. 使用 Glide 下载头像到本地沙盒 File (为了方便的缓存下次更快)。
  3. 识别头像中的人脸信息,并生成人脸Bitmap,(本身有成功失败的处理与重试机制)
  4. 生成人脸对应的特征,并保存人脸特征数据和人脸特征图片到沙盒 File 。
  5. 封装人脸对象并加载到内存中保持全局单例。
  6. 场景业务:与Camera的预览画面中获取到的活体人脸进行人脸比对。
开始并没有设置超时时间,导致 Glide下载图片的自定义队列常常会出现卡死的情况,导致整个队列执行缓慢甚至都无法继续执行,整个注册服务被阻塞,新进来的用户一直等待时间过长甚至无法注册。

问题嘛,就是图片加载的问题,有些图片无法加载,有些图片太大加载时间过长,有些根本就不是图片,有些网络慢,不稳定,或者干脆就无网,有些是访问权限问题,为了让图片下载队列能正常运转加入了 Glide 的超时机制,踩坑之路由此展开。

1问题复现


Glide的使用,大家应该都清除,如何加timeout,这里给出一个示例代码:
依赖:
implementation 'com.github.bumptech.glide:glide:4.15.1'
implementation 'com.github.bumptech.glide:annotations:4.15.1'
kapt 'com.github.bumptech.glide:compiler:4.15.1'
下载的方法使用一个扩展方法封装了一下 :
fun Any.extDownloadImage(context: Context?, path: Any?, block: (file: File) -> Unit) {

    var startMillis = 0L
    var endMillis = 0L


    GlideApp.with(context!!)
        .load(path)
        .timeout(15000)  // 15秒
        .downloadOnly(object : SimpleTarget<File?>() {

            override fun onLoadStarted(placeholder: Drawable?) {
                startMillis = System.currentTimeMillis()
                YYLogUtils.w("开始加载:$startMillis")
                super.onLoadStarted(placeholder)
            }

            override fun onLoadFailed(errorDrawable: Drawable?) {
                endMillis = System.currentTimeMillis()
                YYLogUtils.w("Glide-onLoadFailed-Drawable,一共耗时:${endMillis - startMillis}")
                super.onLoadFailed(errorDrawable)
            }

            override fun onResourceReady(resource: File, transition: Transition<in File?>?) {
                endMillis = System.currentTimeMillis()
                YYLogUtils.w("Glide-onResourceReady-Drawable,一共耗时:${endMillis - startMillis}")
                block(resource)
            }
        })

}
大家使用工具类或者直接 Glide 写都是一样的效果,不影响最终的结果。
使用:
    val url = "https://s3.ap-southeast-1.amazonaws.com/yycircle-ap/202307/11/KZ8xIVsrlrYtjhw3t2t2RTUj0ZTWUFr2EhawOd4I-810x1080.jpeg"

    extDownloadImage(this@MainActivity, url, block = { file ->

        YYLogUtils.w("file:${file.absolutePath}")

    })
以亚马逊云服务的图片地址为例,不同的网络情况,不同的网络加载框架情况下,分别有什么不同。

1.1 HttpURLConnection 没网的情况

原生 Glide 的网络请求源码在 HttpUrlFetcher 类中。
具体方法:
就算我们在 buildAndConfigureConnection 中设置了超时时间,但是 connect 方法直接就报错了,也不会走timeout的逻辑。
com.bumptech.glide.load.HttpException: Failed to connect or obtain data, status code: -1

1.2 HttpURLConnection 有网的但是不通

那如果有网,但是网不通呢?
这下确实会等待一小会了,由于我们设置的超时时间是15秒,打印Log看看。
class com.bumptech.glide.load.HttpException: Failed to connect or obtain data, status code: -1
错误和上面一样,但是超时时间是10秒:
喂,玩我是吧。那我改 Glide 的超时时间为 5000, 也就是5秒,但是最终的结果还是10秒。
这是为什么呢?虽然连上了WIFI,但是没网,还是无法解析hostname,而 HttpURLConnection 内部定义的这一阶段的超时就是 10 秒。
我们可以把 Glide 的网络请求源码拷过来试试!
class HttpTest {

    private final HttpUrlConnectionFactory connectionFactory = new DefaultHttpUrlConnectionFactory();

    public HttpTest() {
    }


    public HttpURLConnection buildAndConfigureConnection(URL url, Map<String, String> headers) throws HttpException {
        HttpURLConnection urlConnection;
        try {
            urlConnection = connectionFactory.build(url);
        } catch (IOException e) {
            throw new RuntimeException("URL.openConnection threw");
        }
        for (Map.Entry<String, String> headerEntry : headers.entrySet()) {
            urlConnection.addRequestProperty(headerEntry.getKey(), headerEntry.getValue());
        }
        urlConnection.setConnectTimeout(7000);
        urlConnection.setReadTimeout(7000);
        urlConnection.setUseCaches(false);
        urlConnection.setDoInput(true);
        urlConnection.setInstanceFollowRedirects(false);
        return urlConnection;
    }

    interface HttpUrlConnectionFactory {
        HttpURLConnection build(URL url) throws IOException;
    }

    private static class DefaultHttpUrlConnectionFactory implements HttpUrlConnectionFactory {

        DefaultHttpUrlConnectionFactory() {}

        @Override
        public HttpURLConnection build(URL url) throws IOException {
            return (HttpURLConnection) url.openConnection();
        }
    }
}
为了和之前的区别开,我们设置7秒的超时,看看结果有什么变化?
java.net.UnknownHostException: Unable to resolve host "s3.ap-southeast-1.amazonaws.com": No address associated with hostname
错误已经很明显了,哎。

1.3 HttpURLConnection 有网通了,但是没访问权限

那我现在把网连上,把授权关掉,虽然能解析域名,但是没有访问权限,还是无法获取图片,此时又会出现什么情况。
我们还是设置为15秒的超时:
 GlideApp.with(context!!)
        .load(path)
        .apply(options)
        .timeout(15000)
        .into(object : SimpleTarget<Drawable>() {

            override fun onLoadStarted(placeholder: Drawable?) {
                startMillis = System.currentTimeMillis()
                YYLogUtils.w("开始加载:$startMillis")
                super.onLoadStarted(placeholder)
            }

            override fun onLoadFailed(errorDrawable: Drawable?) {
                endMillis = System.currentTimeMillis()
                YYLogUtils.w("Glide-onLoadFailed-Drawable,一共耗时:${endMillis - startMillis}")
                super.onLoadFailed(errorDrawable)
            }

            override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
                endMillis = System.currentTimeMillis()
                YYLogUtils.w("Glide-onResourceReady-Drawable,一共耗时:${endMillis - startMillis}")
                block(resource)
            }

        })
出错的信息,这次网络请求确实是通了,确实是走到 timeout 里面了。
但是这个时间为什么是30秒?
如果我们设置超时时间是20秒?那么结果就是40秒!
HttpURLConnection 的问题?我们还是用上一步的 7秒超时的原生 HttpURLConnection 代码访问试试!
可以看到结果是符合我们预期的7秒超时。
那为什么 Glide 默认的 HttpURLConnection 会是两倍的超时时间呢?
是因为 Glide 内部对 HttpURLConnection 的请求做了重试处理。
当它第一次超时的时候,会走到错误回调中,但是并没有回调出去,而是自己处理了一遍。
真的太迷了,我自己不会学重试吗,要你多管闲事...

1.4 换成 OkHttp3

如果摆脱这一套 HttpURLConnection 的逻辑与重试逻辑,Glide 也提供了第三方网络请求的接口,例如我们常用的用 OkHttp 来加载图片。
大家应该是不陌生的,加入依赖库即可:
implementation 'com.github.bumptech.glide:okhttp3-integration:4.15.1'
此时已经换成OkHttp加载了,它默认的超时时间就是10秒,此时我们修改Glide的超时时间是无效的。
GlideApp.with(context!!)
    .load(path)
    .apply(options)
    .timeout(20000
    .into(object : SimpleTarget<Drawable>() {

        override fun onLoadStarted(placeholder: Drawable?) {
            startMillis = System.currentTimeMillis()
            YYLogUtils.w("开始加载:$startMillis")
            super.onLoadStarted(placeholder)
        }

        override fun onLoadFailed(errorDrawable: Drawable?) {
            endMillis = System.currentTimeMillis()
            YYLogUtils.w("Glide-onLoadFailed-Drawable,一共耗时:${endMillis - startMillis}")
            super.onLoadFailed(errorDrawable)
        }

        override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
            endMillis = System.currentTimeMillis()
            YYLogUtils.w("Glide-onResourceReady-Drawable,一共耗时:${endMillis - startMillis}")
            block(resource)
        }

    })
别说改成20秒,改成100秒也无效!因为这些配置是修改的默认的 HttpURLConnection 的超时时间的。OkHttp的加载根本就不走那一套了。
打印 Log 如下:

哎,真的是头都大了,不是说好的开箱即用吗,咋个这么多问题,还分这么多情况,真不知道该如何是好。

2问题解决1,使用 OkHttp3 的自定义 Client


既然我们使用 OkHttp 之后,无法在 Glide 中修改超时时间,那么我们直接修改 OkHttp 的超时时间可不不可以?
大家或多或少都配置过,这里直接贴代码:
@GlideModule
public final class HttpGlideModule extends AppGlideModule {

    @Override
    public void registerComponents(Context context, Glide glide, Registry registry) {
        // 替换自定义的Glide网络加载
        registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory(GlideOkHttpUtils.getHttpClient()));
    }
}
实现我们自己的 OkHttpClient 类:
public class GlideOkHttpUtils {

    public static OkHttpClient getHttpClient() {
        OkHttpClient.Builder builder = new OkHttpClient.Builder()
                .connectTimeout(15, TimeUnit.SECONDS)
                .addInterceptor(new LoggingInterceptor())  //打印请求日志,可有可无
                .sslSocketFactory(getSSLSocketFactory())
                .hostnameVerifier(getHostnameVerifier());
        return builder.build();
    }


    /**
     * getSSLSocketFactory、getTrustManagers、getHostnameVerifier
     * 使OkHttpClient支持自签名证书,避免Glide加载不了Https图片
     */

    private static SSLSocketFactory getSSLSocketFactory() {
        try {
            SSLContext sslContext = SSLContext.getInstance("SSL");
            sslContext.init(null, getTrustManagers(), new SecureRandom());
            return sslContext.getSocketFactory();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static TrustManager[] getTrustManagers() {
        return new TrustManager[]{new X509TrustManager() {

            @Override
            public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            }

            @Override
            public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            }

            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return new X509Certificate[]{};
            }
        }};
    }

    private static HostnameVerifier getHostnameVerifier() {
        return new HostnameVerifier() {
            @Override
            public boolean verify(String hostname, SSLSession session) {
                return true;
            }
        };
    }

}
可以看到我们设置了15秒的超时,打印的结果如下:

想设置几秒就是几秒,没有重试导致时间不对一说。这确实是一种方案。

3问题解决2,使用协程timeout


另一种方案就是使用协程的超时来控制,由于 Glide 的加载图片与回调的处理是匿名函数实现的,内部回调的处理我们先用协程处理铺平回调。
之前讲过,这里直接上代码。
suspend fun Any.downloadImageWithGlide(imgUrl: String): File {
    return suspendCancellableCoroutine { cancellableContinuation ->

        GlideApp.with(commContext())
            .load(imgUrl)
            .timeout(15000)  //设不设都一样,反正不靠你
            .diskCacheStrategy(DiskCacheStrategy.DATA)
            .downloadOnly(object : SimpleTarget<File?>() {

                override fun onResourceReady(resource: File, transition: Transition<in File?>?) {
                    cancellableContinuation.resume(resource)
                }

                override fun onLoadFailed(errorDrawable: Drawable?) {
                    super.onLoadFailed(errorDrawable)

                    cancellableContinuation.resumeWithException(RuntimeException("加载失败了"))

                }
            })

    }
}
使用起来我们就是协程的 timeout 函数,不管底层是什么实现的,直接上层的超时拦截。
launch{

   ...

   try {

        val file = withTimeout(15000) {
            downloadImageWithGlide(userInfo.avatarUrl)
        }

        YYLogUtils.e("注册人脸服务-图片加载成功:${file.absolutePath}")
        //下载成功之后赋值本地路径到对象中
        userInfo.avatarPath = file.absolutePath
        //去注册人脸
        registerHotelMember(userInfo)

    } catch (e: TimeoutCancellationException) {
        YYLogUtils.e("注册人脸服务-图片加载超时:${e.message}")
        checkImageDownloadError(userInfo)
    } catch (e: Exception) {
        YYLogUtils.e("注册人脸服务-图片加载错误:${e.message}")
        checkImageDownloadError(userInfo)
    }
}
这也是比较方便的一种方案。
4后记


如果是网络请求,不管是接口的Http或者是Glide的图片加载,我们可以使用OkHttp加载,可以设置 OkHttpClient 的 Timeout 属性来设置超时。
如果是其他的异步操作,我们也可以使用协程的 timeout 函数直接在上层超时取消协程,也能达到目的。
两种方法都是可以的,我个人是选择了协程 timeout 的方式,因为我发现有些情况下就算设置 OkHttp 的超时,偶尔还是会长时间超时。如网络连接较慢或不稳定,如服务端没有及时响应或响应时间过长,那么超时机制将无法起作用。所以为了保险起见还是使用协程 timeout 直接上层处理了,更新之后目前运行状况良好。
代码比较简答都已经在文中贴出。
如果本文的讲解有什么错漏的地方,希望同学们一定要指出哦。有疑问也可以评论区交流学习进步,谢谢!
当然如果觉得本文还不错对你有些帮助的话,还请点赞支持一下哦,你的支持是我最大的动力啦!
Ok,这一期就此完结。


最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!


推荐阅读

2023 Android 折叠屏适配详解,是时候点亮新技能了
Kotlin 泛型擦除不要慌,reified 来帮忙
“终于懂了“系列:Android性能优化—FPS提升实战


扫一扫 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存