Baseflow / flutter_cached_network_image

Download, cache and show images in a flutter app
https://baseflow.com
2.43k stars 652 forks source link

perf: memory Memory Overflow Issue due to CachedNetworkImage #429

Open pranavkpr1 opened 4 years ago

pranavkpr1 commented 4 years ago

Hi, Allez gezellig! Thanks for creating this plugin. I was really concerned about my app crashes and then I did memory profiling on the app. And I am attaching the screenshots of heap dumps here. I have used Cached Network Image in my entire project and have not used Network. Image or any other plugin. As you can see that the images/ Cached Network Image is responsible for app crashes and out of memory errors. I have also read the similar issues here in issues section for the plugin. And now I am reading even Network.Image or ImagePicker causes the same kind of behavior if used without cacheWidth/width and cacheHeight. I think flutter team should update their official docs for these plugins/libraries so that unexpected behavior is not triggered causing the production app to crash if developers used these plugins without any recommendations to reduce memory consumption in the heap. It's also important to mention in Readme for this plugin to use all recommended measures such as memWidth, memHeight or use the 2.3.0 beta version so that future developers can use this plugin effectively to add multiple images in the app. It would be also helpful if you can elaborate on all these recommended measures here or update the Readme so that it can help the future developers. I also tried using 2.3.0-beta.1 in android app because that't the latest version on stable flutter channel. I also tried adding memWidth and memHeight parameters to CachedNetworkImage. But to no avail. That's why need some recommendations from the contributors. I saw the code for CachedNetwork image too and it's using default cache manager from flutter cache manager package if not provided with cache manager as parameter. The only thing that now I could try is to limit the maximum number of cache objects for Cache Manager which is passed as parameter to the Cached Network Image. So my question to the team are as follows:

  1. What's the maximum number of cache objects that default cache manager can hold because in my code, I have seen it increasing to 600-700 object levels in heap dump which would definitely crash the app frequently. While looking at the code for default cache manager in this link https://github.com/Baseflow/flutter_cache_manager/blob/develop/flutter_cache_manager/lib/src/cache_manager.dart I observed that max duration is set to 30 days and max number of objects is set at 200 for Default Cache Manager. As I am using Cached network image in number of places in my code so that's why it might be going to 600-700 level. Is it recommend to use Cache Manager as global variable in the code and then pass that global Cache Manager as parameter to different instances of Cached Network Image?
  2. And what should be the ideal number of cache objects we should limit the cache manager to, if that global cache manager is passed as parameter to Cached Network Image.
    Looking forward to the reply. Dank je wel!

snapshot1 snapshot2 snapshot3 snapshot7 snapshot8 snapshot9 snapshot10 snapshot11

pranavkpr1 commented 4 years ago

Important update: I also tried using global cache manager with maxNumberOfObjects limited to some number which could be stored in cache. Then I shared this BaseCacheManager instance as parameter to all instances of CachedNetworkImage in my code as shown as follows:

class ImageCacheManager extends BaseCacheManager { static const key = "MyApp_Cache"; //just use some unique name instead of MyApp static ImageCacheManager _instance; factory ImageCacheManager() { if (_instance == null) { instance = new ImageCacheManager.(); } return instance; } ImageCacheManager.() : super(key, maxAgeCacheObject: Duration(days: 1), maxNrOfCacheObjects:20 ); Future getFilePath() async { var directory = await getTemporaryDirectory(); return path.join(directory.path, key);

} } ImageCacheManager ImageCacheStorage=new ImageCacheManager();

CachedNetworkImage( imageUrl: "", placeholder: (context, url) => Image.memory(kTransparentImage), errorWidget: (context, url, error) => Image.memory(kTransparentImage), fadeInCurve: Curves.easeIn, memCacheWidth: 500, memCacheHeight:500, cacheManager: ImageCacheStorage, ) But still the memory used in the heap is increasing to 500MB-600MB which doesn't make any sense to me if I am limiting the maxNumber of objects stored in cache to 20.

Then I tried using Image.network with cacheWidth and cacheHeight parameters and setting imageCache size to 50MB as follows: imageCache.maximumSizeBytes=50000000; Network Image is behaving in correct way and heap size doesn't increase beyond 70-80 MB.

For now, I would prefer to use NetworkImage to avoid app crashes even if CachedNetworkImage has positive points such as errorWidget and placeholderWidget. I hope that the contributors could comment on the issue for how to use the CachedNetworkImage effectively in case of large number of images in the app without increasing the heap size to a large amount so that it can help me and other future developers to take the right decision :)

pushuhengyang commented 4 years ago

@pranavkpr1 _imageCache.clearLiveImages(); Can reduce a lot of memory

截屏2020-08-08 下午4 22 39
kushalmahapatro commented 4 years ago

@pranavkpr1 _imageCache.clearLiveImages(); Can reduce a lot of memory

截屏2020-08-08 下午4 22 39

Hi @pushuhengyang , Thanks for the solution, but can you please help us where to use this code. We are also using cachednetwork image and facing the OOM in iOS (Low-end devices) mostly.

clragon commented 4 years ago

I am also experiencing extreme memory issues.

I prefer this library because it has fade in animations and provides good functionality, but it can easily bring my app to use over 1GB of RAM and beyond.

I have experienced the app using so much RAM that the OS starts killing off all background apps to eventually kill off my app in a last attempt to not die, because images are not freed. External memory just goes on to consume all available RAM.

My app allows accessing large images up to 8k etc and if the user loads a significant amount of them, this will lead to the app getting killed by the OS or like others mentioned, just crashing.

I understand that keeping images in RAM is important so they dont have to be loaded again and again and this is also very useful in the case of my app, but that there is literally no limit to RAM consumption is rather detrimental.

This is a rather big issue for me right now, and I would love it if there was a fix.

pushuhengyang commented 3 years ago

@pranavkpr1 _imageCache.clearLiveImages(); Can reduce a lot of memory

截屏2020-08-08 下午4 22 39

Hi @pushuhengyang , Thanks for the solution, but can you please help us where to use this code. We are also using cachednetwork image and facing the OOM in iOS (Low-end devices) mostly.

Judge every time you load the image

截屏2020-08-24 下午7 14 18
vanlooverenkoen commented 3 years ago

@pushuhengyang and where would you call _checkMemory()? Can you provide use with a minimal working example?

pushuhengyang commented 3 years ago

@pushuhengyang and where would you call _checkMemory()? Can you provide use with a minimal working example?

截屏2020-08-27 下午6 03 19

It's best to encapsulate a widget

renefloor commented 3 years ago

Hi all. In version 2.3.1 there have been some improvements in memory usage. Next to that you can set the height and/or width of the image in memory using memCacheWidth/memCacheHeight. Setting these to a reasonable size should help a large part of the memory issues. https://pub.dev/documentation/cached_network_image/latest/cached_network_image/CachedNetworkImage-class.html

pranavkpr1 commented 3 years ago

Hi Rene, As I mentioned in this post, I have already tried using dev version of CachedNetworkImage which is the new version you are asking to try. I have already tried adding memCacheWidth/ memCacheHeight as mentioned in the original post but still I was facing the same memory issue when I was performing memory profiling as shown in the attached screenshots in the original post. According to my experience, we can't use this package in e-commerce or social media kind of app where there is use case of showing large number of images as it can trigger crashes due to OOM issue. Hope to hear from you soon.

renefloor commented 3 years ago

Sorry didn't read it good enough. I have to take some time to dig into this.

sawankumarbundelkhandi commented 3 years ago

I am also facing the same issue. On the simulator, it just works and on the real iPhone, it crashes.

For the time being, I switched to https://github.com/humblerookie/optimized_cached_image which is very similar to flutter_cached_network_image and in fact, internally uses octo_image.

One more thing I noticed is when providing memCacheWidth/memCacheHeight in flutter_cached_network_image it stops crashing but flutter's ResizeImage cacheWidth/cacheHeight don't care about the image fit we mention for the CachedNetworkImage

Source https://github.com/flutter/flutter/issues/52802 https://github.com/flutter/flutter/pull/64352

vanlooverenkoen commented 3 years ago

@pushuhengyang and where would you call _checkMemory()? Can you provide use with a minimal working example?

截屏2020-08-27 下午6 03 19

It's best to encapsulate a widget

This does not work for me. I execute this code on every build of a CachedNetworkImage

  print(_imageCache.liveImageCount);
  print(_imageCache.currentSize);
  print(_imageCache.currentSizeBytes);

This results in

flutter: 1
flutter: 1
flutter: 3616960

But my app still crashes. I wil now start with profiling to check why this is happening.

vanlooverenkoen commented 3 years ago

When profiling I see a very big spike in memory usage. I have the same problem with Image.network:

https://github.com/flutter/flutter/issues/47378#issuecomment-692060368

vanlooverenkoen commented 3 years ago

This is our workaround for the moment. We wrote our own widget that downscales and caches the downscaled image to the cache

for this we will need the devicePixelRatio but that is coming from MediaQuery.of so you will need to fetch that value before you are using this widget. (We save this value in our config when MaterialApp is build. so we can always access this because the devicePixelRatio is will not change. )

import 'dart:typed_data'; import 'dart:ui';

import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:pedantic/pedantic.dart'; import 'package:flutter/material.dart';

class MyProjectBetterImageWidget extends StatefulWidget { final String imageUrl; final BoxFit fit; final double width; final double height; final Widget placeholder;

const MyProjectBetterImageWidget({ @required this.imageUrl, @required this.fit, @required this.width, @required this.height, @required this.placeholder, Key key, }) : super(key: key);

@override _MyProjectBetterImageWidgetState createState() => _MyProjectBetterImageWidgetState(); }

class _MyProjectBetterImageWidgetState extends State { var _isLoading = false; var _hasError = false;

Uint8List _image;

Uint8List get image => _image;

bool get showPlaceholder => _hasError || _isLoading || _image == null;

@override void initState() { super.initState(); _getImage(); }

@override void didUpdateWidget(MyProjectBetterImageWidget oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.imageUrl != widget.imageUrl) { _getImage(); } }

Future _getImage() async { final originalUrl = widget.imageUrl; final widgetWidth = widget.width; final widgetHeight = widget.height; if (originalUrl == null || originalUrl.endsWith('unknown.jpg')) { return; } if (widgetWidth == double.infinity || widgetHeight == double.infinity || widgetWidth == null || widgetHeight == null) { MyProjectLogger.logDebug('IMAGE-ERROR: $originalUrl ($widgetWidth/$widgetHeight'); setState(() => _hasError = true); return; } try { setState(() { _isLoading = true; _hasError = false; });

  final width = (widgetWidth * FlavorConfig.instance.devicePixelRatio).toInt();
  final height = (widgetHeight * FlavorConfig.instance.devicePixelRatio).toInt();
  final url = '$originalUrl?w=$width&h=$height';
  var fileInfo = await DefaultCacheManager().getFileFromCache(url);
  var fromCache = true;
  if (!mounted) return;
  if (fileInfo == null) {
    fromCache = false;
    fileInfo = await DefaultCacheManager().downloadFile(url);
  }
  // ignore: invariant_booleans
  if (!mounted) return;
  if (fromCache) {
    _image = await fileInfo.file.readAsBytes();
    setState(() {
      _isLoading = false;
      _hasError = false;
    });
    return;
  }

  final bytes = await fileInfo.file.readAsBytes();
  final codec = await instantiateImageCodec(
    bytes,
    targetWidth: width >= height ? width : null,
    targetHeight: height > width ? height : null,
  );
  final frame = await codec.getNextFrame();
  final data = await frame.image.toByteData(format: ImageByteFormat.png);
  _image = data.buffer.asUint8List();
  if (!fromCache) {
    unawaited(_cacheImage(url));
  }
} catch (e) {
  MyProjectLogger.logError(message: 'Failed to parse image: $originalUrl', error: e);
  _hasError = true;
} finally {
  _isLoading = false;
}
// ignore: invariant_booleans
if (mounted) {
  setState(() {});
}

}

Future _cacheImage(String url) async { try { await DefaultCacheManager().putFile(url, _image, fileExtension: 'png'); } catch (e) { MyProjectLogger.logError(message: 'Failed to cache image: $url', error: e); } }

@override Widget build(BuildContext context) { if (showPlaceholder) return widget.placeholder; return Image.memory( image, width: widget.width, height: widget.height, fit: widget.fit, ); } }

dgilperez commented 3 years ago

Thanks for the workaround, @vanlooverenkoen! Are you using that in production now? No memory errors? I was trying to adapt it to my project and I have a couple of questions.

Questions:

Also, do you think this widget is relevant enough to move this to its own repo, so we don't divert the conversation from the main issue about flutter_cached_network_image?

winterdl commented 3 years ago

I've got the same issue, the app loading about 100s images then exit suddenly (randomly, sometimes it crashes on about load 20-30 images,sometimes 200+ images), I read the log, the message about not ensure folder exists /mnt/shell/emulated/0/Android/data/..../caches and /mnt/shell/emulated/0/Android/data/..../files

I've added read, write externalcard permission, and the app not crash and exit suddenly.

If I didnt add the permission, why the app still run and crash randomly, why the lib not require permission at the first run.

updated: my app still randomly crash

RaashVision commented 3 years ago

@renefloor adding the memCacheWidth/memCacheHeight do reduce the memory hike as I check in Memory dart tools. But setting this value causing the image to be blur. Or we can say it pixelate. Is there any solution for it?

renefloor commented 3 years ago

@RaashVision at what value did you set it? If you set it really small I can imaging that it becomes pixelated. It is better to take other measures, for example that you don't show more than 10 images at the same time. One way is for example by using ListView.builder: https://api.flutter.dev/flutter/widgets/ListView/ListView.builder.html

halkportal970 commented 3 years ago

I get the same problem too. I have a listview builder and hundreds of png images on it. But when app try to load listview with images and scroll on listview, memory goes up to 2GB. App starts 300MB of memory usage but when app try to load images, memory starts to increase, and on iphone 11 pro kills the app around 2gb of RAM usage, iphone 6 kills the app around 650 Mb of RAM usage. I tried various methods but somehow couldn't solve the memory problem.

CachedNetworkImage( imageUrl: image_url, placeholder: (context, url) => CircularProgressIndicator(), errorWidget: (context, url, error) => Image.asset(noImage), ),

AdnanKazi commented 3 years ago

@halkportal970 Did you find any solution my case is the same as yours

halkportal970 commented 3 years ago

@AdnanKazi Yes I found a solution. I added memCacheWidth to all CachedNetworkImage widgets. This realy reduced memory usage. Because my images width and height properties are very big. But this is not a very good solution. In web service side we must decrease images width and height according to mobile.

CachedNetworkImage(
                                    imageUrl: image_url,
                                    memCacheWidth: (Get.width * 0.6).toInt(),
                                    placeholder: (context, url) =>
                                        CircularProgressIndicator(),
                                    errorWidget: (context, url, error) =>
                                        Image.asset(noImage),
                                  ),
AdnanKazi commented 3 years ago

@halkportal970 Thank you so much for your advice in the meantime I have also found the solution which doesn't crash the app

Image.network('url',
       cacheHeight: widget.cacheHeight,
       cacheWidth: widget.cacheWidth,
       fit: BoxFit.cover,
          errorBuilder: (context, url, error) => Icon(Icons.error),
)

I will also try your solution as well as your solution looks better one

delCatta commented 3 years ago

Having the same issue using this plugin. It raises a EXC_RESOURCE RESOURCE_TYPE_MEMORY on the thread that has name = io.flutter.1.io.

tang4595 commented 3 years ago

@pranavkpr1 _imageCache.clearLiveImages(); Can reduce a lot of memory

截屏2020-08-08 下午4 22 39

It works!

renefloor commented 3 years ago

I just tried an example with setting memCacheHeight and for me it largely reduced the memory footprint of the app.

gathodeharrkirat commented 3 years ago

@pranavkpr1 Hi... Can you pls share an example how u solved this issue........ I am also facing the same issue

renefloor commented 3 years ago

@gathodeharrkirat did you try setting memCacheHeight? For example:

      CachedNetworkImage(
        height: 200,
        width: 200,
        memCacheHeight: 200,
        imageUrl: url,
      ),
ventr1x commented 3 years ago

I am currently debugging an app ready for production but getting reports of crashes on older iphones (1gb RAM). They are mostly caused by images not being correctly optimized by our client (like 4k pics for previews...). Limiting them with memCacheHeight and memCacheWidth should do the trick for most, but it's kind of a sledgehammer method. Source Images should be optimized, not cut like this, as this also stretches non fitting resolutions.

A good idea is also using your own global cache manager instance, listening to low memory warning notifications from the system and dumping the cache to prevent crashes.

This is overall a very classic "limitation" of GPU frameworks and game engines. Pictures and Text (which is rendered and uploaded as bitmaps) have to be highly optimized and manually controlled. A 1kb picture can easily take 20mb+ RAM. Look up the tech and math behind it if you professionally build apps with GPU frameworks!

0ttik commented 3 years ago

Is it possible to add same memCacheHeight option to CachedNetworkImageProvider?

My usecase is that I want to preload some images before showing them and to do so I need ImageProvider in precacheImage() method.

Or maybe an option to get ImageProvider from CachedNetworkImage object as it's done in Image class (image property).

gitaaron commented 3 years ago

I was still getting issues with the app crashing when loading several (approximately 30) large (1-5 MB) images until I also set 'maxWidthDiskCache' and 'maxHeightDiskCache' (setting 'memCacheWidth' and 'memCacheHeight' was not enough).

I think I also had to ensure that the 'maxWidthDiskCache' and 'maxHeightDiskCache' values were the same as 'memCacheWidth' and 'memCacheHeight' respectively or else the memory cache was not used.

Does anyone know how I can verify that I have fixed this issue for good? I am worried the app will continue to crash on older devices or if I introduce more images.

alvindrakes commented 3 years ago

I was still getting issues with the app crashing when loading several (approximately 30) large (1-5 MB) images until I also set 'maxWidthDiskCache' and 'maxHeightDiskCache' (setting 'memCacheWidth' and 'memCacheHeight' was not enough).

I think I also had to ensure that the 'maxWidthDiskCache' and 'maxHeightDiskCache' values were the same as 'memCacheWidth' and 'memCacheHeight' respectively or else the memory cache was not used.

Does anyone know how I can verify that I have fixed this issue for good? I am worried the app will continue to crash on older devices or if I introduce more images.

@gitaaron I'm not sure if this will help for your case. But I was facing a similar crash problem due to memory limit in the past with a lot of images ranging from 1mb - 4mb showing in a listview builder, even after setting up everything as suggested in the sub.

I ended up compressing all the images manually in the database to 70kb, and the crashing issues are gone.

So my assumptions is that images cannot be over 1mb when showing in a listview. 🤔

weiminghuaa commented 2 years ago

@renefloor Setting memCacheHeight is an effective way,but not wok on gif,any idea?

renefloor commented 2 years ago

@weiminghuaa I can imagine that gifs are not resized. I have to study on how that could work. However, resizing a gif will probably mean that we will have to extract all images from the gif, resize them all and combine it again to a gif. I really recommend to resize gifs on the backend and return a smaller gif if that's possible.

khangpt commented 2 years ago

I read code and saw that _memCache is Map without having any method to limit number of item cached on memory. Hence I think the memory size will increase until it reach the maximum allowance of system => app crashed

joechan-cq commented 2 years ago

I think the memory issue of Image In ListView, maybe, because of 'addAutomaticKeepAlives' of ListView, setting false could sovle the problem.

Charlesvandamme commented 2 years ago

I was experiencing a lot of memory issues with a gallery builder that loads up to 60 images using cachednetworkimage package.

I could solve my memory problems with:

  1. Adding a memCacheWidth parameter to the CachedNetworkImage widgets
  2. Inserting this piece of code in my dispose method:

    `  @override
    void dispose() {
    
    _scrollController.dispose();
    
    ImageCache _imageCache = PaintingBinding.instance!.imageCache!;
    
    _imageCache.clear();
    
    _imageCache.clearLiveImages();
    
    super.dispose();
    }`
dieptx commented 1 year ago

https://github.com/Baseflow/flutter_cached_network_image/issues/429#issuecomment-1121360368 Good point but it doesn't work 😢 Is there any solution, my app still crashed with huge memory? 😭

ZacharyHandshoe commented 1 year ago

This is an ongoing issue on our end as well, but our image sizes are fairly small. This primarily occurs on ios devices actually. Our Android tests have shown no crashes, while ios crashes due to this package.

ghost commented 1 year ago

We are also facing this issue, similar performance degradation on both ios and android. The image sizes are fairly reasonable. Is there some file format that has a lower memory footprint that we can use?

inc16sec commented 1 year ago

Facing this issue as well with Flutter 3.7

KaelTeck commented 1 year ago

Downgrading to Flutter 3.3.10 has significantly reduced the number of crashes for me.

redvelvet-fan commented 1 year ago

Is it being solved?

keithcwk commented 1 year ago

Noticed its causing performance issues on flutter 3.7 as well

Here I have a comparison of a list containing images without memCacheWidth and memCacheHeight on release mode, vs with memCacheWidth and memCacheHeight in profile mode, the performance difference is quite signficant

Apologies for the quality due to compression, but do observe the CPU and GPU charts

Without memCacheHeight and memCacheWidth on release mode

https://user-images.githubusercontent.com/65481917/235132650-a5d2d920-1d73-4f8f-9325-63f5695888e4.mp4

With memCacheHeight and memCacheWidth on profile mode

https://user-images.githubusercontent.com/65481917/235132614-a07e0be6-e2fd-4c91-b06e-b3a81bd18ca4.mp4

mrRedSun commented 1 year ago

Still an issue

RomanSoviak commented 11 months ago

Any updates?

vuhoangminh91 commented 8 months ago

Call evict on imageProvider get from CachedNetworkImage help me solve issue. await imageProvider.obtainCacheStatus(configuration: ImageConfiguration.empty).then( (value) { imageProvider.evict(); }, );

trupeshv commented 6 months ago

Call evict on imageProvider get from CachedNetworkImage help me solve issue. await imageProvider.obtainCacheStatus(configuration: ImageConfiguration.empty).then( (value) { imageProvider.evict(); }, );

@vuhoangminh91 Where do we need to add this line?

marwenbk commented 2 months ago

Any updates?

richanshah commented 1 month ago

any updates?

richanshah commented 1 week ago

any updates?