/
Sketch.kt
418 lines (374 loc) · 17 KB
/
Sketch.kt
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
/*
* Copyright (C) 2022 panpf <panpfpanpf@outlook.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.github.panpf.sketch
import android.content.ComponentCallbacks2
import android.content.Context
import android.net.ConnectivityManager.NetworkCallback
import androidx.annotation.AnyThread
import androidx.compose.runtime.Stable
import androidx.lifecycle.Lifecycle
import com.github.panpf.sketch.cache.BitmapPool
import com.github.panpf.sketch.cache.DiskCache
import com.github.panpf.sketch.cache.MemoryCache
import com.github.panpf.sketch.cache.internal.LruBitmapPool
import com.github.panpf.sketch.cache.internal.LruDiskCache
import com.github.panpf.sketch.cache.internal.LruMemoryCache
import com.github.panpf.sketch.cache.internal.defaultMemoryCacheBytes
import com.github.panpf.sketch.decode.BitmapDecodeInterceptor
import com.github.panpf.sketch.decode.BitmapDecoder
import com.github.panpf.sketch.decode.DrawableDecodeInterceptor
import com.github.panpf.sketch.decode.DrawableDecoder
import com.github.panpf.sketch.decode.internal.BitmapResultCacheDecodeInterceptor
import com.github.panpf.sketch.decode.internal.DefaultBitmapDecoder
import com.github.panpf.sketch.decode.internal.DefaultDrawableDecoder
import com.github.panpf.sketch.decode.internal.DrawableBitmapDecoder
import com.github.panpf.sketch.decode.internal.EngineBitmapDecodeInterceptor
import com.github.panpf.sketch.decode.internal.EngineDrawableDecodeInterceptor
import com.github.panpf.sketch.fetch.AssetUriFetcher
import com.github.panpf.sketch.fetch.Base64UriFetcher
import com.github.panpf.sketch.fetch.ContentUriFetcher
import com.github.panpf.sketch.fetch.Fetcher
import com.github.panpf.sketch.fetch.FileUriFetcher
import com.github.panpf.sketch.fetch.HttpUriFetcher
import com.github.panpf.sketch.fetch.ResourceUriFetcher
import com.github.panpf.sketch.http.HttpStack
import com.github.panpf.sketch.http.HurlStack
import com.github.panpf.sketch.request.DisplayRequest
import com.github.panpf.sketch.request.DisplayResult
import com.github.panpf.sketch.request.Disposable
import com.github.panpf.sketch.request.DownloadRequest
import com.github.panpf.sketch.request.DownloadResult
import com.github.panpf.sketch.request.ImageOptions
import com.github.panpf.sketch.request.ImageRequest
import com.github.panpf.sketch.request.LoadRequest
import com.github.panpf.sketch.request.LoadResult
import com.github.panpf.sketch.request.OneShotDisposable
import com.github.panpf.sketch.request.RequestInterceptor
import com.github.panpf.sketch.request.internal.EngineRequestInterceptor
import com.github.panpf.sketch.request.internal.MemoryCacheRequestInterceptor
import com.github.panpf.sketch.request.internal.RequestExecutor
import com.github.panpf.sketch.request.internal.requestManager
import com.github.panpf.sketch.target.ViewDisplayTarget
import com.github.panpf.sketch.transform.internal.BitmapTransformationDecodeInterceptor
import com.github.panpf.sketch.util.Logger
import com.github.panpf.sketch.util.SystemCallbacks
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.coroutineScope
import java.lang.ref.WeakReference
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.math.roundToLong
/**
* A service class that performs an [ImageRequest] to load an image.
*
* Sketch is responsible for handling data acquisition, image decoding, image conversion,
* processing cache, request management, memory management and other functions.
*
* You just need to create an instance of the ImageRequest subclass and pass it to the [enqueue] or [execute] method for execution.
*
* Sketch is designed to be sharable and works best when the same instance is used throughout the
* application via the built-in extension function `Context.sketch`
*/
@Stable
class Sketch private constructor(
_context: Context,
_logger: Logger?,
_memoryCache: MemoryCache?,
_downloadCache: DiskCache?,
_resultCache: DiskCache?,
_bitmapPool: BitmapPool?,
_componentRegistry: ComponentRegistry?,
_httpStack: HttpStack?,
_globalImageOptions: ImageOptions?,
) {
private val scope = CoroutineScope(
SupervisorJob() + Dispatchers.Main.immediate + CoroutineExceptionHandler { _, throwable ->
logger.e("scope", throwable, "exception")
}
)
private val requestExecutor = RequestExecutor()
private val isShutdown = AtomicBoolean(false)
/** Application Context */
val context: Context = _context.applicationContext
/** Output log */
val logger: Logger = _logger ?: Logger()
/** Memory cache of previously loaded images */
val memoryCache: MemoryCache
/** Reuse Bitmap */
val bitmapPool: BitmapPool
/** Disk caching of http downloads images */
val downloadCache: DiskCache =
_downloadCache ?: LruDiskCache.ForDownloadBuilder(context).build()
/** Disk caching of transformed images */
val resultCache: DiskCache =
_resultCache ?: LruDiskCache.ForResultBuilder(context).build()
/** Execute HTTP request */
val httpStack: HttpStack = _httpStack ?: HurlStack.Builder().build()
/** Fill unset [ImageRequest] value */
val globalImageOptions: ImageOptions? = _globalImageOptions
/** Register components that are required to perform [ImageRequest] and can be extended,
* such as [Fetcher], [BitmapDecoder], [DrawableDecoder], [RequestInterceptor], [BitmapDecodeInterceptor], [DrawableDecodeInterceptor] */
val components: Components
/** Proxies [ComponentCallbacks2] and [NetworkCallback]. Clear memory cache when system memory is low, and monitor network connection status */
val systemCallbacks = SystemCallbacks(context, WeakReference(this))
/* Limit the number of concurrent network tasks, too many network tasks will cause network congestion */
@OptIn(ExperimentalCoroutinesApi::class)
val networkTaskDispatcher: CoroutineDispatcher = Dispatchers.IO.limitedParallelism(10)
/* Limit the number of concurrent decoding tasks because too many concurrent BitmapFactory tasks can affect UI performance */
@OptIn(ExperimentalCoroutinesApi::class)
val decodeTaskDispatcher: CoroutineDispatcher = Dispatchers.IO.limitedParallelism(4)
init {
val defaultMemoryCacheBytes = context.defaultMemoryCacheBytes()
memoryCache = _memoryCache
?: LruMemoryCache((defaultMemoryCacheBytes * 0.66f).roundToLong())
bitmapPool = _bitmapPool
?: LruBitmapPool((defaultMemoryCacheBytes * 0.33f).roundToLong())
memoryCache.logger = logger
bitmapPool.logger = logger
downloadCache.logger = logger
resultCache.logger = logger
val componentRegistry =
(_componentRegistry?.newBuilder() ?: ComponentRegistry.Builder()).apply {
addFetcher(HttpUriFetcher.Factory())
addFetcher(FileUriFetcher.Factory())
addFetcher(ContentUriFetcher.Factory())
addFetcher(ResourceUriFetcher.Factory())
addFetcher(AssetUriFetcher.Factory())
addFetcher(Base64UriFetcher.Factory())
addBitmapDecoder(DrawableBitmapDecoder.Factory())
addBitmapDecoder(DefaultBitmapDecoder.Factory())
addDrawableDecoder(DefaultDrawableDecoder.Factory())
addRequestInterceptor(MemoryCacheRequestInterceptor())
addRequestInterceptor(EngineRequestInterceptor())
addBitmapDecodeInterceptor(BitmapResultCacheDecodeInterceptor())
addBitmapDecodeInterceptor(BitmapTransformationDecodeInterceptor())
addBitmapDecodeInterceptor(EngineBitmapDecodeInterceptor())
addDrawableDecodeInterceptor(EngineDrawableDecodeInterceptor())
}.build()
components = Components(this, componentRegistry)
logger.d("Configuration") {
buildString {
append("\n").append("logger: $logger")
append("\n").append("httpStack: $httpStack")
append("\n").append("memoryCache: $memoryCache")
append("\n").append("bitmapPool: $bitmapPool")
append("\n").append("downloadCache: $downloadCache")
append("\n").append("resultCache: $resultCache")
append("\n").append("fetchers: ${componentRegistry.fetcherFactoryList}")
append("\n").append("bitmapDecoders: ${componentRegistry.bitmapDecoderFactoryList}")
append("\n").append("drawableDecoders: ${componentRegistry.drawableDecoderFactoryList}")
append("\n").append("requestInterceptors: ${componentRegistry.requestInterceptorList}")
append("\n").append("bitmapDecodeInterceptors: ${componentRegistry.bitmapDecodeInterceptorList}")
append("\n").append("drawableDecodeInterceptors: ${componentRegistry.drawableDecodeInterceptorList}")
}
}
}
/**
* Execute the DisplayRequest asynchronously.
*
* Note: The request will not start executing until Lifecycle state is STARTED
* reaches [Lifecycle.State.STARTED] state and [ViewDisplayTarget.view] is attached to window
*
* @return A [Disposable] which can be used to cancel or check the status of the request.
*/
@AnyThread
fun enqueue(request: DisplayRequest): Disposable<DisplayResult> {
val job = scope.async {
requestExecutor.execute(this@Sketch, request, enqueue = true) as DisplayResult
}
val target = request.target
return if (target is ViewDisplayTarget<*>) {
target.view?.requestManager?.getDisposable(job) ?: OneShotDisposable(job)
} else {
OneShotDisposable(job)
}
}
/**
* Execute the DisplayRequest synchronously in the current coroutine scope.
*
* Note: The request will not start executing until Lifecycle state is STARTED
* reaches [Lifecycle.State.STARTED] state and [ViewDisplayTarget.view] is attached to window
*
* @return A [DisplayResult.Success] if the request completes successfully. Else, returns an [DisplayResult.Error].
*/
suspend fun execute(request: DisplayRequest): DisplayResult =
coroutineScope {
val job = async(Dispatchers.Main.immediate) {
requestExecutor.execute(this@Sketch, request, enqueue = false) as DisplayResult
}
// Update the current request attached to the view and await the result.
val target = request.target
if (target is ViewDisplayTarget<*>) {
target.view?.requestManager?.getDisposable(job)
}
job.await()
}
/**
* Execute the LoadRequest asynchronously.
*
* Note: The request will not start executing until Lifecycle state is STARTED
* reaches [Lifecycle.State.STARTED] state and [ViewDisplayTarget.view] is attached to window
*
* @return A [Disposable] which can be used to cancel or check the status of the request.
*/
@AnyThread
fun enqueue(request: LoadRequest): Disposable<LoadResult> {
val job = scope.async {
requestExecutor.execute(this@Sketch, request, enqueue = true) as LoadResult
}
return OneShotDisposable(job)
}
/**
* Execute the LoadRequest synchronously in the current coroutine scope.
*
* Note: The request will not start executing until Lifecycle state is STARTED
* reaches [Lifecycle.State.STARTED] state and [ViewDisplayTarget.view] is attached to window
*
* @return A [LoadResult.Success] if the request completes successfully. Else, returns an [LoadResult.Error].
*/
suspend fun execute(request: LoadRequest): LoadResult = coroutineScope {
val job = async(Dispatchers.Main.immediate) {
requestExecutor.execute(this@Sketch, request, enqueue = false) as LoadResult
}
job.await()
}
/**
* Execute the DownloadRequest asynchronously.
*
* Note: The request will not start executing until Lifecycle state is STARTED
* reaches [Lifecycle.State.STARTED] state and [ViewDisplayTarget.view] is attached to window
*
* @return A [Disposable] which can be used to cancel or check the status of the request.
*/
@AnyThread
fun enqueue(request: DownloadRequest): Disposable<DownloadResult> {
val job = scope.async {
requestExecutor.execute(this@Sketch, request, enqueue = true) as DownloadResult
}
return OneShotDisposable(job)
}
/**
* Execute the DownloadRequest synchronously in the current coroutine scope.
*
* Note: The request will not start executing until Lifecycle state is STARTED
* reaches [Lifecycle.State.STARTED] state and [ViewDisplayTarget.view] is attached to window
*
* @return A [DownloadResult.Success] if the request completes successfully. Else, returns an [DownloadResult.Error].
*/
suspend fun execute(request: DownloadRequest): DownloadResult =
coroutineScope {
val job = async(Dispatchers.Main.immediate) {
requestExecutor.execute(this@Sketch, request, enqueue = false) as DownloadResult
}
job.await()
}
/**
* Cancel any new and in progress requests, clear the [MemoryCache] and [BitmapPool], and close any open
* system resources.
*
* Shutting down an image loader is optional.
*/
fun shutdown() {
if (isShutdown.getAndSet(true)) return
scope.cancel()
systemCallbacks.shutdown()
memoryCache.clear()
downloadCache.close()
resultCache.close()
bitmapPool.clear()
}
class Builder constructor(context: Context) {
private val appContext: Context = context.applicationContext
private var logger: Logger? = null
private var memoryCache: MemoryCache? = null
private var downloadCache: DiskCache? = null
private var resultCache: DiskCache? = null
private var bitmapPool: BitmapPool? = null
private var componentRegistry: ComponentRegistry? = null
private var httpStack: HttpStack? = null
private var globalImageOptions: ImageOptions? = null
/**
* Set the [Logger] to write logs to.
*/
fun logger(logger: Logger?): Builder = apply {
this.logger = logger
}
/**
* Set the [MemoryCache]
*/
fun memoryCache(memoryCache: MemoryCache?): Builder = apply {
this.memoryCache = memoryCache
}
/**
* Set the [DiskCache] for download cache
*/
fun downloadCache(diskCache: DiskCache?): Builder = apply {
this.downloadCache = diskCache
}
/**
* Set the [DiskCache] for result cache
*/
fun resultCache(diskCache: DiskCache?): Builder = apply {
this.resultCache = diskCache
}
/**
* Set the [BitmapPool]
*/
fun bitmapPool(bitmapPool: BitmapPool?): Builder = apply {
this.bitmapPool = bitmapPool
}
/**
* Set the [ComponentRegistry]
*/
fun components(components: ComponentRegistry?): Builder = apply {
this.componentRegistry = components
}
/**
* Build and set the [ComponentRegistry]
*/
fun components(configBlock: (ComponentRegistry.Builder.() -> Unit)): Builder =
components(ComponentRegistry.Builder().apply(configBlock).build())
/**
* Set the [HttpStack] used for network requests.
*/
fun httpStack(httpStack: HttpStack?): Builder = apply {
this.httpStack = httpStack
}
/**
* Set an [ImageOptions], fill unset [ImageRequest] value
*/
fun globalImageOptions(globalImageOptions: ImageOptions?): Builder = apply {
this.globalImageOptions = globalImageOptions
}
fun build(): Sketch = Sketch(
_context = appContext,
_logger = logger,
_memoryCache = memoryCache,
_downloadCache = downloadCache,
_resultCache = resultCache,
_bitmapPool = bitmapPool,
_componentRegistry = componentRegistry,
_httpStack = httpStack,
_globalImageOptions = globalImageOptions,
)
}
}