Skip to content

Commit 69bca27

Browse files
committed
♻️ [kmp/pureImage] 因为LoaderCacheMap的变更,重构相关的使用
现在图像的缓存跟着视图走,LoaderCacheMap主要用来共享加载任务。 默认提升LoaderCacheMap的大小以提升命中率
1 parent 93052c3 commit 69bca27

6 files changed

Lines changed: 126 additions & 85 deletions

File tree

next/kmp/browser/src/commonMain/kotlin/org/dweb_browser/browser/desk/render/activity/ActivityIconRender.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ fun ActivityItem.ImageIcon.Render(
4141
coilImageRequest?.also { imageRequest ->
4242
val painter = rememberAsyncImagePainter(
4343
model = imageRequest,
44-
imageLoader = LocalCoilImageLoader.current.loader()
44+
imageLoader = LocalCoilImageLoader.current.ImageLoader()
4545
)
4646
Image(painter = painter, contentDescription = null, modifier = modifier)
4747
} ?: Image(bitmap = it, contentDescription = null, modifier = modifier)

next/kmp/helper/src/commonMain/kotlin/org/dweb_browser/helper/SafeHashMap.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ public class SafeHashMap<K, V>(public val origin: MutableMap<K, V> = mutableMapO
3939
public inline fun getOrElse(key: K, defaultValue: () -> V): V =
4040
sync { getOrElse(key, defaultValue) }
4141

42+
public fun toList(): List<Pair<K, V>> = sync { toList() }
43+
4244
override fun toString(): String {
4345
return origin.toString()
4446
}

next/kmp/pureImage/src/commonMain/kotlin/org/dweb_browser/pure/image/compose/CoilImageLoader.kt

Lines changed: 49 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ package org.dweb_browser.pure.image.compose
33
import androidx.compose.runtime.Composable
44
import androidx.compose.runtime.collectAsState
55
import androidx.compose.runtime.compositionLocalOf
6+
import androidx.compose.runtime.remember
67
import coil3.ComponentRegistry
78
import coil3.ImageLoader
89
import coil3.PlatformContext
910
import coil3.SingletonImageLoader
10-
import coil3.annotation.ExperimentalCoilApi
1111
import coil3.compose.LocalPlatformContext
1212
import coil3.disk.DiskCache
1313
import coil3.memory.MemoryCache
@@ -29,9 +29,7 @@ import io.ktor.util.date.GMTDate
2929
import io.ktor.util.flattenEntries
3030
import io.ktor.utils.io.InternalAPI
3131
import kotlinx.coroutines.CoroutineDispatcher
32-
import kotlinx.coroutines.delay
3332
import kotlinx.coroutines.flow.MutableStateFlow
34-
import kotlinx.coroutines.flow.StateFlow
3533
import kotlinx.coroutines.launch
3634
import org.dweb_browser.helper.Debugger
3735
import org.dweb_browser.helper.buildUrlString
@@ -46,7 +44,6 @@ import org.dweb_browser.pure.http.ktor.KtorPureClient
4644
import org.dweb_browser.pure.http.ktor.toPureBody
4745
import org.dweb_browser.pure.image.removeOriginAndAcceptEncoding
4846
import kotlin.coroutines.CoroutineContext
49-
import kotlin.math.min
5047
import kotlin.time.measureTimedValue
5148

5249
val debugCoilImageLoader = Debugger("coilImageLoader")
@@ -66,32 +63,34 @@ class CoilImageLoader(private val diskCache: DiskCache? = null) : PureImageLoade
6663
}
6764

6865
private val scope = globalDefaultScope
69-
private val caches = LoaderCacheMap<MutableStateFlow<ImageLoadResult>>(scope)
66+
private val sharedLoaderResults = LoaderCacheMap<MutableStateFlow<ImageLoadResult>>(scope)
7067

7168
@Composable
7269
override fun Load(
7370
task: LoaderTask,
7471
): ImageLoadResult {
7572
val platformContext = LocalPlatformContext.current
76-
return load(platformContext, loader(platformContext), task).collectAsState().value
73+
val imageLoader = this@CoilImageLoader.ImageLoader(platformContext)
74+
val taskLoader = remember(task.key, imageLoader, platformContext) {
75+
startLoad(platformContext, imageLoader, task)
76+
}
77+
return taskLoader.result.collectAsState().value
7778
}
7879

7980
@Composable
80-
fun loader(platformContext: PlatformContext = LocalPlatformContext.current): ImageLoader {
81-
return getLoader(platformContext)
81+
fun ImageLoader(platformContext: PlatformContext = LocalPlatformContext.current): ImageLoader {
82+
return remember(platformContext) { getLoader(platformContext) }
8283
}
8384

84-
@OptIn(ExperimentalCoilApi::class)
85-
fun load(
86-
context: PlatformContext,
87-
loader: ImageLoader,
88-
task: LoaderTask,
89-
): StateFlow<ImageLoadResult> {
90-
val cache = caches.get(task)
91-
return cache ?: run {
92-
val imageResultState = MutableStateFlow(ImageLoadResult.Setup)
93-
val cacheItem = CacheItem(task, imageResultState)
94-
caches.save(cacheItem)
85+
inner class TaskLoader(
86+
val task: LoaderTask,
87+
val context: PlatformContext,
88+
val loader: ImageLoader,
89+
val result: MutableStateFlow<ImageLoadResult>,
90+
) {
91+
init {
92+
val imageResultState = this.result
93+
9594
scope.launch {
9695
val requestHref = task.url.replace("{WIDTH}", task.containerWidth.toString())
9796
.replace("{HEIGHT}", task.containerHeight.toString())
@@ -123,13 +122,6 @@ class CoilImageLoader(private val diskCache: DiskCache? = null) : PureImageLoade
123122
is ErrorResult -> ImageLoadResult.error(result.throwable).also { res ->
124123
val failTimes = PureImageLoader.urlErrorCount.getOrPut(task.url) { 0 } + 1
125124
PureImageLoader.urlErrorCount[task.url] = failTimes
126-
launch {
127-
/// 失败后,定时删除缓存。失败的次数越多,定时越久
128-
delay(min(failTimes * failTimes * 1000L, 30000L)) // 1 4 9 16 25 30 30 30
129-
if (cacheItem.result.value == res) {
130-
caches.delete(task, cacheItem)
131-
}
132-
}
133125
}
134126

135127
is SuccessResult -> {
@@ -146,6 +138,33 @@ class CoilImageLoader(private val diskCache: DiskCache? = null) : PureImageLoade
146138
}
147139
}
148140

141+
142+
fun startLoad(
143+
context: PlatformContext,
144+
loader: ImageLoader,
145+
task: LoaderTask,
146+
): TaskLoader {
147+
return TaskLoader(
148+
task,
149+
context,
150+
loader,
151+
sharedLoaderResults.get(task) ?: MutableStateFlow(ImageLoadResult.Setup)
152+
).also { loader ->
153+
val cacheItem = CacheItem(task, loader.result)
154+
sharedLoaderResults.save(cacheItem)
155+
ResvgImageLoader.Companion.scope.launch {
156+
loader.result.collect { result ->
157+
if (result.isError) {
158+
/// 失败后,移除执行缓存。但是这里的result仍然不会变
159+
if (cacheItem.result.value == loader.result) {
160+
sharedLoaderResults.delete(task, cacheItem)
161+
}
162+
}
163+
}
164+
}
165+
}
166+
}
167+
149168
companion object {
150169
val defaultInstance by lazy { CoilImageLoader(null) }
151170
private val defaultHttpClient =
@@ -215,10 +234,10 @@ class CoilImageLoader(private val diskCache: DiskCache? = null) : PureImageLoade
215234
addPlatformComponents()
216235
}.build()
217236
).memoryCache {
218-
MemoryCache.Builder()
219-
// Set the max size to 25% of the app's available memory.
220-
.maxSizePercent(platformContext, percent = 0.25).build()
221-
}.diskCache(diskCache)
237+
MemoryCache.Builder()
238+
// Set the max size to 25% of the app's available memory.
239+
.maxSizePercent(platformContext, percent = 0.25).build()
240+
}.diskCache(diskCache)
222241
// Show a short crossfade when loading images asynchronously.
223242
.crossfade(true).build()
224243
}

next/kmp/pureImage/src/commonMain/kotlin/org/dweb_browser/pure/image/compose/LoaderCacheMap.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ data class CacheItem<T>(
1414
internal var hot = 30f
1515
}
1616

17-
class LoaderCacheMap<T : Any>(scope: CoroutineScope, var cacheSize: Int = 10) {
17+
class LoaderCacheMap<T : Any>(scope: CoroutineScope, var cacheSize: Int = 30) {
1818
private val map = SafeHashMap<String, CacheItem<T>>()
1919
private val lruList = SafeLinkList<CacheItem<T>>()
2020

next/kmp/pureImage/src/commonMain/kotlin/org/dweb_browser/pure/image/compose/ResvgImageLoader.kt

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ package org.dweb_browser.pure.image.compose
33
import androidx.compose.runtime.Composable
44
import androidx.compose.runtime.collectAsState
55
import androidx.compose.runtime.compositionLocalOf
6-
import kotlinx.coroutines.delay
6+
import androidx.compose.runtime.remember
77
import kotlinx.coroutines.flow.MutableStateFlow
8-
import kotlinx.coroutines.flow.StateFlow
98
import kotlinx.coroutines.launch
109
import org.dweb_browser.helper.Debugger
1110
import org.dweb_browser.helper.compose.toCssRgba
@@ -22,7 +21,6 @@ import org.dweb_browser.pure.http.fetch
2221
import resvg_render.FitMode
2322
import resvg_render.RenderOptions
2423
import resvg_render.svgToPng
25-
import kotlin.math.min
2624

2725
val LocalResvgImageLoader = compositionLocalOf { ResvgImageLoader.defaultInstance }
2826
val debugResvg = Debugger("resvg")
@@ -37,31 +35,28 @@ class ResvgImageLoader : PureImageLoader {
3735

3836
@Composable
3937
override fun Load(task: LoaderTask): ImageLoadResult {
40-
return load(task).collectAsState().value
38+
val loader = remember(task.key) {
39+
startLoad(task)
40+
}
41+
return loader.result.collectAsState().value
4142
}
4243

43-
private val caches = LoaderCacheMap<MutableStateFlow<ImageLoadResult>>(scope)
44+
private val sharedLoaderResults = LoaderCacheMap<MutableStateFlow<ImageLoadResult>>(scope)
4445

4546
@Composable
4647
fun getLoadCache(task: LoaderTask): ImageLoadResult? {
47-
return caches.get(task)?.collectAsState()?.value
48+
return sharedLoaderResults.get(task)?.collectAsState()?.value
4849
}
4950

50-
fun load(
51-
task: LoaderTask,
52-
): StateFlow<ImageLoadResult> {
53-
val cache = caches.get(task)
54-
return cache ?: run {
55-
val imageResultState = MutableStateFlow(ImageLoadResult.Setup)
56-
val cacheItem = CacheItem(task, imageResultState)
57-
caches.save(cacheItem)
51+
inner class TaskLoader(val task: LoaderTask, val result: MutableStateFlow<ImageLoadResult>) {
52+
init {
5853
scope.launch {
5954
runCatching {
60-
imageResultState.emit(ImageLoadResult.Loading)
55+
result.emit(ImageLoadResult.Loading)
6156
val pureResponse =
6257
task.hook?.invoke(FetchHookContext(PureServerRequest(task.url, PureMethod.GET)))
6358
?: client.fetch(task.url);
64-
imageResultState.emit(ImageLoadResult.Loading)
59+
result.emit(ImageLoadResult.Loading)
6560

6661
val svgData = when (val currentColor = task.currentColor) {
6762
null -> pureResponse.binary()
@@ -83,28 +78,39 @@ class ResvgImageLoader : PureImageLoader {
8378
)
8479
)
8580
pngData.toImageBitmap()?.let {
86-
imageResultState.emit(ImageLoadResult.success(it))
81+
result.emit(ImageLoadResult.success(it))
8782
} ?: run {
88-
imageResultState.emit(ImageLoadResult.error(Exception("image decode fail")))
83+
result.emit(ImageLoadResult.error(Exception("image decode fail")))
8984
}
9085
}.getOrElse {
9186
debugResvg("load", "fail", it)
9287
val failTimes = PureImageLoader.urlErrorCount.getOrPut(task.url) { 0 } + 1
9388
PureImageLoader.urlErrorCount[task.url] = failTimes
9489

95-
imageResultState.emit(ImageLoadResult.error(it).also { res ->
96-
launch {
97-
/// 失败后,定时删除缓存。失败的次数越多,定时越久
98-
delay(min(failTimes * failTimes * 1000L, 30000L)) // 1 4 9 16 25 30 30 30
99-
if (cacheItem.result.value == res) {
100-
caches.delete(task, cacheItem)
101-
}
102-
}
103-
})
90+
result.emit(ImageLoadResult.error(it))
10491
}
10592
}
10693

107-
imageResultState
94+
}
95+
}
96+
97+
fun startLoad(task: LoaderTask): TaskLoader {
98+
return TaskLoader(
99+
task,
100+
sharedLoaderResults.get(task) ?: MutableStateFlow(ImageLoadResult.Setup)
101+
).also { loader ->
102+
val cacheItem = CacheItem(task, loader.result)
103+
sharedLoaderResults.save(cacheItem)
104+
scope.launch {
105+
loader.result.collect { result ->
106+
if (result.isError) {
107+
/// 失败后,移除执行缓存。但是这里的result仍然不会变
108+
if (cacheItem.result.value == loader.result) {
109+
sharedLoaderResults.delete(task, cacheItem)
110+
}
111+
}
112+
}
113+
}
108114
}
109115
}
110116
}

next/kmp/pureImage/src/commonMain/kotlin/org/dweb_browser/pure/image/compose/WebImageLoader.kt

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,15 @@ package org.dweb_browser.pure.image.compose
33
import androidx.compose.runtime.Composable
44
import androidx.compose.runtime.InternalComposeApi
55
import androidx.compose.runtime.collectAsState
6-
import kotlinx.coroutines.delay
6+
import androidx.compose.runtime.remember
77
import kotlinx.coroutines.flow.MutableStateFlow
8-
import kotlinx.coroutines.flow.StateFlow
98
import kotlinx.coroutines.launch
109
import org.dweb_browser.helper.compose.compositionChainOf
1110
import org.dweb_browser.helper.globalDefaultScope
1211
import org.dweb_browser.pure.image.OffscreenWebCanvas
1312
import org.dweb_browser.pure.image.offscreenwebcanvas.WebCanvasContextSession.Companion.buildTask
1413
import org.dweb_browser.pure.image.offscreenwebcanvas.waitReady
1514
import org.dweb_browser.pure.image.setHook
16-
import kotlin.math.min
1715

1816

1917
val LocalWebImageLoader = compositionChainOf("WebImageLoader") { WebImageLoader.defaultInstance }
@@ -33,27 +31,28 @@ class WebImageLoader : PureImageLoader {
3331
@OptIn(InternalComposeApi::class)
3432
@Composable
3533
override fun Load(task: LoaderTask): ImageLoadResult {
36-
return load(rememberOffscreenWebCanvas(), task).collectAsState().value
34+
val webCanvas = rememberOffscreenWebCanvas()
35+
val loader = remember(task.key, webCanvas) {
36+
startLoad(task, webCanvas)
37+
}
38+
return loader.result.collectAsState().value
3739
}
3840

3941

40-
private val caches = LoaderCacheMap<MutableStateFlow<ImageLoadResult>>(scope)
42+
private val sharedLoaderResults = LoaderCacheMap<MutableStateFlow<ImageLoadResult>>(scope)
4143

4244

4345
@Composable
4446
fun getLoadCache(task: LoaderTask): ImageLoadResult? {
45-
return caches.get(task)?.collectAsState()?.value
47+
return sharedLoaderResults.get(task)?.collectAsState()?.value
4648
}
4749

48-
fun load(
49-
webCanvas: OffscreenWebCanvas,
50-
task: LoaderTask,
51-
): StateFlow<ImageLoadResult> {
52-
val cache = caches.get(task)
53-
return cache ?: run {
54-
val imageResultState = MutableStateFlow(ImageLoadResult.Setup)
55-
val cacheItem = CacheItem(task, imageResultState)
56-
caches.save(cacheItem)
50+
inner class TaskLoader(
51+
val task: LoaderTask,
52+
val webCanvas: OffscreenWebCanvas, val result: MutableStateFlow<ImageLoadResult>,
53+
) {
54+
init {
55+
val imageResultState = this.result
5756
scope.launch {
5857
val dispose = task.hook?.let { webCanvas.setHook(task.url, it) }
5958
imageResultState.value = try {
@@ -77,21 +76,36 @@ class WebImageLoader : PureImageLoader {
7776
val failTimes = PureImageLoader.urlErrorCount.getOrPut(task.url) { 0 } + 1
7877
PureImageLoader.urlErrorCount[task.url] = failTimes
7978

80-
ImageLoadResult.error(e).also { res ->
81-
launch {
82-
/// 失败后,定时删除缓存。失败的次数越多,定时越久
83-
delay(min(failTimes * failTimes * 1000L, 30000L)) // 1 4 9 16 25 30 30 30
84-
if (cacheItem.result.value == res) {
85-
caches.delete(task, cacheItem)
86-
}
87-
}
88-
}
79+
ImageLoadResult.error(e)
8980
} finally {
9081
dispose?.invoke()
9182
}
9283
}
9384

94-
imageResultState
9585
}
9686
}
87+
88+
fun startLoad(
89+
task: LoaderTask,
90+
webCanvas: OffscreenWebCanvas,
91+
): TaskLoader {
92+
return TaskLoader(
93+
task, webCanvas,
94+
sharedLoaderResults.get(task) ?: MutableStateFlow(ImageLoadResult.Setup)
95+
).also { loader ->
96+
val cacheItem = CacheItem(task, loader.result)
97+
sharedLoaderResults.save(cacheItem)
98+
scope.launch {
99+
loader.result.collect { result ->
100+
if (result.isError) {
101+
/// 失败后,移除执行缓存。但是这里的result仍然不会变
102+
if (cacheItem.result.value == loader.result) {
103+
sharedLoaderResults.delete(task, cacheItem)
104+
}
105+
}
106+
}
107+
}
108+
}
109+
}
110+
97111
}

0 commit comments

Comments
 (0)