flutter_cached_network_image icon indicating copy to clipboard operation
flutter_cached_network_image copied to clipboard

Memory leak on real device only

Open EArminjon opened this issue 10 months ago • 7 comments

🐛 Bug Report

On real device Android & iOS this package have a memory leak.

Our app got some crash in production because of this issue : we have a long list of product inside a paginated infinite list. User can scroll on it and some of them reported crash. After investigation we discover this memory leak.

Expected behavior

Constant RSS usage

Reproduction steps

Use code bellow and check on devtools the memory usage graph.

With CachedNetworkImage : (increase during scroll) Capture d’écran 2024-04-10 à 17 01 45

With Image.network: (stable during scroll) Capture d’écran 2024-04-10 à 17 02 46

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: false,
      ),
      home: const Home(),
    );
  }
}

class Home extends StatefulWidget {
  const Home({super.key});

  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  bool useCachedNetwork = true;

  @override
  Widget build(BuildContext context) {
    return Builder(
      builder: (BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text("Memory leak"),
          ),
          floatingActionButton: FloatingActionButton.extended(
            label: Text(
              "Use ${useCachedNetwork ? "Image.network" : "CachedNetworkImage"}",
            ),
            onPressed: () {
              setState(() => useCachedNetwork = !useCachedNetwork);
            },
          ),
          body: ListView.builder(
            itemBuilder: (BuildContext context, int index) => SizedBox(
              height: 80,
              child: Card(
                child: Padding(
                  padding: const EdgeInsets.all(16),
                  child: Row(
                    children: <Widget>[
                      AspectRatio(
                        aspectRatio: 1,
                        child: useCachedNetwork
                            ? CachedNetworkImage(
                                imageUrl: "https://picsum.photos/id/$index/1000/1000",
                                errorListener: (_) {},
                                progressIndicatorBuilder: (
                                  BuildContext context,
                                  String url,
                                  DownloadProgress progress,
                                ) =>
                                    Center(
                                  child: CircularProgressIndicator(
                                    value: progress.progress,
                                  ),
                                ),
                                errorWidget: (_, __, ___) => const Center(
                                  child: Icon(Icons.error),
                                ),
                              )
                            : Image.network(
                                "https://picsum.photos/id/$index/1000/1000",
                                errorBuilder: (_, __, ___) => const Center(
                                  child: Icon(Icons.error),
                                ),
                              ),
                      ),
                      const SizedBox(width: 16),
                      Expanded(
                        child: Text(
                          index.toString(),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ),
        );
      },
    );
  }
}

Configuration

Versions:

flutter: 3.16.8
cached_network_image: 3.3.1

Platform:

  • [x] :iphone: iOS
  • [X] :robot: Android (Pixel 7 Android 14)

EArminjon avatar Apr 10 '24 15:04 EArminjon

on Linux same issue, actually it crush quicker.

marwenbk avatar Jun 06 '24 10:06 marwenbk

For those facing this issue, the following solution/workaround worked for me. https://github.com/flutter/flutter/issues/102140#issuecomment-2031004058

Ockerjo avatar Jun 11 '24 14:06 Ockerjo

@EArminjon I found using the errorListener makes Flutter unable to remove unused instances of the CachedNetworkImage with the garbage collector. If you don't mind having error handling, try removing the errorListener property and check if that helps with the memory leaks.

Reference to https://github.com/Baseflow/flutter_cached_network_image/issues/951

limonadev avatar Jun 14 '24 17:06 limonadev

Any update for this Issue? I create a simple app and confirmed that errorListener causing memory leak. bug_cached_network_image

  • with setting errorListener img_v3_02dc_2d21940d-60cd-4128-896d-68c1d32603ix

  • without setting errorListener image

andannn avatar Aug 02 '24 08:08 andannn

Sadly we also bumped into this very serious issue. If you check ImageCompleterHandler's dispose method you can see that it calls maybeDispose on the ImageStreamCompleter object. This maybeDispose method does not do anything if the completer has any listeners, so basically nothing is disposed from the memory.

Replacing addListener() by addEphemeralErrorListener() (https://github.com/flutter/flutter/commit/aeddab428d4d51dc2e7b070969572067a37d3650)?) would solve this issue. The only problem is: this method is only available since 3.16, but this package has flutter: '>=3.10.0' requirements in its pubspec.yaml, so I'm not sure how could be this implemented with backward compatibility:

if (errorListener != null) {
  imageStreamCompleter.addEphemeralErrorListener((exception, stackTrace) {
    errorListener?.call(exception);
  });
}

Without the listener all errors are forwarded to FlutterError.onError which spams them to Crashlytics if its connected, so it is a tricky situation.

slaci avatar Aug 07 '24 12:08 slaci

I also confirm errorListener cause memory leak. Is this a bug of this package or a Flutter bug?

XuanTung95 avatar Aug 19 '24 05:08 XuanTung95

@XuanTung95 This is a bug of this package, and I think it is related to this PR https://github.com/Baseflow/flutter_cached_network_image/pull/891

andannn avatar Aug 20 '24 00:08 andannn