skydoves / landscapist

🌻 A pluggable, highly optimized Jetpack Compose and Kotlin Multiplatform image loading library that fetches and displays network images with Glide, Coil, and Fresco.
https://skydoves.github.io/landscapist/
Apache License 2.0
2.18k stars 113 forks source link

memory leak #230

Closed lulumeya closed 1 year ago

lulumeya commented 1 year ago

Please complete the following information:

Describe the Bug:

There were reports of crashes on long scrolls in the user reviews, so we checked and found a severe memory leak in the Compose function that uses the GlideImage function. We spent a whole day looking for a reproduction path, but the only condition we could find was that it would always reproduce in an Activity that was not the top-level Activity in the app. We solved the problem by replacing it with the GlideImage function provided by bumptech.

Expected Behavior:

Memory leak should not occur even in various use cases.

skydoves commented 1 year ago

Hey @lulumeya, thanks for reporting this issue. Could you elaborate on the crash details and use cases? Thank you!

skydoves commented 1 year ago

Hey, @lulumeya, I wonder if the issue is still happening in the latest snapshot. You can import the snapshot following instructions: https://github.com/skydoves/landscapist#snapshot

lulumeya commented 1 year ago

@skydoves Unfortunately, we're having trouble migrating to Kotlin 1.8, so the latest version of landscapist I was able to test was 2.1.2. The crash log was just an OOM. Since we're drawing quite a few images to the list, I was able to reproduce the memory usage easily reaching around 4GB when measured with Profiler. As for the use case, we tried testing with many different structures thus that means not the structural problem. The only overall rule we found while testing is that problem occur when the function used in an Activity not on top of the stack.

skydoves commented 1 year ago

@lulumeya Thanks for the details! Jetpack Compose projects that I simulated were mainly based on single Activity. So I guess the GlideImage Composable function doesn't clear the redundant bitmaps properly, which are not used anymore, like invisible on the screen. I just fixed some clearing bitmap behaviors #231, and let me try to reproduce them in a multi-Activities structure. Thanks!

lulumeya commented 1 year ago

@skydoves That's a pretty quick response, hopefully that resolves the issue!

skydoves commented 1 year ago

@lulumeya I just checked the 2.1.5-SNAPSHOT with multiple Activities & complicated fragment structures, and I couldn't find any memory leak issues by profiling on IDE + Leak Canary. I guess #231 works well.

Screenshot 2023-02-28 at 5 39 40 PM

Screenshot 2023-02-28 at 5 39 48 PM

I will close this issue now, and thanks for reporting an important issue!

lulumeya commented 1 year ago

@skydoves Good news. Thanks. I would check in detail of #231 soon.

jsericksk commented 1 year ago

I'm using version 2.1.5 and unfortunately I'm still having the same problem. It doesn't normally cause an OutOfMemoryError if the images are small, but large images (from the camera, for example) will eventually result in an OutOfMemoryError. Devices tested: Android 10 and Android 12 (API 31, emulator). On the emulator I didn't get an OutOfMemoryError, but there were several crashes.

I did a test using the Coil library (not landscapist) and Landscapist-Glide by simply rolling a LazyColumn (same code in both, changing only the library to load the images) and this was the result on my Android 10:

Coil: profiler-coil

Landscapist-Glide: profiler-glide

Some logs: **OOM allocating Bitmap with dimensions 4160 x 3120** [...] java.lang.OutOfMemoryError at android.graphics.Bitmap.nativeCreate(Native Method) at android.graphics.Bitmap.createBitmap(Bitmap.java:1122) at android.graphics.Bitmap.createBitmap(Bitmap.java:1080) at android.graphics.Bitmap.createBitmap(Bitmap.java:1030) at android.graphics.Bitmap.createBitmap(Bitmap.java:991) at com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool.createBitmap(LruBitmapPool.java:175) at com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool.get(LruBitmapPool.java:157) at com.bumptech.glide.load.resource.bitmap.TransformationUtils.rotateImageExif(TransformationUtils.java:329) at com.bumptech.glide.load.resource.bitmap.Downsampler.decodeFromWrappedStreams(Downsampler.java:450) at com.bumptech.glide.load.resource.bitmap.Downsampler.decode(Downsampler.java:285) at com.bumptech.glide.load.resource.bitmap.Downsampler.decode(Downsampler.java:222) at com.bumptech.glide.load.resource.bitmap.StreamBitmapDecoder.decode(StreamBitmapDecoder.java:62) at com.bumptech.glide.load.resource.bitmap.StreamBitmapDecoder.decode(StreamBitmapDecoder.java:18) at com.bumptech.glide.load.resource.bitmap.BitmapDrawableDecoder.decode(BitmapDrawableDecoder.java:58) at com.bumptech.glide.load.engine.DecodePath.decodeResourceWithList(DecodePath.java:92) at com.bumptech.glide.load.engine.DecodePath.decodeResource(DecodePath.java:70) at com.bumptech.glide.load.engine.DecodePath.decode(DecodePath.java:59) at com.bumptech.glide.load.engine.LoadPath.loadWithExceptionList(LoadPath.java:76) at com.bumptech.glide.load.engine.LoadPath.load(LoadPath.java:57) at com.bumptech.glide.load.engine.DecodeJob.runLoadPath(DecodeJob.java:539) at com.bumptech.glide.load.engine.DecodeJob.decodeFromFetcher(DecodeJob.java:503) at com.bumptech.glide.load.engine.DecodeJob.decodeFromData(DecodeJob.java:489) at com.bumptech.glide.load.engine.DecodeJob.decodeFromRetrievedData(DecodeJob.java:434) at com.bumptech.glide.load.engine.DecodeJob.onDataFetcherReady(DecodeJob.java:399) at com.bumptech.glide.load.engine.SourceGenerator.onDataReadyInternal(SourceGenerator.java:211) at com.bumptech.glide.load.engine.SourceGenerator$1.onDataReady(SourceGenerator.java:101) at com.bumptech.glide.load.model.FileLoader$FileFetcher.loadData(FileLoader.java:72) at com.bumptech.glide.load.model.stream.QMediaStoreUriLoader$QMediaStoreUriFetcher.loadData(QMediaStoreUriLoader.java:141) at com.bumptech.glide.load.engine.SourceGenerator.startNextLoad(SourceGenerator.java:95) at com.bumptech.glide.load.engine.SourceGenerator.startNext(SourceGenerator.java:88) at com.bumptech.glide.load.engine.DecodeJob.runGenerators(DecodeJob.java:311) at com.bumptech.glide.load.engine.DecodeJob.runWrapped(DecodeJob.java:280) at com.bumptech.glide.load.engine.DecodeJob.run(DecodeJob.java:235) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641) at com.bumptech.glide.load.engine.executor.GlideExecutor$DefaultThreadFactory$1.run(GlideExecutor.java:421) at java.lang.Thread.run(Thread.java:919) at com.bumptech.glide.load.engine.executor.GlideExecutor$DefaultPriorityThreadFactory$1.run(GlideExecutor.java:380)
skydoves commented 1 year ago

@jsericksk, thanks for providing the crash details! According to your crash report, this crash is not the problem of Glide or Landscapist; it's because of abusing the massive size of images, like 4160 x 3120. Coil does not fire crash because they constrain the request image sizes by force with content size resolver.

You can also resize the 'request size' of your massive size of images by adding the image option below:

      GlideImage(
        imageModel = { IMAGE },
        modifier = Modifier.size(128.dp, 128.dp)
        imageOptions = ImageOptions(
          contentScale = ContentScale.Crop,
          requestSize = IntSize(300, 200)
        )
      )

But anyway, this is a great use case that resizes the original image sizes to prevent memory leaks on the user side. Let me consider implementing the resizing options in the next release! Thank you guys for reporting this issue :)

jsericksk commented 1 year ago

This is really the problem in my case. Thanks a lot for the help and solution, @skydoves .

skydoves commented 1 year ago

@lulumeya, @jsericksk Hey folks, a new version 2.1.7 includes constraining the large image sizes by measuring composable layouts. Please check out this issue again with the new version. Thanks!

jsericksk commented 1 year ago

I just tested the new version and it's working perfectly fine. The problem I mentioned has been completely resolved. Thank you again, @skydoves.