flutter / flutter

Flutter makes it easy and fast to build beautiful apps for mobile and beyond
https://flutter.dev
BSD 3-Clause "New" or "Revised" License
165.52k stars 27.32k forks source link

PaintingBinding.instance.imageCache.maximumSizeBytes not working #110847

Open Tom3652 opened 2 years ago

Tom3652 commented 2 years ago

Steps to Reproduce

The title is actually wrong : the setter is working (i have logged it) but the actions that should evict and release images from memory after setting the value are not working.

  1. Run the sample app
  2. Open dev tools on the memory tab and note the current memory usage value
  3. Click on the start button
  4. See the memory increase
  5. Go back to the main page and note the memory usage

Expected results:

In previous version of Flutter ( < 3.0.x), when using PaintingBinding.instance.imageCache.maximumSizeBytes = 1024 * 1024 * 30 it would evict any images from memory cache to maintain the cap at 30MB of memory usage.

In this example, the memory usage of step 5 should be equal at step 2 ideally. But if for any reason it would not be the case, the memory of step 5 should NOT be higher than memory_step2_value + 30MB thanks to the PaintingBinding.instance.imageCache.maximumSizeBytes setter.

Actual results:

The memory usage is much higher.

In my case it's + 140MB :

Capture d’écran 2022-09-02 à 13 03 21

Against 30MB initially :

Capture d’écran 2022-09-02 à 13 04 20

This is a real issue because this was my workaround to counter #102140 and #62107 and it's not working anymore.
The real issue is the above one because images are not released from memory or at least in a very bad / long way.

Code sample ```dart import 'package:flutter/material.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); PaintingBinding.instance.imageCache.maximumSizeBytes = 1024 * 1024 * 30; runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) => const MaterialApp(home: Home()); } class Home extends StatelessWidget { const Home({Key? key}) : super(key: key); Future openGallery(BuildContext context) { return Navigator.of(context).push(MaterialPageRoute(builder: (_) => Gallery())); } @override Widget build(BuildContext context) { return Scaffold( body: Center( child: ElevatedButton( child: const Text("Start"), onPressed: () => openGallery(context), ), ), ); } } class Gallery extends StatelessWidget { final String imageURL = 'https://picsum.photos/1500?image='; final int maxImageID = 75; final int columns = 2; final ScrollController controller = ScrollController(); Gallery({Key? key}) : super(key: key); void animateToOffset(double offset) { if (offset > controller.offset) { Future.delayed(const Duration(seconds: 1)).then((_) { controller.animateTo( offset, duration: const Duration(seconds: 1), curve: Curves.easeInOut, ); }); } } @override Widget build(BuildContext context) { double imageSize = MediaQuery.of(context).size.width / columns; return Scaffold( appBar: AppBar(title: const Text("Gallery")), body: GridView.builder( controller: controller, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: columns), itemBuilder: (context, index) { return Image.network( "$imageURL${index % maxImageID}", fit: BoxFit.cover, height: imageSize, width: imageSize, frameBuilder: (context, child, frame, _) { if (frame == null) return child; animateToOffset(imageSize * (index ~/ columns)); return child; }, ); }, ), ); } } ```
Logs ``` [✓] Flutter (Channel stable, 3.3.0, on macOS 12.5.1 21G83 darwin-x64, locale fr-FR) • Flutter version 3.3.0 on channel stable at /Users/foxtom/Desktop/flutter • Upstream repository https://github.com/flutter/flutter.git • Framework revision ffccd96b62 (3 days ago), 2022-08-29 17:28:57 -0700 • Engine revision 5e9e0e0aa8 • Dart version 2.18.0 • DevTools version 2.15.0 [✓] Android toolchain - develop for Android devices (Android SDK version 33.0.0) • Android SDK at /Users/foxtom/Library/Android/sdk • Platform android-33, build-tools 33.0.0 • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840) • All Android licenses accepted. [✓] Xcode - develop for iOS and macOS (Xcode 13.4.1) • Xcode at /Applications/Xcode.app/Contents/Developer • Build 13F100 • CocoaPods version 1.11.3 [✓] Chrome - develop for the web • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome [✓] Android Studio (version 2021.2) • Android Studio at /Applications/Android Studio.app/Contents • Flutter plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/9212-flutter • Dart plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/6351-dart • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840) [✓] VS Code (version 1.70.2) • VS Code at /Users/foxtom/Desktop/Visual Studio Code.app/Contents • Flutter extension version 3.46.0 [✓] Connected device (3 available) • Now You See Me (mobile) • 00008020-001204401E78002E • ios • iOS 15.6.1 19G82 • macOS (desktop) • macos • darwin-x64 • macOS 12.5.1 21G83 darwin-x64 • Chrome (web) • chrome • web-javascript • Google Chrome 104.0.5112.101 [✓] HTTP Host Availability • All required HTTP hosts are available • No issues found! ```
darshankawar commented 2 years ago

Thanks for the report @Tom3652 Can we know what's the image resolution is being used ? Also, can you try by using cacheWidth, cacheHeight inside Image.Network and see if it makes any difference ?

Tom3652 commented 2 years ago

Hi @darshankawar, yes it's in the URL actually 1500x1500. However this is only to demonstrate the bad behavior that i am using higher resolution images, if you set a lower cacheWidth it will only delay the problem but the OOM is inevitable if you have an infinite list of photos and scrolling down.

In previous versions of flutter (especially 2.10.x), the usage of PaintingBinding.instance.imageCache.maximumSizeBytes in my app allowed me to keep the same memory usage while scrolling this infinite photo list.

darshankawar commented 2 years ago

Check this comment and issue for your reference : https://github.com/flutter/flutter/issues/110331#issuecomment-1230505200

Tom3652 commented 2 years ago

I have checked the provided link and the comment doesn't help unfortunately. In my real app, i am display veeery small resolution pictures (thumbnails 15-30kB) each, but after scrolling through hundreds of items, i run into OOM.

The comment you mention is only about delaying the real issue. The real issue is that flutter doesn't release image from memory when they are no longer on the screen. If it was the case, in my very simple sample code, you would be at 30MB after going back from the ListView with Navigator.pop(), which isn't the case.

Also, the comment you mentioned is not talking about infinite list of items. It's just a reminder to not load an image in 16000x16000 in memory when the phone can only display 1920x1080 for example, which makes perfect sense & it's normal that if users are trying to display nonsense image resolution, the OOM will come with 3 images and lags etc... So i am guessing that no one is doing so because it doesn't make any sense.

Besides, in my example code above, i didn't put high resolution images, 1500px isn't even full HD and is lower than Instagram quality display to take a real-world example. And Instagram doesn't go in OOM when displaying infinite list of Items with higher resolution than i do. I simply would like Flutter to do the same as React Native on this one.

But to come back to this current issue, there is something changed in the PaintingBinding.instance.imageCache.maximumSizeByte that make it useless now unfortunately, and that was the issue i was pointing out. There are plenty other issues about OOM in Image widget and i don't want to duplicate, i have already made some myself last year.

darshankawar commented 2 years ago

Thanks for the feedback @Tom3652. Curious to know if the memory usage is also same in release and profile mode ? Because, if you are running in debug mode, the memory usage is on higher side as compared to in other modes. Can you try by running in --profile mode and share the observation to see if it is still high ?

Tom3652 commented 2 years ago

Hi @darshankawar ! I have tried in profile mode and i do see the same behavior, with a lower amount of RAM used with lower resolution pictures of 500x500.

The images are not cleared from memory while scrolling the ListView.builder either. The images no longer on the screen should be evicted from memory using the above method.

You can see the same behavior by running this sample, click on start button and see the memory in the dev tools only increasing, just wait and see the bad behavior. You will also see that popping the page will not release the memory :

import 'package:flutter/material.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  PaintingBinding.instance.imageCache.maximumSizeBytes = 1024 * 1024 * 30;
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) => const MaterialApp(home: Home());
}

class Home extends StatelessWidget {
  const Home({Key? key}) : super(key: key);

  Future<void> openGallery(BuildContext context) {
    return Navigator.of(context).push(MaterialPageRoute(builder: (_) => Gallery()));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: ElevatedButton(
          child: const Text("Start"),
          onPressed: () => openGallery(context),
        ),
      ),
    );
  }
}

class Gallery extends StatelessWidget {
  final String imageURL = 'https://picsum.photos/500?image=';
  final int maxImageID = 75;
  final int columns = 2;
  final ScrollController controller = ScrollController();

  Gallery({Key? key}) : super(key: key);

  void animateToOffset(double offset) {
    if (offset > controller.offset) {
      Future.delayed(const Duration(seconds: 1)).then((_) {
        controller.animateTo(
          offset,
          duration: const Duration(seconds: 1),
          curve: Curves.easeInOut,
        );
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    double imageSize = MediaQuery.of(context).size.width / columns;

    return Scaffold(
      appBar: AppBar(title: const Text("Gallery")),
      body: GridView.builder(
        controller: controller,
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: columns),
        itemBuilder: (context, index) {
          return Image.network(
            "$imageURL${index % maxImageID}",
            fit: BoxFit.cover,
            height: imageSize,
            width: imageSize,
            frameBuilder: (context, child, frame, _) {
              if (frame == null) return child;
              animateToOffset(imageSize * (index ~/ columns));
              return child;
            },
          );
        },
      ),
    );
  }
}
darshankawar commented 2 years ago

Thanks for the update. Running code sample above specially in profile and release mode on iOS, I am getting same behavior as reported. The images being higher resolution don't seem to release memory which seems to be adding in total memory usage.

stable, master flutter doctor -v ``` [✓] Flutter (Channel stable, 3.3.0, on macOS 12.2.1 21D62 darwin-x64, locale en-GB) • Flutter version 3.3.0 on channel stable at /Users/dhs/documents/fluttersdk/flutter • Upstream repository https://github.com/flutter/flutter.git • Framework revision ffccd96b62 (29 hours ago), 2022-08-29 17:28:57 -0700 • Engine revision 5e9e0e0aa8 • Dart version 2.18.0 • DevTools version 2.15.0 [!] Xcode - develop for iOS and macOS (Xcode 12.3) • Xcode at /Applications/Xcode.app/Contents/Developer ! Flutter recommends a minimum Xcode version of 13. Download the latest version or update via the Mac App Store. • CocoaPods version 1.11.2 [✓] Chrome - develop for the web • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome [✓] VS Code (version 1.62.0) • VS Code at /Applications/Visual Studio Code.app/Contents • Flutter extension version 3.21.0 [✓] Connected device (5 available) • SM G975F (mobile) • RZ8M802WY0X • android-arm64 • Android 11 (API 30) • Darshan's iphone (mobile) • 21150b119064aecc249dfcfe05e259197461ce23 • ios • iOS 14.4.1 18D61 • iPhone 12 Pro Max (mobile) • A5473606-0213-4FD8-BA16-553433949729 • ios • com.apple.CoreSimulator.SimRuntime.iOS-14-3 (simulator) • macOS (desktop) • macos • darwin-x64 • Mac OS X 10.15.4 19E2269 darwin-x64 • Chrome (web) • chrome • web-javascript • Google Chrome 98.0.4758.80 [✓] HTTP Host Availability • All required HTTP hosts are available ! Doctor found issues in 1 category. [✓] Flutter (Channel master, 3.4.0-19.0.pre.85, on macOS 12.2.1 21D62 darwin-x64, locale en-GB) • Flutter version 3.4.0-19.0.pre.85 on channel master at /Users/dhs/documents/fluttersdk/flutter • Upstream repository https://github.com/flutter/flutter.git • Framework revision 4b5893fbee (49 minutes ago), 2022-09-06 00:14:13 -0400 • Engine revision 61020e974c • Dart version 2.19.0 (build 2.19.0-171.0.dev) • DevTools version 2.17.0 [!] Xcode - develop for iOS and macOS (Xcode 12.3) • Xcode at /Applications/Xcode.app/Contents/Developer ! Flutter recommends a minimum Xcode version of 13. Download the latest version or update via the Mac App Store. • CocoaPods version 1.11.2 [✓] Chrome - develop for the web • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome [✓] VS Code (version 1.62.0) • VS Code at /Applications/Visual Studio Code.app/Contents • Flutter extension version 3.21.0 [✓] Connected device (5 available) • SM G975F (mobile) • RZ8M802WY0X • android-arm64 • Android 11 (API 30) • Darshan's iphone (mobile) • 21150b119064aecc249dfcfe05e259197461ce23 • ios • iOS 14.4.1 18D61 • iPhone 12 Pro Max (mobile) • A5473606-0213-4FD8-BA16-553433949729 • ios • com.apple.CoreSimulator.SimRuntime.iOS-14-3 (simulator) • macOS (desktop) • macos • darwin-x64 • Mac OS X 10.15.4 19E2269 darwin-x64 • Chrome (web) • chrome • web-javascript • Google Chrome 98.0.4758.80 [✓] HTTP Host Availability • All required HTTP hosts are available ! Doctor found issues in 1 category. ```
darshankawar commented 2 years ago

Maybe similar / related to:

https://github.com/flutter/flutter/issues/110331 https://github.com/flutter/flutter/issues/108928

Tom3652 commented 2 years ago

Thanks for verifying. The links you provided are with loading high resolution images. I just want to specify that in the example you just run, the images are in 500x500 which is even less than half of the device screen resolution usually.

And i have picked 500x500 photos to show you the behavior faster, but in my real-world app i am loading pictures 200x200 which are only thumbnails and the app crashes eventually at 100% :(

ChristineWasike commented 9 months ago

I'm also experiencing this same issue

kirill-21 commented 9 months ago

Same, but on Windows, not ios