Skip to content

Android memory leaks until it is killed by the system #31303

Closed

Description

@fly512

We tested the latest version, as well as beta version, and found that if any animation on the APP plus a picture, it would lead to a constant increase in memory. The more complex the UI, the faster the memory grows (Native memory).

The simplest version below, with only one picture and one animation

企业微信截图_15556719181553

When APP goes back to the background, Native memory decreases slightly. In the background, the memory will not change, but when it goes back to the front, Native memory will be higher than before.

[√] Flutter (Channel unknown, v1.5.2, on Microsoft Windows [Version 10.0.17134.706], locale zh-CN)
    • Flutter version 1.5.2 at C:\tool\flutter
    • Framework revision efe744a360 (2 days ago), 2019-04-17 10:43:19 -0400
    • Engine revision fdd427241b
    • Dart version 2.2.1 (build 2.2.1-dev.4.2 8e73b3c006)


[√] Android toolchain - develop for Android devices (Android SDK version 28.0.3)
    • Android SDK at C:\Users\xxx\AppData\Local\Android\sdk
    • Android NDK location not configured (optional; useful for native profiling support)
    • Platform android-28, build-tools 28.0.3
    • ANDROID_HOME = C:\Users\xxx\AppData\Local\Android\sdk
    • Java binary at: C:\tool\android-studio\jre\bin\java
    • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-1248-b01)
    • All Android licenses accepted.

[√] Android Studio (version 3.3)
    • Android Studio at C:\tool\android-studio
    • Flutter plugin version 34.0.1
    • Dart plugin version 182.5215
    • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-1248-b01)

[√] IntelliJ IDEA Community Edition (version 2018.2)
    • IntelliJ at C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2018.2.1
    • Flutter plugin version 34.0.1
    • Dart plugin version 182.5215

[√] VS Code, 64-bit edition (version 1.31.1)
    • VS Code at C:\Program Files\Microsoft VS Code
    • Flutter extension version 2.25.1

[√] Connected device (1 available)
    • KNT AL10 • 69DDU16511000544 • android-arm64 • Android 6.0 (API 23)
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';

void main() async {
	runApp(App());
}

class App extends StatefulWidget {
	App() {
		print("new App");
	}
	
	@override
	_AppState createState() => new _AppState();
	
}

class _AppState extends State<App> {
	@override
	void initState() {
		super.initState();
	}
	
	@override
	void dispose() {
		super.dispose();
	}
	
	@override
	Widget build(BuildContext context) {
		print("App Build");
		return new MaterialApp(
			title: "test",
			home: Boot()
		);
	}
}

class Boot extends StatefulWidget {
	Boot({Key key}) : super(key: key);
	
	@override
	BootState createState() => new BootState();
}

class BootState extends State<Boot> {
	@override
	void initState() {
		super.initState();
	}
	
	@override
	void dispose() {
		super.dispose();
	}
	
	@override
	Widget build(BuildContext context) {
		List<Widget> res = [];
		for(int i = 0; i < 200; i++){
			res.add(
				new Container(
					height: 20.0,
					decoration: new BoxDecoration(
						border: new Border(
							bottom: new BorderSide(
								color: Theme.of(context).dividerColor
							)
						)
					),
					child: new Text(
						i.toString()
					),
				)
			);
		}
		return new Scaffold(
			body: new Stack(
				children: <Widget>[
					new Positioned(
						child: new Image.network(
							"http://47.254.30.207/background.png",
							fit: BoxFit.cover,
						),
						left: 0.0,
						top: 0.0,
						width: MediaQuery.of(context).size.width,
						height: MediaQuery.of(context).size.height,
					),
					new Positioned(
						left: 0.0,
						top: 0.0,
						width: MediaQuery.of(context).size.width,
						height: MediaQuery.of(context).size.height,
						child: new Center(
							child: new CupertinoActivityIndicator(
								animating: true,
								radius: 50.0,
							),
						),
					)
				],
			),
		);
	}
}

Activity

  1. fly512 commented on Apr 19, 2019

    @fly512
    Author
  2. mklim commented on Apr 19, 2019

    @mklim
    Contributor

    Thanks for reporting the bug!

    In debug mode I'm seeing memory climbing just from combining the loop/array with the animation even without any image.

    So far I've been unable to reproduce any kind of memory climb at all in release mode though.

    Just making sure, are you seeing this leak in both release mode and debug mode?

  3. added
    c: crashStack traces logged to the console
    c: performanceRelates to speed or footprint issues (see "perf:" labels)
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Apr 19, 2019
  4. fly512 commented on Apr 20, 2019

    @fly512
    Author

    @mklim We also found this problem in profile and release modes, as long as the time is long enough (the screen is not closed, less than the background, hours) memory will continue to grow, when the UI is very complex, memory growth is very fast.

    We also found that if there were no image resources on the UI, there would be no problem.

  5. removed
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Apr 20, 2019
  6. fly512 commented on Apr 20, 2019

    @fly512
    Author

    Profile Mode
    A more complex UI was redone and ran for more than an hour (the screen was always on in the front desk).
    You'll find that Native memory keeps going up.

    profile

    2 hours
    3

  7. fly512 commented on Apr 20, 2019

    @fly512
    Author

    This is the test code.
    Two resource images are needed in the assets/images/ directory, as shown in the annex.

    import 'package:flutter/material.dart';
    import 'package:flutter/cupertino.dart';
    import 'package:flutter/foundation.dart';
    import 'dart:async';
    import 'dart:math';
    
    void main() async {
    	runApp(App());
    }
    
    class App extends StatefulWidget {
    	App() {
    		print("new App");
    	}
    	
    	@override
    	_AppState createState() => new _AppState();
    	
    }
    
    class _AppState extends State<App> {
    	@override
    	void initState() {
    		super.initState();
    	}
    	
    	@override
    	void dispose() {
    		super.dispose();
    	}
    	
    	@override
    	Widget build(BuildContext context) {
    		print("App Build");
    		return new MaterialApp(
    			title: "test",
    			home: Boot()
    		);
    	}
    }
    
    class Boot extends StatefulWidget {
    	Boot({Key key}) : super(key: key);
    	
    	@override
    	BootState createState() => new BootState();
    }
    
    class BootState extends State<Boot> {
    	int _index = 0;
    	@override
    	void initState() {
    		super.initState();
    		new Timer.periodic(new Duration(seconds: 3), (Timer timer){
    			this.setState((){
    				this._index = (this._index + 1) % 8;
    			});
    		});
    	}
    	
    	@override
    	void dispose() {
    		super.dispose();
    	}
    	
    	@override
    	Widget build(BuildContext context) {
    		return new Scaffold(
    			body: new Stack(
    				children: <Widget>[
    					new Positioned(
    						child: new Image.asset(
    							"assets/images/background.png",
    							fit: BoxFit.cover,
    						),
    						left: 0.0,
    						top: 0.0,
    						width: MediaQuery.of(context).size.width,
    						height: MediaQuery.of(context).size.height,
    					),
    					new Container(
    						width: MediaQuery.of(context).size.width,
    						child: new Column(
    							children: <Widget>[
    								new AppBar(
    									automaticallyImplyLeading: false,
    									leading: new BackButton(),
    									title: new Text("test"),
    									backgroundColor: Colors.transparent,
    									elevation: 0.0,
    								),
    								new Padding(
    									child: new Wrap(
    										runSpacing: 15.0,
    										children: List.generate(8, (int index){
    											return new HeadIcon(
    												isSpeaking : index == this._index
    											);
    										}),
    									),
    									padding: new EdgeInsets.only(
    										top: 15.0,
    										bottom: 15.0
    									),
    								),
    								new Expanded(
    									child: new MessageList(),
    								)
    							],
    						),
    					),
    				],
    			),
    		);
    	}
    }
    
    class MessageList extends StatefulWidget {
    	MessageList({Key key}):super(key: key);
    	MessageListState createState() => new MessageListState();
    }
    
    class MessageListState extends State<MessageList> {
    	Timer _timer;
    	List<int> _data = [];
    	ScrollController _controller;
    	Random _random = new Random();
    	
    	@override
      void initState() {
        super.initState();
        this._controller = new ScrollController(keepScrollOffset: true);
        this._controller.addListener(this._onScroll);
        this._timer = Timer.periodic(new Duration(milliseconds: 200), this._onTimer);
      }
      
      @override
      void dispose() {
    		if(this._timer != null){
    			this._timer.cancel();
    			this._timer = null;
    		}
    		this._controller.removeListener(this._onScroll);
    		this._controller.dispose();
        super.dispose();
      }
    	
    	_onTimer(Timer timer){
    		this.setState((){
    			this._data.add(this._random.nextInt(20));
    			if(this._data.length > 200){
    				this._data.removeRange(0, this._data.length - 200);
    			}
    		});
    		//for test
    		this._scrollEnd();
    	}
    	
    	void _onScroll(){
    	
    	}
    	
    	void _scrollEnd(){
    		if(!this._controller.hasClients) return;
    		if(this._controller.position == null) return;
    		if(this._controller.position.maxScrollExtent == null) return;
    		if(this._controller.position.maxScrollExtent > this._controller.offset){
    			this._scrollToEnd();
    		}
    	}
    	
    	void _scrollToEnd() {
    		if(!this._controller.hasClients) return;
    		this._controller.jumpTo(
    			this._controller.position.maxScrollExtent
    		);
    	}
     
    	@override
    	Widget build(BuildContext context) {
        return new ListView.builder(
    	    controller: this._controller,
    	    itemCount: this._data.length,
    	    itemBuilder: (BuildContext context, int index){
    	    	return new Container(
    			    height: 42.0 + this._data.elementAt(index),
    			    padding: new EdgeInsets.only(
    				    left: 15.0,
    				    top: 6.0,
    				    bottom: 6.0
    			    ),
    			    child: new Row(
    				    children: <Widget>[
    							new ClipRRect(
    								borderRadius: new BorderRadius.all(
    									new Radius.circular(15.0)
    								),
    								child: new Image.asset(
    									"assets/images/head.jpg",
    									width: 30.0,
    									height: 30.0,
    								),
    							),
    					    new Padding(
    						    padding: new EdgeInsets.only(
    							    left: 12.0
    						    ),
    						    child: new Text(
    							    "number ${this._data.elementAt(index)} => data len = ${this._data.length} => max len = 200",
    							    style: new TextStyle(
    								    fontSize: 12.0
    							    ),
    						    ),
    					    )
    				    ],
    			    ),
    		    );
        });
      }
    }
    
    class HeadIcon extends StatelessWidget {
    	final bool isSpeaking;
    	HeadIcon({Key key, this.isSpeaking = false}):super(key: key);
    	
    	@override
    	Widget build(BuildContext context) {
        return new Container(
    	    width: MediaQuery.of(context).size.width / 4,
    	    alignment: Alignment.topCenter,
    	    child: new Stack(
    		    overflow: Overflow.visible,
    		    children: <Widget>[
    		    	new ClipRRect(
    				    borderRadius: new BorderRadius.all(
    					    new Radius.circular(33.0)
    				    ),
    				    child: new Image.asset(
    					    "assets/images/head.jpg",
    					    width: 66.0,
    					    height: 66.0,
    				    ),
    			    ),
    			    new SoundFastWare(
    				    isSpeaking: this.isSpeaking,
    			    )
    		    ],
    	    ),
        );
      }
    }
    
    class SoundFastWare extends StatefulWidget {
    	final bool isSpeaking;
    	SoundFastWare({Key key, this.isSpeaking}):super(key: key);
    	
    	@override
    	_SoundFastWareState createState() => new _SoundFastWareState();
    }
    
    class _SoundFastWareState extends State<SoundFastWare> with SingleTickerProviderStateMixin {
    	AnimationController _controller;
    	Animation _opacity;
    	Animation _scale;
    	
    	@override
    	void initState() {
    		super.initState();
    		this._controller = new AnimationController(
    			duration: const Duration(milliseconds: 800),
    			vsync: this,
    		);
    		this._opacity = Tween(
    			begin: 1.0,
    			end: 0.0,
    		).animate(
    			CurvedAnimation(
    				parent: this._controller,
    				curve: Interval(
    					0.0,
    					1.0,
    					curve: Curves.linear,
    				),
    			),
    		);
    		this._scale = Tween(
    			begin: 1.0,
    			end: 1.25,
    		).animate(
    			CurvedAnimation(
    				parent: this._controller,
    				curve: Interval(
    					0.0,
    					1.0,
    					curve: Curves.linear,
    				),
    			),
    		);
    		this._scale.addListener(this._onChanged);
    		if(this.widget.isSpeaking){
    			this._controller.forward();
    		}
    	}
    	
    	@override
    	void didUpdateWidget(SoundFastWare old){
    		super.didUpdateWidget(old);
    		if(old.isSpeaking != this.widget.isSpeaking){
    			if(this.widget.isSpeaking){
    				this._controller.forward(from: 0.0);
    			}
    		}
    	}
    	
    	@override
    	void dispose() {
    		this._scale.removeListener(this._onChanged);
    		this._controller.dispose();
    		super.dispose();
    	}
    	
    	_onChanged() async {
    		setState(() {
    		
    		});
    		if(this._controller.status == AnimationStatus.completed && this.widget.isSpeaking){
    			this._controller.forward(from: 0.0);
    		}
    	}
    	
    	Widget _buildAni(BuildContext context, Widget child) {
    		double width = 66.0;
    		double translate = 0.0 - ((this._scale.value - 1.0) * width / 2 / this._scale.value);
    		return new Container(
    			width: width,
    			height: width,
    			decoration: new BoxDecoration(
    				border: new Border.all(
    					color: this.widget.isSpeaking ?
    					Colors.amberAccent.withOpacity(this._opacity.value) :
    					Colors.transparent,
    					width: 3.0,
    					style: BorderStyle.solid,
    				),
    				borderRadius: new BorderRadius.circular(width / 2),
    			),
    			transform: Matrix4.identity()
    				..scale(this._scale.value, this._scale.value)
    				..translate(translate, translate),
    		);
    	}
    	
    	@override
    	Widget build(BuildContext context) {
    		double width = 66.0;
    		return new Positioned(
    			left: 0.0,
    			top: 0.0,
    			width: width,
    			height: width,
    			child: new IgnorePointer(
    				child: new AnimatedBuilder(
    					animation: this._controller,
    					builder: _buildAni
    				),
    			),
    		);
    	}
    }
    

    background
    head

  8. fly512 commented on Apr 22, 2019

    @fly512
    Author

    @mklim I'm sorry to disturb you. Very anxious, online project

  9. mklim commented on Apr 22, 2019

    @mklim
    Contributor

    Got it, thanks for the clarification. And it's not a problem, thanks for the extra info. Looking into it again.

  10. mklim commented on Apr 22, 2019

    @mklim
    Contributor

    I'm seeing a leak with the latest sample case. No OOM crash yet, but I can see it climbing just from letting it run for a couple minutes. /cc @jason-simmons

  11. mklim commented on Apr 22, 2019

    @mklim
    Contributor

    I think ImageCache is likely related here. I'm not seeing the same linear climb with maximumSize = 0.

  12. jason-simmons commented on Apr 22, 2019

    @jason-simmons
    Member

    I noticed that a lot of EngineLayer objects are not being garbage collected promptly when running this app. Increasing EngineLayer::GetAllocationSize to something much higher seems to help.

  13. fly512 commented on Apr 23, 2019

    @fly512
    Author

    Thank you very much. We will adjust Engine Layer:: GetAllocation Size and then test it.

  14. 18 remaining items

  15. zhileichen commented on Apr 28, 2019

    @zhileichen

    Are any optimizations turned on to disable the modification of EngineLayer::GetAllocationSize in the Release mode ?

  16. mraleph commented on Apr 28, 2019

    @mraleph
    Member

    @dnfield I don't think there is any amazing GC based solution here - it a fairly classical issue when GC managed heap retains native resources of unknown size. If you have an object which is managed by GC then you need to periodically trigger full GC to collect it. For GC to trigger VM needs to understand that heap is too large. However if full GC triggers too often - you get performance degradation.

    /cc @rmacnak-google

  17. liyuqian commented on Apr 28, 2019

    @liyuqian
    Contributor

    @zhileichen I think huge EngineLayer::GetAllocationSize in the profile mode may be triggering full GC to collect memory in other places. It seems that doesn't happen in release mode so you may need to find another way. @mraleph and @rmacnak-google from the Dart team may have better suggestions to that end.

  18. fly512 commented on May 21, 2019

    @fly512
    Author

    @jason-simmons @mklim @dnfield
    I'm sorry to interrupt. Is there any progress to solve this problem?

  19. fly512 commented on May 31, 2019

    @fly512
    Author

    @jason-simmons @mklim @dnfield
    I'm sorry to interrupt. Is there any progress to solve this problem?

  20. added
    customer: crowdAffects or could affect many people, though not necessarily a specific customer.
    on Oct 7, 2019
  21. liyuqian commented on Nov 25, 2019

    @liyuqian
    Contributor

    Bringing this back to our attentions. This is already a "customer: crowd" issue, and I just heard from @cbracken that Fuchsia is also experiencing this kind of memory issue (the memory usage keeps going up). So this issue is becoming more and more important.

    Here's one potential strategy to fix this:

    1. Confirm the reproduction of this issue on master Flutter today. @mklim : since you've tried reproduced it earlier, can you please confirm if it's still reproducible today?
    2. Once reproduced, turn the reproduction into a device lab memory test so everyone can reproduce this issue with a minimum effort, and we can continuously track our working progress. Either I, @dnfield , or @jason-simmons can take this once the reproduction is confirmed.
    3. Collaborate with the DevTools (CC @cobblest @terrylucas ) and Dart teams to figure out exactly where the memory goes. The engine may need to provide better APIs for that.
  22. dnfield commented on Nov 26, 2019

    @dnfield
    Contributor

    Since this bug was opened, we've reduced the size of the Skia resource cache significantly. Is it significantly increasing beyond that?

    We should take a look at where the allocated memory is going.

  23. liyuqian commented on Nov 26, 2019

    @liyuqian
    Contributor

    @dnfield : if it never increases beyond that point, which means that this issue is no longer reproducible, then we can close this issue without further work :D That's the best case and I do hope Michael would give us that good news. And if that's indeed the case, we'll probably just file a separate issue on Fuchsia only as they may not set the resource cache properly.

  24. mklim commented on Nov 26, 2019

    @mklim
    Contributor

    I'm not seeing an obvious leak given the earlier repro case from issuecomment-485139640. I am seeing an initial spike in memory, but the usage seems to drop back down to around 9400KB on my test device given enough time. Test data is in this spreadsheet.

    Given that I think we should close this and re-open new issues about leaks in the future with specific reproduction cases. Memory leaks can come from basically anywhere, so it's likely whatever was root causing this initially has been fixed like @dnfield suggested and the leaks @cbracken is reporting on Fuchsia are from a different path.

  25. dnfield commented on Nov 26, 2019

    @dnfield
    Contributor

    Thanks for the detailed analysis @mklim . A quick look at your data suggests to me that flutter-team-archive/engine#10434 probably helped this case significantly. The graphics cache is not getting larger than 50mb (assuming I have the scale right) in your sheet, whereas previously it would have been allowed to grow to about 10x that.

  26. github-actions commented on Aug 25, 2021

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

  27. locked as resolved and limited conversation to collaborators on Aug 25, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

c: crashStack traces logged to the consolec: performanceRelates to speed or footprint issues (see "perf:" labels)customer: crowdAffects or could affect many people, though not necessarily a specific customer.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions