-
Notifications
You must be signed in to change notification settings - Fork 569
Fix JNI global ref leak in TypeManager.Activate and ValueManager.ActivateViaReflection #11111
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
Changes from all commits
15fa01d
65c7a53
7debed6
527cb22
00e04ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| using System; | ||
| using Android.App; | ||
| using Android.Content; | ||
| using Android.Runtime; | ||
| using Android.Util; | ||
| using Android.Views; | ||
| using Java.Interop; | ||
| using NUnit.Framework; | ||
|
|
||
| namespace Xamarin.Android.RuntimeTests; | ||
|
|
||
| // https://github.com/dotnet/android/issues/11101 | ||
| [TestFixture] | ||
| public class InflatedCustomViewTests | ||
| { | ||
| [Test] | ||
| public void InflatedCustomView_HasValidPeerReference () | ||
| { | ||
| var inflater = LayoutInflater.From (Application.Context)!; | ||
| var layout = inflater.Inflate (Resource.Layout.inflated_custom_view, null, false)!; | ||
|
|
||
|
Comment on lines
+19
to
+21
|
||
| // Find our custom view in the inflated layout | ||
| var customView = FindCustomView (layout); | ||
|
|
||
| Assert.IsNotNull (customView, "Custom view should be found in inflated layout"); | ||
|
|
||
| // After inflation via Java-initiated activation, the peer should have a | ||
| // properly managed global JNI reference, not a raw local ref with Invalid type. | ||
| var peerRef = customView!.PeerReference; | ||
| Assert.IsTrue (peerRef.IsValid, "PeerReference should be valid"); | ||
|
Comment on lines
+29
to
+30
|
||
| Assert.AreNotEqual ( | ||
| JniObjectReferenceType.Invalid, | ||
| peerRef.Type, | ||
| "PeerReference.Type should not be Invalid — it should be a Global ref"); | ||
|
|
||
| // The peer should be registered so PeekObject can find it | ||
| var peeked = Java.Lang.Object.PeekObject (customView.Handle); | ||
| Assert.IsNotNull (peeked, "PeekObject should find the registered peer"); | ||
| Assert.AreSame (customView, peeked, "PeekObject should return the same instance"); | ||
| } | ||
|
|
||
| [Test] | ||
| public void InflatedCustomView_CanBeCollected () | ||
| { | ||
| WeakReference? weakRef = null; | ||
|
|
||
| // Create and discard the inflated view on a separate thread | ||
| // to avoid any local variable keeping it alive | ||
| var t = new System.Threading.Thread (() => { | ||
| var inflater = LayoutInflater.From (Application.Context)!; | ||
| var layout = inflater.Inflate (Resource.Layout.inflated_custom_view, null, false)!; | ||
| var customView = FindCustomView (layout); | ||
| Assert.IsNotNull (customView, "Custom view should be found in inflated layout"); | ||
| weakRef = new WeakReference (customView); | ||
| }); | ||
| t.Start (); | ||
| t.Join (); | ||
|
|
||
| // Force GC + bridge processing | ||
| GC.Collect (); | ||
| GC.WaitForPendingFinalizers (); | ||
| GC.Collect (); | ||
| GC.WaitForPendingFinalizers (); | ||
|
|
||
| Assert.IsNotNull (weakRef, "WeakReference should have been created"); | ||
| Assert.IsFalse (weakRef!.IsAlive, | ||
| "Custom view should be collected after GC — if it's still alive, there is a memory leak (https://github.com/dotnet/android/issues/11101)"); | ||
|
Comment on lines
+65
to
+67
|
||
| } | ||
|
|
||
| // Stress test: repeated inflation + GC to trigger the race condition | ||
| // between Activate.SetPeerReference and ConstructPeer. Under the bug, | ||
| // each race hit leaks a JNI global ref, so gref count grows unboundedly. | ||
| [Test] | ||
| public void InflatedCustomView_RepeatedInflation_DoesNotLeakGlobalRefs () | ||
| { | ||
| int initialGrefCount = Java.Interop.JniEnvironment.Runtime.GlobalReferenceCount; | ||
|
|
||
| for (int i = 0; i < 50; i++) { | ||
| var t = new System.Threading.Thread (() => { | ||
| var inflater = LayoutInflater.From (Application.Context)!; | ||
| inflater.Inflate (Resource.Layout.inflated_custom_view, null, false); | ||
| }); | ||
| t.Start (); | ||
| t.Join (); | ||
| } | ||
|
|
||
| GC.Collect (); | ||
| GC.WaitForPendingFinalizers (); | ||
| GC.Collect (); | ||
| GC.WaitForPendingFinalizers (); | ||
|
|
||
| int finalGrefCount = Java.Interop.JniEnvironment.Runtime.GlobalReferenceCount; | ||
|
|
||
| // Allow some tolerance — other code may allocate/release grefs. | ||
| // The key assertion is that gref count doesn't grow proportionally | ||
| // to the number of inflations (under the bug, each inflation | ||
| // leaks ~1 gref, so 50 inflations would leak ~50 grefs). | ||
| int leaked = finalGrefCount - initialGrefCount; | ||
| Assert.Less (leaked, 10, | ||
| $"Global reference count grew by {leaked} after 50 inflations — " + | ||
| $"expected near-zero growth after GC (initial={initialGrefCount}, final={finalGrefCount})"); | ||
| } | ||
|
|
||
| static InflatedCustomView? FindCustomView (View root) | ||
| { | ||
| if (root is InflatedCustomView customView) | ||
| return customView; | ||
|
|
||
| if (root is ViewGroup viewGroup) { | ||
| for (int i = 0; i < viewGroup.ChildCount; i++) { | ||
| var child = viewGroup.GetChildAt (i); | ||
| if (child is InflatedCustomView found) | ||
| return found; | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
| } | ||
|
|
||
| // A simple custom view that can be inflated from XML | ||
| public sealed class InflatedCustomView : View | ||
| { | ||
| public InflatedCustomView (Context? context) : base (context) { } | ||
| public InflatedCustomView (nint javaReference, JniHandleOwnership transfer) : base (javaReference, transfer) { } | ||
| public InflatedCustomView (Context? context, IAttributeSet? attrs) : base (context, attrs) { } | ||
| public InflatedCustomView (Context? context, IAttributeSet? attrs, int defStyleAttr) : base (context, attrs, defStyleAttr) { } | ||
| public InflatedCustomView (Context? context, IAttributeSet? attrs, int defStyleAttr, int defStyleRes) : base (context, attrs, defStyleAttr, defStyleRes) { } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <FrameLayout | ||
| xmlns:android="http://schemas.android.com/apk/res/android" | ||
| android:layout_width="match_parent" | ||
| android:layout_height="match_parent"> | ||
|
|
||
| <Xamarin.Android.RuntimeTests.InflatedCustomView | ||
| android:layout_width="match_parent" | ||
| android:layout_height="match_parent" /> | ||
|
|
||
| </FrameLayout> |
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.
peeris declared in the pattern match (if (newobj is IJavaPeerable peer)) and is out of scope after theifblock. The logging aftercinfo.Invoke()referencespeer, which will not compile. Move the post-ctor log inside theifblock, or hoist a separateIJavaPeerable peervariable outside theifand assign it before invoking the constructor.