Implement image request merging in standalone Interceptor, Don't modify coil

This commit is contained in:
tarsin committed 2023-02-19 18:08:35 +08:00
1 parent 16e7d266fe
commit d15c5fc9d1
3 files changed
+79 -2

No files matched your search

@@ -35,6 +35,7 @@ import com.hippo.ehviewer.client.EhDns
import com.hippo.ehviewer.client.EhSSLSocketFactory
import com.hippo.ehviewer.client.EhTagDatabase
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.coil.MergeInterceptor
import com.hippo.ehviewer.dao.buildMainDB
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.ui.CommonOperations
@@ -186,6 +187,7 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(this).apply {
okHttpClient(okHttpClient)
components { add(MergeInterceptor) }
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
// Coil spawns a new thread for every image load by default
fetcherDispatcher(Dispatchers.IO.limitedParallelism(8))
@@ -0,0 +1,76 @@
package com.hippo.ehviewer.coil
import coil.intercept.Interceptor
import coil.request.ImageRequest
import coil.request.ImageResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
object MergeInterceptor : Interceptor {
private val pendingContinuationMap: HashMap<String, MutableList<Continuation<Unit>>> = hashMapOf()
private val pendingContinuationMapLock = Mutex()
private val notifyScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
val originRequest = chain.request
val originListener = originRequest.listener
val key = originRequest.data as String
var newRequest = originRequest
pendingContinuationMapLock.lock()
val existPendingContinuations = pendingContinuationMap[key]
if (existPendingContinuations == null) {
pendingContinuationMap[key] = mutableListOf()
newRequest = ImageRequest.Builder(originRequest).listener(
{ originListener?.onStart(it) },
{
originListener?.onCancel(it)
notifyScope.launch {
pendingContinuationMapLock.withLock {
// Wake up a pending continuation to continue executing task
val successor = pendingContinuationMap[key]?.removeFirstOrNull()?.apply { resume(Unit) }
// If no successor, delete this entry from hashmap
successor ?: pendingContinuationMap.remove(key)
}
}
},
{ request, result ->
originListener?.onError(request, result)
// Wake all pending continuations since this request is to be failed
notifyScope.launch {
pendingContinuationMapLock.withLock {
pendingContinuationMap.remove(key)?.forEach { it.resume(Unit) }
}
}
},
{ request, result ->
originListener?.onSuccess(request, result)
// Wake all pending continuations shared with the same memory key since we have written it to memory cache
notifyScope.launch {
pendingContinuationMapLock.withLock {
pendingContinuationMap.remove(key)?.forEach { it.resume(Unit) }
}
}
}
).build()
pendingContinuationMapLock.unlock()
} else {
existPendingContinuations.apply {
suspendCancellableCoroutine { continuation ->
add(continuation)
pendingContinuationMapLock.unlock()
continuation.invokeOnCancellation { remove(continuation) }
}
}
}
return withContext(originRequest.interceptorDispatcher) { chain.proceed(newRequest) }
}
}