作者:newki
前言
Glide 相信大家都不陌生,各種源碼分析,使用介紹大家應該都是爛熟于心。但是設置 Glide 的超時問題大家遇到過沒有。
我遇到了,并且掉坑里了,情況是這樣的。
- 調用接口從網絡拉取用戶頭像,目前數據量不大,大致1000多個人。(用了自定義隊列)
- 使用 Glide 下載頭像到本地沙盒 File (為了方便的緩存下次更快)。
- 識別頭像中的人臉信息,并生成人臉Bitmap,(本身有成功失敗的處理與重試機制)
- 生成人臉對應的特征,并保存人臉特征數據和人臉特征圖片到沙盒 File 。
- 封裝人臉對象并加載到內存中保持全局單例。
- 場景業務:與Camera的預覽畫面中獲取到的活體人臉進行人臉比對。
開始并沒有設置超時時間,導致 Glide下載圖片的自定義隊列常常會出現卡死的情況,導致整個隊列執行緩慢甚至都無法繼續執行,整個注冊服務被阻塞,新進來的用戶一直等待時間過長甚至無法注冊。
問題嘛,就是圖片加載的問題,有些圖片無法加載,有些圖片太大加載時間過長,有些根本就不是圖片,有些網絡慢,不穩定,或者干脆就無網,有些是訪問權限問題,為了讓圖片下載隊列能正常運轉加入了 Glide 的超時機制,踩坑之路由此展開。
一、問題復現
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 = 0Lvar endMillis = 0LGlideApp.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.1 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() {}@Overridepublic 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.1 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.1 換成 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 如下:
哎,真的是頭都大了,不是說好的開箱即用嗎,咋個這么多問題,還分這么多情況,真不知道該如何是好。
二、問題解決1,使用 OkHttp3 的自定義 Client
既然我們使用 OkHttp 之后,無法在 Glide 中修改超時時間,那么我們直接修改 OkHttp 的超時時間可不不可以?
大家或多或少都配置過,這里直接貼代碼:
@GlideModule
public final class HttpGlideModule extends AppGlideModule {@Overridepublic 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() {@Overridepublic void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {}@Overridepublic void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {}@Overridepublic X509Certificate[] getAcceptedIssuers() {return new X509Certificate[]{};}}};}private static HostnameVerifier getHostnameVerifier() {return new HostnameVerifier() {@Overridepublic boolean verify(String hostname, SSLSession session) {return true;}};}}
可以看到我們設置了15秒的超時,打印的結果如下:
想設置幾秒就是幾秒,沒有重試導致時間不對一說。這確實是一種方案。
三、問題解決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)}}
這也是比較方便的一種方案。
后記
如果是網絡請求,不管是接口的Http或者是Glide的圖片加載,我們可以使用OkHttp加載,可以設置 OkHttpClient 的 Timeout 屬性來設置超時。
如果是其他的異步操作,我們也可以使用協程的 timeout 函數直接在上層超時取消協程,也能達到目的。
兩種方法都是可以的,我個人是選擇了協程 timeout 的方式,因為我發現有些情況下就算設置 OkHttp 的超時,偶爾還是會長時間超時。如網絡連接較慢或不穩定,如服務端沒有及時響應或響應時間過長,那么超時機制將無法起作用。所以為了保險起見還是使用協程 timeout 直接上層處理了,更新之后目前運行狀況良好。
Android 學習筆錄
Android 性能優化篇:https://qr18.cn/FVlo89
Android 車載篇:https://qr18.cn/F05ZCM
Android 逆向安全學習筆記:https://qr18.cn/CQ5TcL
Android Framework底層原理篇:https://qr18.cn/AQpN4J
Android 音視頻篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(內含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源碼解析筆記:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知識體:https://qr18.cn/CyxarU
Android 核心筆記:https://qr21.cn/CaZQLo
Android 往年面試題錦:https://qr18.cn/CKV8OZ
2023年最新Android 面試題集:https://qr18.cn/CgxrRy
Android 車載開發崗位面試習題:https://qr18.cn/FTlyCJ
音視頻面試題錦:https://qr18.cn/AcV6Ap