-
Notifications
You must be signed in to change notification settings - Fork 26.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Dispose render objects when owning element is unmounted. #82883
Conversation
Something is missing here. Need to add it back and make sure it still passes tests. |
void dispose() { | ||
_foregroundRenderObject?.dispose(); | ||
_backgroundRenderObject?.dispose(); | ||
super.dispose(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not null out the references as well as disposing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
@@ -2841,6 +2887,8 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im | |||
header += ' NEEDS-COMPOSITING-BITS-UPDATE'; | |||
if (!attached) | |||
header += ' DETACHED'; | |||
if (_debugDisposed) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe move this higher up in the list and short circuit all the other checks if the object is disposed? NEEDS-LAYOUT, NEEDS-PAINT, etc are probably all misleading if the object has been disposed.
@override | ||
RenderObject get renderObject => _renderObject!; | ||
RenderObject get renderObject { | ||
assert(_renderObject != null, 'Object disposed'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Object disposed --> RenderObject disposed?
or '$runtimeType unmounted'
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Going with $runtimeType
disposed`, since ROs don't have a notion of unmounting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But isn't this the case where this RenderObjectElement has been unmounted not really the case that the RenderObject has been disposed? The RenderObject probably has been disposed given it is null.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh I missed which thing I was in. I see what you're saying now. Will change to unmounted.
// Don't use the getter, the _renderObject may be null if the element | ||
// is defunct, and the getter asserts that it is not null. This method | ||
// does not guarantee a non-null return anyway. | ||
result = element._renderObject; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we instead return null immediately at the top of this method if the element is defunct?
Seems like a bug if you ever had non-defunct elements in a tree with defunct elements.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. We have other assertions around element that make sure you don't end up in that kind of state.
@@ -5768,6 +5778,11 @@ abstract class RenderObjectElement extends Element { | |||
|
|||
@override | |||
void unmount() { | |||
assert( | |||
!renderObject.debugDisposed!, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: this will also blow up if there is a double unmount causing the _renderObject to already be null. do we care about that case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should not unmount an element more than once. If you do, asserts will fire in the unmount
method itself.
if (isRepaintBoundary) { | ||
_layer = null; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why only null this out if we are a repaint boundary? Shouldn't we release all resources?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What other resources does the top level RenderObject
have?
Children will get dropped or disposed by elements holding them, unless we ourselves created the children, which I'll add to the top level documentation on RO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I meant is: "Why only null our _layer
if it is a repaint boundary? If you're not a repaint boundary, who is responsible for freeing up this resource?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahhh, I guess we could null out our reference. In another branch, this code also calls dispose
on thelayer, which we should only do if we're a repaint boundary. But yeah we can unconditionally null it out.
}); | ||
return true; | ||
}()); | ||
assert(() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: to reduce clutter, could this just be part of the previous assert?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
|
||
/// Release any resources held by this render object. | ||
/// | ||
/// If this render object has created any children directly, it should dispose |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should -> must dispose them before calling super.dispose() ? (since the method asserts that children are disposed?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
/// If this render object has created any children directly, it should dispose | ||
/// of those children in this method as well. However, it should not | ||
/// dispose of children that were created by some other object, such as | ||
/// a [RenderObjectElement]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure I understand what is meant by this "However"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tried to clarify. Basically, only dispose ROs you created directly, which might include your children but does not automatically include them.
|
||
bool _debugDisposed = false; | ||
|
||
/// Release any resources held by this render object. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's probably worth mentioning in the overall class doc on RenderObjects when to dispose them?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added some docs at the top.
@@ -208,4 +208,19 @@ Future<void> main() async { | |||
image.dispose(); | |||
expect(image.debugGetOpenHandleStackTraces()!.length, 0); | |||
}, skip: kIsWeb); // Web doesn't track open image handles. | |||
|
|||
test('Render image disposes its image when it is disposed', () async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: RenderImage
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
@@ -5768,6 +5778,11 @@ abstract class RenderObjectElement extends Element { | |||
|
|||
@override | |||
void unmount() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update the docs on unmount to indicate that that's also the place where resources may get released?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
/// Whether this has been disposed. | ||
/// | ||
/// If asserts are disabled, this property is always null. | ||
bool? get debugDisposed { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should there be more methods guarded with an assert on this? e.g. presumably, you shouldn't re-attach a RenderObject that has been disposed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added to attach, scheduleIniitialPaint, layout, markNeedsCompositingBitsUpdate, markNeedsPaint, replaceRootLayer, scheduleInitialSemantics, markNeedsSemanticsUpdate.
Other things are covered by _debugCanPerformMutations. I was surprised more of these weren't.
} | ||
assert(() { | ||
visitChildren((RenderObject child) { | ||
assert(child._debugDisposed); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: access via regular getter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done - I was avoiding the !
but it doesn't really matter much for asserts and will be less confusing if someone modifies the getter
|
||
bool _debugDisposed = false; | ||
|
||
/// Release any resources held by this render object. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We usually also say that the object is no longer usable after calling dispose.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
Added a fix for |
@override | ||
void dispose() { | ||
visitChildren((RenderObject child) { | ||
child.dispose(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This contradicts the documentation on dispose that you should only dispose children that you created yourself?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This particular render object does create its own children. We'd get an assertion error in the tests otherwise when the element tried to dispose them. It's just kind of confusing to me as to exactly where this happens...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it does create its own children. They are added from the outside here:
rootCircle.add(RenderSolidColor(const Color(0xFF00FFFF), desiredDeltaTheta: kTwoPi * 0.15)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there a way to enumerate just the children that the selector created itself? Would help clarify why the children are disposed and avoid bugs if say someone added a subclass that added more children.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@goderbauer is right, but the test that's failing is actually failing from SectorApp rather than this file. I'm looking into it.
@@ -461,6 +469,12 @@ class RenderBoxToRenderSectorAdapter extends RenderBox with RenderObjectWithChil | |||
this.child = child; | |||
} | |||
|
|||
@override | |||
void dispose() { | |||
child?.dispose(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dito?
if (isRepaintBoundary) { | ||
_layer = null; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I meant is: "Why only null our _layer
if it is a repaint boundary? If you're not a repaint boundary, who is responsible for freeing up this resource?
/// | ||
/// [RenderObject]s are responsible for cleaning up any expensive resources | ||
/// they hold when [dispose] is called, such as [Picture] or [Image] objects. | ||
/// They must _only_ dispose of their [layer] object if [isRepaintBoundary] is |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not super-clear to me. If I own the layer (e.g. because I set it in my paint method) why am I not in charge of releasing it as well? How can my parent still use it if I (the owner) are no longer arround?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dnfield and I haven't finished this discussion, so I'm also interested in the ownership model here. I think there's shared ownership going on, but we lack means to express it. In a ref-counted language I'd be thinking about it as follows: a layer's ref count is bumped by its parent layer and its render object. The count can be in the following states:
2 - when a layer has both a parent layer and an owning RenderObject
1 - when a layer has either a parent layer or an owning RenderObject, but not both
0 - when nothing references the layer, this is when I'd clear the resources of this layer, this is also when recursively this layer's descendants' ref-counts are dropped by one and, possibly, reach 0, get their resources cleaned up, and so on.
This way we don't need to treat repaint boundaries as special. Any render object can own a sub-tree of layers, and the recursive disposal of render objects introduced by this PR makes sure we don't leak layers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this doc is incorrect and I have updated it.
I may have to revisit this as I continue to work out the details about layer disposal, but I think it makes more sense now. PTAL.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. The doc comment is clear, but I'm not sure if what it describes is the right thing to do. Perhaps it is, and I just don't have a full picture in my head. Here's a couple of examples that I'm not sure are handled properly:
- A repaint boundary has a
RenderOpacity
child. The repaint boundary disposes of its layer. Is theRepaintBoundary
pointing to a disposed layer (i.e. a dangling pointer)? - A repaint boundary has another repaint boundary as a child. The parent disposes of its layer. Do we recursively delete pictures of the child repaint boundary? If not, how do we know to not delete them? If yes, how does the child repaint boundary know what its resources got cleaned-up and it needs to repaint?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've changed this wording because it's no longer relevant to this patch. This patch doesn't add anything for disposing of layers, that will be coming in a separate patch.
To start to answer these questions though - if a repaint boundary disposes its layer, it means that repaint boundary is going way and won't be drawn again without getting completely recreated/repainted. And so if it has any child repaint boundaries that have not been moved to some active part of the tree, they will not get drawn again either unless recreated/repainted.
/// [RenderObject]s are responsible for cleaning up any expensive resources | ||
/// they hold when [dispose] is called, such as [Picture] or [Image] objects. | ||
/// This includes any [Layer]s that the render object has directly created. By | ||
/// default, the base implementation of dispose will dispose of the [layer], and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think subclasses can change this "default" behavior since they have to call super.dispose
? Should we maybe just say that Layers created by the render object must be disposes manually unless they have been set to the layer property, which is automatically disposed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also: This patch doesn't contain any layer disposing mechanics (it just nulls out the property). Do you plan to add that logic to this PR or will that be added later? If it is latter, maybe we should just document what's implemented for now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reworded this a bit -layer.dispose will be in a separate patch, instead just talking about nulling the property.
/// If [isRepaintBoundary] returns true, the layer tree rooted at this | ||
/// object's layer will also dispose. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this still valid? Looks like _layer is now nulled regardless of isRepaintBoundary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Also note my comment above regarding that layers do not implement a disposal concept yet...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, removing this for now.
visitChildren((RenderObject child) { | ||
assert( | ||
child.debugDisposed!, | ||
'${child.runtimeType} (child of $runtimeType) must be disposed before calling super.dispose().', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add a note about this condition to the doc comment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This corresponds to the doc comment
/// Implementations of this method must end with a call to the inherited
/// method, as in `super.dispose()`.
@@ -6221,13 +6221,18 @@ class DefaultAssetBundle extends InheritedWidget { | |||
/// A given render object can be placed at most once in the widget tree. This | |||
/// widget enforces that restriction by keying itself using a [GlobalObjectKey] | |||
/// for the given render object. | |||
/// | |||
/// This widget will call [RenderObject.dispose] on the [renderBox] when it is |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's somewhat surprising that the adaptor takes ownership of the passed in renderBox
and disposes it, no?
I originally expected that you'd be in charge of doing that in onUnmount
if you don't want to reuse it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Maybe also document this in the renderBox
property?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My thought is to keep this as a less-breaking change. Otherwise users will have to update their code to use the unmount property now where they didn't before. Updated the renderBox
property.
Perhaps at some point it would be worth changing this, but my assumption is that most users use this widget as if it will dispose the render object, rather than caching a render object to use repeatedly with this widget.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. I was thinking that maybe the default onUnmount handler would just dispose the renderBox, but if you don't want that you could provide a custom one and make it to do something else (e.g. keep the renderObject alive longer).
But maybe that can be added later. Maybe we should however specifically call out in the doc here that the provided renderBox becomes unusable when the adapter is unmounted from the tree?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added another line explaining that.
/// Called when it is safe to dispose of the render object and its | ||
/// descendants. | ||
/// | ||
/// Typically, this should dispose of any children attached to the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The two paragraphs sound a little contradictory: One the one hand its safe to dispose the render object (first paragraph) on the other hand it must not be disposed (second paragraph)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, I've updated this to try to clarify it and added links to some relevant areas.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
@@ -6221,13 +6221,18 @@ class DefaultAssetBundle extends InheritedWidget { | |||
/// A given render object can be placed at most once in the widget tree. This | |||
/// widget enforces that restriction by keying itself using a [GlobalObjectKey] | |||
/// for the given render object. | |||
/// | |||
/// This widget will call [RenderObject.dispose] on the [renderBox] when it is |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. I was thinking that maybe the default onUnmount handler would just dispose the renderBox, but if you don't want that you could provide a custom one and make it to do something else (e.g. keep the renderObject alive longer).
But maybe that can be added later. Maybe we should however specifically call out in the doc here that the provided renderBox becomes unusable when the adapter is unmounted from the tree?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
…tter#82883)" (flutter#83790) This reverts commit 63c49c3.
When a RenderObjectElement is unmounted, the associated RenderObject should not be retained.
This patch adds a
dispose
method toRenderObject
, which is called inRenderObjectElement.unmount
. It also updatesRenderImage
to use this to dispose of its image, along with updating a single instance where we accessed arenderObject
after callingsuper.unmount()
. I've checked internal repo, and don't see any other instances that this should fail on.This will help with #81514 (because in addition to nulling out a repaing boundary's layer, we'll want to dispose it and whatever pictures it holds) as well as making some cases of #79605 better.
/cc @jacob314
Pre-launch Checklist
///
).If you need help, consider asking for advice on the #hackers-new channel on Discord.