coil-kt / coil

Image loading for Android and Compose Multiplatform.
https://coil-kt.github.io/coil/
Apache License 2.0
10.84k stars 665 forks source link

File cache key creation performs IO read on main thread #1878

Closed pavelreiter closed 1 year ago

pavelreiter commented 1 year ago

Describe the bug When default ImageLoader is preparing cache keys for File models, it will perform disk IO read on main thread. This access can be reported as policy violation by StrictMode. Known workaround is to set interceptorDispatcher.

To Reproduce Supply a file Uri as AsyncImage model. Sample project: https://github.com/pavelreiter/bugsample-coil-main-thread-access (deleted)

Logs/Screenshots

StrictMode policy violation; ~duration=6 ms: android.os.strictmode.DiskReadViolation
                    at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:1658)
                    at java.io.UnixFileSystem.getLastModifiedTime(UnixFileSystem.java:321)
                    at java.io.File.lastModified(File.java:937)
                    at coil.key.FileKeyer.key(FileKeyer.kt:10)
                    at coil.key.FileKeyer.key(FileKeyer.kt:6)
                    at coil.ComponentRegistry.key(ComponentRegistry.kt:54)
                    at coil.memory.MemoryCacheService.newCacheKey(MemoryCacheService.kt:48)
                    at coil.intercept.EngineInterceptor.intercept(EngineInterceptor.kt:64)
                    at coil.intercept.RealInterceptorChain.proceed(RealInterceptorChain.kt:25)
                    at coil.RealImageLoader$executeMain$result$1.invokeSuspend(RealImageLoader.kt:191)
                    at coil.RealImageLoader$executeMain$result$1.invoke(Unknown Source:8)
                    at coil.RealImageLoader$executeMain$result$1.invoke(Unknown Source:4)
                    at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:78)
                    at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:167)
                    at kotlinx.coroutines.BuildersKt.withContext(Unknown Source:1)
                    at coil.RealImageLoader.executeMain(RealImageLoader.kt:182)
                    at coil.RealImageLoader.access$executeMain(RealImageLoader.kt:65)
                    at coil.RealImageLoader$executeMain$1.invokeSuspend(Unknown Source:16)
                    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
                    at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:235)
                    at kotlinx.coroutines.DispatchedTaskKt.resumeUnconfined(DispatchedTask.kt:191)
                    at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:163)
                    at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:474)
                    at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:508)
                    at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:497)
                    at kotlinx.coroutines.CancellableContinuationImpl.resumeWith(CancellableContinuationImpl.kt:368)
                    at kotlinx.coroutines.flow.StateFlowSlot.makePending(StateFlow.kt:284)
                    at kotlinx.coroutines.flow.StateFlowImpl.updateState(StateFlow.kt:349)
                    at kotlinx.coroutines.flow.StateFlowImpl.setValue(StateFlow.kt:316)
                    at coil.compose.ConstraintsSizeResolver.measure-3p2s80s(AsyncImage.kt:209)
                    at androidx.compose.ui.node.BackwardsCompatNode.measure-3p2s80s(BackwardsCompatNode.kt:311)
                    at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0(LayoutModifierNodeCoordinator.kt:116)
                    at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasure$2.invoke(LayoutNodeLayoutDelegate.kt:1499)
                    at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasure$2.invoke(LayoutNodeLayoutDelegate.kt:1495)
                    at androidx.compose.runtime.snapshots.Snapshot$Companion.observe(Snapshot.kt:2299)
                    at androidx.compose.runtime.snapshots.SnapshotStateObserver$ObservedScopeMap.observe(SnapshotStateObserver.kt:467)
                    at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:230)
                    at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui_release(OwnerSnapshotObserver.kt:133)
                    at androidx.compose.ui.node.OwnerSnapshotObserver.observeMeasureSnapshotReads$ui_release(OwnerSnapshotObserver.kt:113)
                    at androidx.compose.ui.node.LayoutNodeLayoutDelegate.performMeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:1495)
                    at androidx.compose.ui.node.LayoutNodeLayoutDelegate.access$performMeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:35)
                    at androidx.compose.ui.node.LayoutNodeLayoutDelegate$MeasurePassDelegate.remeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:560)
                    at androidx.compose.ui.node.LayoutNodeLayoutDelegate$MeasurePassDelegate.measure-BRTryo0(LayoutNodeLayoutDelegate.kt:539)
                    at androidx.compose.foundation.layout.BoxKt$boxMeasurePolicy$1.measure-3p2s80s(Box.kt:114)
                    at androidx.compose.ui.node.InnerNodeCoordinator.measure-BRTryo0(InnerNodeCoordinator.kt:126)
                    at androidx.compose.ui.graphics.SimpleGraphicsLayerModifier.measure-3p2s80s(GraphicsLayerModifier.kt:646)
                    at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0(LayoutModifierNodeCoordinator.kt:116)
                    at androidx.compose.foundation.layout.FillNode.measure-3p2s80s(Size.kt:698)
                    at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0(LayoutModifierNodeCoordinator.kt:116)
                    at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasure$2.invoke(LayoutNodeLayoutDelegate.kt:1499)
                    at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasure$2.invoke(LayoutNodeLayoutDelegate.kt:1495)
                    at androidx.compose.runtime.snapshots.Snapshot$Companion.observe(Snapshot.kt:2299)
                    at androidx.compose.runtime.snapshots.SnapshotStateObserver$ObservedScopeMap.observe(SnapshotStateObserver.kt:467)
                    at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:230)
                    at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui_release(OwnerSnapshotObserver.kt:133)
                    at androidx.compose.ui.node.OwnerSnapshotObserver.observeMeasureSnapshotReads$ui_release(OwnerSnapshotObserver.kt:113)
                    at androidx.compose.ui.node.LayoutNodeLayoutDelegate.performMeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:1495)
                    at androidx.compose.ui.node.LayoutNodeLayoutDelegate.access$performMeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:35)
                    at androidx.compose.ui.node.LayoutNodeLayoutDelegate$MeasurePassDelegate.remeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:560)
                    at androidx.compose.ui.node.LayoutNode.remeasure-_Sx5XlM$ui_release(LayoutNode.kt:1140)
                    at androidx.compose.ui.node.LayoutNode.remeasure-_Sx5XlM$ui_release$default(LayoutNode.kt:1131)
                    at androidx.compose.ui.node.MeasureAndLayoutDelegate.doRemeasure-sdFAvZA(MeasureAndLayoutDelegate.kt:323)
                    at androidx.compose.ui.node.MeasureAndLayoutDelegate.remeasureOnly(MeasureAndLayoutDelegate.kt:503)
                    at androidx.compose.ui.node.MeasureAndLayoutDelegate.recurseRemeasure(MeasureAndLayoutDelegate.kt:371)
                    at androidx.compose.ui.node.MeasureAndLayoutDelegate.recurseRemeasure(MeasureAndLayoutDelegate.kt:375)
                    at androidx.compose.ui.node.MeasureAndLayoutDelegate.measureOnly(MeasureAndLayoutDelegate.kt:362)
                    at androidx.compose.ui.platform.AndroidComposeView.onMeasure(AndroidComposeView.android.kt:966)
                    at android.view.View.measure(View.java:26357)
                    at androidx.compose.ui.platform.AbstractComposeView.internalOnMeasure$ui_release(ComposeView.android.kt:302)
                    at androidx.compose.ui.platform.AbstractComposeView.onMeasure(ComposeView.android.kt:289)
                    at android.view.View.measure(View.java:26357)
                    at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6981)
                    at android.widget.FrameLayout.onMeasure(FrameLayout.java:194)
                    at android.view.View.measure(View.java:26357)
                    at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6981)
                    at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1608)
                    at android.widget.LinearLayout.measureVertical(LinearLayout.java:878)
                    at android.widget.LinearLayout.onMeasure(LinearLayout.java:721)
                    at android.view.View.measure(View.java:26357)
                    at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6981)
                    at android.widget.FrameLayout.onMeasure(FrameLayout.java:194)
                    at com.android.internal.policy.DecorView.onMeasure(DecorView.java:760)
                    at android.view.View.measure(View.java:26357)
                    at android.view.ViewRootImpl.performMeasure(ViewRootImpl.java:3926)
                    at android.view.ViewRootImpl.measureHierarchy(ViewRootImpl.java:2612)
                    at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2884)
                    at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:2328)
                    at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:9087)
                    at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1231)
                    at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1239)
                    at android.view.Choreographer.doCallbacks(Choreographer.java:899)
                    at android.view.Choreographer.doFrame(Choreographer.java:832)
                    at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1214)
                    at android.os.Handler.handleCallback(Handler.java:942)
                    at android.os.Handler.dispatchMessage(Handler.java:99)
                    at android.os.Looper.loopOnce(Looper.java:201)
                    at android.os.Looper.loop(Looper.java:288)
                    at android.app.ActivityThread.main(ActivityThread.java:7872)
                    at java.lang.reflect.Method.invoke(Native Method)
                    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
                    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)

Version Library version: 2.4.0 APIs: probably all, but tested on 33 and 30

colinrtwhite commented 1 year ago

Thanks for the report, but this is working as intended. We can't change the default behaviour as it would break existing users, but it's possible to disable this behaviour using addLastModifiedToFileCacheKey.

pavelreiter commented 1 year ago

I need the cache updated since the affected file can be rewritten, so I will go with changing dispatcher for the interceptor. Thanks for swift answer.

colinrtwhite commented 1 year ago

Sounds good, the only downside to changing the interceptorDispatcher is the memory cache check will be performed asynchronously as well since computing the memory cache key requires the file timestamp.