Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
ac8e1f8f
Unverified
Commit
ac8e1f8f
authored
Apr 08, 2021
by
Michael Goderbauer
Committed by
GitHub
Apr 08, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Reduce severity of memory leak when BuildContext/Element is retained (#79957)
parent
6db88907
Changes
5
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
170 additions
and
19 deletions
+170
-19
framework.dart
packages/flutter/lib/src/widgets/framework.dart
+36
-12
widget_inspector.dart
packages/flutter/lib/src/widgets/widget_inspector.dart
+21
-3
framework_test.dart
packages/flutter/test/widgets/framework_test.dart
+16
-0
widget_inspector_test.dart
packages/flutter/test/widgets/widget_inspector_test.dart
+90
-4
widget_inspector_test_utils.dart
...ges/flutter/test/widgets/widget_inspector_test_utils.dart
+7
-0
No files found.
packages/flutter/lib/src/widgets/framework.dart
View file @
ac8e1f8f
...
...
@@ -3085,8 +3085,21 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
/// The configuration for this element.
@override
Widget
get
widget
=>
_widget
;
Widget
_widget
;
Widget
get
widget
=>
_widget
!;
Widget
?
_widget
;
/// Returns true if the Element is defunct.
///
/// This getter always returns false in profile and release builds.
/// See the lifecycle documentation for [Element] for additional information.
bool
get
debugIsDefunct
{
bool
isDefunct
=
false
;
assert
(()
{
isDefunct
=
_lifecycleState
==
_ElementLifecycle
.
defunct
;
return
true
;
}());
return
isDefunct
;
}
/// The object that manages the lifecycle of this element.
@override
...
...
@@ -3801,10 +3814,14 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
assert
(
depth
!=
null
);
assert
(
owner
!=
null
);
// Use the private property to avoid a CastError during hot reload.
final
Key
?
key
=
_widget
.
key
;
final
Key
?
key
=
_widget
!
.
key
;
if
(
key
is
GlobalKey
)
{
owner
!.
_unregisterGlobalKey
(
key
,
this
);
}
// Release resources to reduce the severity of memory leaks caused by
// defunct, but accidentally retained Elements.
_widget
=
null
;
_dependencies
=
null
;
_lifecycleState
=
_ElementLifecycle
.
defunct
;
}
...
...
@@ -4116,7 +4133,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
/// A short, textual description of this element.
@override
String
toStringShort
()
=>
widget
.
toStringShort
()
;
String
toStringShort
()
=>
_widget
?.
toStringShort
()
??
'
${describeIdentity(this)}
(DEFUNCT)'
;
@override
DiagnosticsNode
toDiagnosticsNode
({
String
?
name
,
DiagnosticsTreeStyle
?
style
})
{
...
...
@@ -4134,9 +4151,9 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
if
(
_lifecycleState
!=
_ElementLifecycle
.
initial
)
{
properties
.
add
(
ObjectFlagProperty
<
int
>(
'depth'
,
depth
,
ifNull:
'no depth'
));
}
properties
.
add
(
ObjectFlagProperty
<
Widget
>(
'widget'
,
widget
,
ifNull:
'no widget'
));
properties
.
add
(
DiagnosticsProperty
<
Key
>(
'key'
,
widget
.
key
,
showName:
false
,
defaultValue:
null
,
level:
DiagnosticLevel
.
hidden
));
widget
.
debugFillProperties
(
properties
);
properties
.
add
(
ObjectFlagProperty
<
Widget
>(
'widget'
,
_
widget
,
ifNull:
'no widget'
));
properties
.
add
(
DiagnosticsProperty
<
Key
>(
'key'
,
_widget
?
.
key
,
showName:
false
,
defaultValue:
null
,
level:
DiagnosticLevel
.
hidden
));
_widget
?
.
debugFillProperties
(
properties
);
properties
.
add
(
FlagProperty
(
'dirty'
,
value:
dirty
,
ifTrue:
'dirty'
));
if
(
_dependencies
!=
null
&&
_dependencies
!.
isNotEmpty
)
{
final
List
<
DiagnosticsNode
>
diagnosticsDependencies
=
_dependencies
!
...
...
@@ -4296,7 +4313,9 @@ class _ElementDiagnosticableTreeNode extends DiagnosticableTreeNode {
Map
<
String
,
Object
?>
toJsonMap
(
DiagnosticsSerializationDelegate
delegate
)
{
final
Map
<
String
,
Object
?>
json
=
super
.
toJsonMap
(
delegate
);
final
Element
element
=
value
as
Element
;
if
(!
element
.
debugIsDefunct
)
{
json
[
'widgetRuntimeType'
]
=
element
.
widget
.
runtimeType
.
toString
();
}
json
[
'stateful'
]
=
stateful
;
return
json
;
}
...
...
@@ -4660,7 +4679,7 @@ class StatelessElement extends ComponentElement {
class
StatefulElement
extends
ComponentElement
{
/// Creates an element that uses the given widget as its configuration.
StatefulElement
(
StatefulWidget
widget
)
:
state
=
widget
.
createState
(),
:
_
state
=
widget
.
createState
(),
super
(
widget
)
{
assert
(()
{
if
(!
state
.
_debugTypesAreRight
(
widget
))
{
...
...
@@ -4695,7 +4714,8 @@ class StatefulElement extends ComponentElement {
/// There is a one-to-one relationship between [State] objects and the
/// [StatefulElement] objects that hold them. The [State] objects are created
/// by [StatefulElement] in [mount].
final
State
<
StatefulWidget
>
state
;
State
<
StatefulWidget
>
get
state
=>
_state
!;
State
<
StatefulWidget
>?
_state
;
@override
void
reassemble
()
{
...
...
@@ -4810,6 +4830,9 @@ class StatefulElement extends ComponentElement {
]);
}());
state
.
_element
=
null
;
// Release resources to reduce the severity of memory leaks caused by
// defunct, but accidentally retained Elements.
_state
=
null
;
}
@override
...
...
@@ -4895,7 +4918,7 @@ class StatefulElement extends ComponentElement {
@override
void
debugFillProperties
(
DiagnosticPropertiesBuilder
properties
)
{
super
.
debugFillProperties
(
properties
);
properties
.
add
(
DiagnosticsProperty
<
State
<
StatefulWidget
>>(
'state'
,
state
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
State
<
StatefulWidget
>>(
'state'
,
_
state
,
defaultValue:
null
));
}
}
...
...
@@ -5705,13 +5728,14 @@ abstract class RenderObjectElement extends Element {
@override
void
unmount
()
{
final
RenderObjectWidget
oldWidget
=
widget
;
super
.
unmount
();
assert
(
!
renderObject
.
attached
,
'A RenderObject was still attached when attempting to unmount its '
'RenderObjectElement:
$renderObject
'
,
);
w
idget
.
didUnmountRenderObject
(
renderObject
);
oldW
idget
.
didUnmountRenderObject
(
renderObject
);
}
void
_updateParentData
(
ParentDataWidget
<
ParentData
>
parentDataWidget
)
{
...
...
packages/flutter/lib/src/widgets/widget_inspector.dart
View file @
ac8e1f8f
...
...
@@ -1205,6 +1205,7 @@ mixin WidgetInspectorService {
///
/// Use this method only for testing to ensure that object references from one
/// test case do not impact other test cases.
@visibleForTesting
@protected
void
disposeAllGroups
()
{
_groups
.
clear
();
...
...
@@ -1213,6 +1214,19 @@ mixin WidgetInspectorService {
_nextId
=
0
;
}
/// Reset all InspectorService state.
///
/// Use this method only for testing to write hermetic tests for
/// WidgetInspectorService.
@visibleForTesting
@protected
@mustCallSuper
void
resetAllState
()
{
disposeAllGroups
();
selection
.
clear
();
setPubRootDirectories
(<
String
>[]);
}
/// Free all references to objects in a group.
///
/// Objects and their associated ids in the group may be kept alive by
...
...
@@ -2413,7 +2427,8 @@ class InspectorSelection {
/// Setting [candidates] or calling [clear] resets the selection.
///
/// Returns null if the selection is invalid.
RenderObject
?
get
current
=>
_current
;
RenderObject
?
get
current
=>
active
?
_current
:
null
;
RenderObject
?
_current
;
set
current
(
RenderObject
?
value
)
{
if
(
_current
!=
value
)
{
...
...
@@ -2427,7 +2442,10 @@ class InspectorSelection {
/// Setting [candidates] or calling [clear] resets the selection.
///
/// Returns null if the selection is invalid.
Element
?
get
currentElement
=>
_currentElement
;
Element
?
get
currentElement
{
return
_currentElement
?.
debugIsDefunct
??
true
?
null
:
_currentElement
;
}
Element
?
_currentElement
;
set
currentElement
(
Element
?
element
)
{
if
(
currentElement
!=
element
)
{
...
...
@@ -3039,7 +3057,7 @@ String? _describeCreationLocation(Object object) {
///
/// Currently creation locations are only available for [Widget] and [Element].
_Location
?
_getCreationLocation
(
Object
?
object
)
{
final
Object
?
candidate
=
object
is
Element
?
object
.
widget
:
object
;
final
Object
?
candidate
=
object
is
Element
&&
!
object
.
debugIsDefunct
?
object
.
widget
:
object
;
return
candidate
is
_HasCreationLocation
?
candidate
.
_location
:
null
;
}
...
...
packages/flutter/test/widgets/framework_test.dart
View file @
ac8e1f8f
...
...
@@ -1572,6 +1572,22 @@ void main() {
await
tester
.
pumpWidget
(
Container
());
expect
(
tester
.
binding
.
buildOwner
!.
globalKeyCount
,
initialCount
+
0
);
});
testWidgets
(
'Widget and State properties are nulled out when unmounted'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
_StatefulLeaf
());
final
StatefulElement
element
=
tester
.
element
<
StatefulElement
>(
find
.
byType
(
_StatefulLeaf
));
expect
(
element
.
state
,
isA
<
State
<
_StatefulLeaf
>>());
expect
(
element
.
widget
,
isA
<
_StatefulLeaf
>());
// Replace the widget tree to unmount the element.
await
tester
.
pumpWidget
(
Container
());
// Accessing state/widget now throws a CastError because they have been
// nulled out to reduce severity of memory leaks when an Element (e.g. in
// the form of a BuildContext) is retained past its useful life. See also
// https://github.com/flutter/flutter/issues/79605 for examples why this may
// occur.
expect
(()
=>
element
.
state
,
throwsA
(
isA
<
TypeError
>()));
expect
(()
=>
element
.
widget
,
throwsA
(
isA
<
TypeError
>()));
},
skip:
kIsWeb
);
}
class
_WidgetWithNoVisitChildren
extends
StatelessWidget
{
...
...
packages/flutter/test/widgets/widget_inspector_test.dart
View file @
ac8e1f8f
...
...
@@ -233,6 +233,10 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
final
TestWidgetInspectorService
service
=
TestWidgetInspectorService
();
WidgetInspectorService
.
instance
=
service
;
tearDown
(()
{
service
.
resetAllState
();
});
testWidgets
(
'WidgetInspector smoke test'
,
(
WidgetTester
tester
)
async
{
// This is a smoke test to verify that adding the inspector doesn't crash.
await
tester
.
pumpWidget
(
...
...
@@ -759,6 +763,61 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
expect
(
service
.
selection
.
currentElement
,
equals
(
elementA
));
});
testWidgets
(
'WidgetInspectorService defunct selection regression test'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
Stack
(
children:
const
<
Widget
>[
Text
(
'a'
,
textDirection:
TextDirection
.
ltr
),
],
),
),
);
final
Element
elementA
=
find
.
text
(
'a'
).
evaluate
().
first
;
service
.
setSelection
(
elementA
);
expect
(
service
.
selection
.
currentElement
,
equals
(
elementA
));
expect
(
service
.
selection
.
current
,
equals
(
elementA
.
renderObject
));
await
tester
.
pumpWidget
(
const
SizedBox
(
child:
Text
(
'b'
,
textDirection:
TextDirection
.
ltr
),
),
);
// Selection is now empty as the element is defunct.
expect
(
service
.
selection
.
currentElement
,
equals
(
null
));
expect
(
service
.
selection
.
current
,
equals
(
null
));
// Verify that getting the debug creation location of the defunct element
// does not crash.
expect
(
debugIsLocalCreationLocation
(
elementA
),
isFalse
);
// Verify that generating json for a defunct element does not crash.
expect
(
elementA
.
toDiagnosticsNode
().
toJsonMap
(
InspectorSerializationDelegate
(
service:
service
,
summaryTree:
false
,
includeProperties:
true
,
addAdditionalPropertiesCallback:
null
,
),
),
isNotNull
,
);
final
Element
elementB
=
find
.
text
(
'b'
).
evaluate
().
first
;
service
.
setSelection
(
elementB
);
expect
(
service
.
selection
.
currentElement
,
equals
(
elementB
));
expect
(
service
.
selection
.
current
,
equals
(
elementB
.
renderObject
));
// Set selection back to a defunct element.
service
.
setSelection
(
elementA
);
expect
(
service
.
selection
.
currentElement
,
equals
(
null
));
expect
(
service
.
selection
.
current
,
equals
(
null
));
});
testWidgets
(
'WidgetInspectorService getParentChain'
,
(
WidgetTester
tester
)
async
{
const
String
group
=
'test-group'
;
...
...
@@ -934,6 +993,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
),
);
final
Element
elementA
=
find
.
text
(
'a'
).
evaluate
().
first
;
service
.
setSelection
(
elementA
,
'my-group'
);
late
String
pubRootTest
;
if
(
widgetTracked
)
{
final
Map
<
String
,
Object
?>
jsonObject
=
json
.
decode
(
...
...
@@ -1047,6 +1107,8 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
activeDevToolsServerAddress
=
'http://127.0.0.1:9100'
;
connectedVmServiceUri
=
'http://127.0.0.1:55269/798ay5al_FM=/'
;
setupDefaultPubRootDirectory
(
service
);
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
...
...
@@ -1079,6 +1141,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
testWidgets
(
'test transformDebugCreator will not add DevToolsDeepLinkProperty for non-overflow errors'
,
(
WidgetTester
tester
)
async
{
activeDevToolsServerAddress
=
'http://127.0.0.1:9100'
;
connectedVmServiceUri
=
'http://127.0.0.1:55269/798ay5al_FM=/'
;
setupDefaultPubRootDirectory
(
service
);
await
tester
.
pumpWidget
(
Directionality
(
...
...
@@ -1110,6 +1173,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
testWidgets
(
'test transformDebugCreator will not add DevToolsDeepLinkProperty if devtoolsServerAddress is unavailable'
,
(
WidgetTester
tester
)
async
{
activeDevToolsServerAddress
=
null
;
connectedVmServiceUri
=
'http://127.0.0.1:55269/798ay5al_FM=/'
;
setupDefaultPubRootDirectory
(
service
);
await
tester
.
pumpWidget
(
Directionality
(
...
...
@@ -2782,6 +2846,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
),
),
);
service
.
setSelection
(
find
.
text
(
'Hello, World!'
).
evaluate
().
first
,
'my-group'
);
// Figure out the pubRootDirectory
final
Map
<
String
,
Object
?>
jsonObject
=
(
await
service
.
testExtension
(
...
...
@@ -2855,11 +2920,14 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
final
Map
<
String
,
Object
>
additionalJson
=
<
String
,
Object
>{};
final
Object
?
value
=
node
.
value
;
if
(
value
is
Element
)
{
final
RenderObject
?
renderObject
=
value
.
renderObject
;
if
(
renderObject
!=
null
)
{
additionalJson
[
'renderObject'
]
=
value
.
renderObject
!
.
toDiagnosticsNode
().
toJsonMap
(
renderObject
.
toDiagnosticsNode
().
toJsonMap
(
delegate
.
copyWith
(
subtreeDepth:
0
),
);
}
}
additionalJson
[
'callbackExecuted'
]
=
true
;
return
additionalJson
;
},
...
...
@@ -2892,6 +2960,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
});
testWidgets
(
'debugIsLocalCreationLocation test'
,
(
WidgetTester
tester
)
async
{
setupDefaultPubRootDirectory
(
service
);
final
GlobalKey
key
=
GlobalKey
();
...
...
@@ -2953,6 +3022,23 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
);
});
}
static
void
setupDefaultPubRootDirectory
(
TestWidgetInspectorService
service
)
{
final
Map
<
String
,
Object
?>
jsonObject
=
const
SizedBox
().
toDiagnosticsNode
().
toJsonMap
(
InspectorSerializationDelegate
(
service:
service
));
final
Map
<
String
,
Object
?>
creationLocation
=
jsonObject
[
'creationLocation'
]!
as
Map
<
String
,
Object
?>;
expect
(
creationLocation
,
isNotNull
);
final
String
file
=
creationLocation
[
'file'
]!
as
String
;
expect
(
file
,
endsWith
(
'widget_inspector_test.dart'
));
final
List
<
String
>
segments
=
Uri
.
parse
(
file
)
.
pathSegments
;
final
String
pubRootTest
=
'/'
+
segments
.
take
(
segments
.
length
-
2
).
join
(
'/'
);
// Strip a couple subdirectories away to generate a plausible pub root
// directory.
service
.
setPubRootDirectories
(<
String
>[
pubRootTest
]);
}
}
void
addToKnownLocationsMap
(
{
...
...
packages/flutter/test/widgets/widget_inspector_test_utils.dart
View file @
ac8e1f8f
...
...
@@ -62,4 +62,11 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
binding
.
buildOwner
!.
reassemble
(
binding
.
renderViewElement
!);
}
}
@override
void
resetAllState
()
{
super
.
resetAllState
();
eventsDispatched
.
clear
();
rebuildCount
=
0
;
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment