Baseflow / flutter_cached_network_image

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

Memory leak on real device only #938

Open EArminjon opened 7 months ago

EArminjon commented 7 months ago

🐛 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:

marwenbk commented 5 months ago

on Linux same issue, actually it crush quicker.

Ockerjo commented 5 months ago

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

limonadev commented 5 months ago

@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

andannn commented 3 months ago

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

slaci commented 3 months ago

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.

XuanTung95 commented 3 months ago

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

andannn commented 3 months ago

@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