diff --git a/src/Mono.Android/Java.Interop/TypeManager.cs b/src/Mono.Android/Java.Interop/TypeManager.cs index cc0b22936bd..528534a4b13 100644 --- a/src/Mono.Android/Java.Interop/TypeManager.cs +++ b/src/Mono.Android/Java.Interop/TypeManager.cs @@ -181,12 +181,12 @@ static void n_Activate (IntPtr jnienv, IntPtr jclass, IntPtr typename_ptr, IntPt internal static void Activate (IntPtr jobject, ConstructorInfo cinfo, object? []? parms) { try { - var newobj = RuntimeHelpers.GetUninitializedObject (cinfo.DeclaringType!); - if (newobj is IJavaPeerable peer) { - peer.SetPeerReference (new JniObjectReference (jobject)); - } else { - throw new InvalidOperationException ($"Unsupported type: '{newobj}'"); - } + var newobj = GetUninitializedObject (cinfo.DeclaringType!); + var reference = new JniObjectReference (jobject); + JniEnvironment.Runtime.ValueManager.ConstructPeer ( + newobj, + ref reference, + JniObjectReferenceOptions.Copy); cinfo.Invoke (newobj, parms); } catch (Exception e) { var m = FormattableString.Invariant ( @@ -425,15 +425,15 @@ internal static object CreateProxy ( throw new MissingMethodException ( "No constructor found for " + type.FullName + "::.ctor(System.IntPtr, Android.Runtime.JniHandleOwnership)", CreateJavaLocationException ()); + } - static IJavaPeerable GetUninitializedObject ( - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] - Type type) - { - var v = (IJavaPeerable) System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject (type); - v.SetJniManagedPeerState (JniManagedPeerStates.Replaceable | JniManagedPeerStates.Activatable); - return v; - } + static IJavaPeerable GetUninitializedObject ( + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + Type type) + { + var v = (IJavaPeerable) System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject (type); + v.SetJniManagedPeerState (JniManagedPeerStates.Replaceable | JniManagedPeerStates.Activatable); + return v; } public static void RegisterType (string java_class, Type t) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index 2278e272461..371160af317 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -268,14 +268,26 @@ public override void ActivatePeer (IJavaPeerable? self, JniObjectReference refer void ActivateViaReflection (JniObjectReference reference, ConstructorInfo cinfo, object?[]? argumentValues) { var declType = GetDeclaringType (cinfo); + var self = GetUninitializedObject (declType); -#pragma warning disable IL2072 - var self = (IJavaPeerable) System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject (declType); -#pragma warning restore IL2072 - self.SetPeerReference (reference); + // ConstructPeer BEFORE the constructor to create a proper + // global ref and eliminate the race window where bridge + // processing could see a raw local ref. + // See: https://github.com/dotnet/android/issues/11101 + JniEnvironment.Runtime.ValueManager.ConstructPeer ( + self, ref reference, JniObjectReferenceOptions.Copy); cinfo.Invoke (self, argumentValues); + [UnconditionalSuppressMessage ("Trimming", "IL2072", Justification = "Activation constructors are preserved by the runtime typemap.")] + static IJavaPeerable GetUninitializedObject ( + [DynamicallyAccessedMembers (Constructors)] Type type) + { + var value = (IJavaPeerable) System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject (type); + value.SetJniManagedPeerState (JniManagedPeerStates.Replaceable | JniManagedPeerStates.Activatable); + return value; + } + [UnconditionalSuppressMessage ("Trimming", "IL2073", Justification = "🤷‍♂️")] [return: DynamicallyAccessedMembers (Constructors)] Type GetDeclaringType (ConstructorInfo cinfo) => diff --git a/src/Mono.Android/Microsoft.Android.Runtime/SimpleValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/SimpleValueManager.cs index 57d8ef5f84d..df01d621414 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/SimpleValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/SimpleValueManager.cs @@ -228,14 +228,26 @@ public override void ActivatePeer (IJavaPeerable? self, JniObjectReference refer void ActivateViaReflection (JniObjectReference reference, ConstructorInfo cinfo, object?[]? argumentValues) { var declType = GetDeclaringType (cinfo); + var self = GetUninitializedObject (declType); -#pragma warning disable IL2072 - var self = (IJavaPeerable) System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject (declType); -#pragma warning restore IL2072 - self.SetPeerReference (reference); + // ConstructPeer BEFORE the constructor to create a proper + // global ref and eliminate the race window where bridge + // processing could see a raw local ref. + // See: https://github.com/dotnet/android/issues/11101 + JniEnvironment.Runtime.ValueManager.ConstructPeer ( + self, ref reference, JniObjectReferenceOptions.Copy); cinfo.Invoke (self, argumentValues); + [UnconditionalSuppressMessage ("Trimming", "IL2072", Justification = "Activation constructors are preserved by the runtime typemap.")] + static IJavaPeerable GetUninitializedObject ( + [DynamicallyAccessedMembers (Constructors)] Type type) + { + var value = (IJavaPeerable) System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject (type); + value.SetJniManagedPeerState (JniManagedPeerStates.Replaceable | JniManagedPeerStates.Activatable); + return value; + } + [UnconditionalSuppressMessage ("Trimming", "IL2073", Justification = "🤷‍♂️")] [return: DynamicallyAccessedMembers (Constructors)] Type GetDeclaringType (ConstructorInfo cinfo) => diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Widget/CustomWidgetTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Widget/CustomWidgetTests.cs index 78d62576a0e..bcefa10a8c7 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Widget/CustomWidgetTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Widget/CustomWidgetTests.cs @@ -1,4 +1,5 @@ -using Android.App; +using System; +using Android.App; using Android.Content; using Android.Util; using Android.Views; @@ -44,6 +45,44 @@ public void UpperAndLowerCaseCustomWidget_FromLibrary_ShouldNotThrowInflateExcep inflater.Inflate (Resource.Layout.upper_lower_custom, null); }, "Regression test for widgets with uppercase and lowercase namespace (bug #23880) failed."); } + + // https://github.com/dotnet/android/issues/11101 + [Test] + public void InflateCustomView_ShouldNotLeakGlobalRefs () + { + var inflater = (LayoutInflater) Application.Context.GetSystemService (Context.LayoutInflaterService); + Assert.IsNotNull (inflater); + + // Warm up: inflate once to ensure all caches and type mappings are populated + inflater.Inflate (Resource.Layout.lowercase_custom, null); + + CollectGarbage (times: 3); + + int grefBefore = Java.Interop.Runtime.GlobalReferenceCount; + + for (int i = 0; i < 10; i++) { + inflater.Inflate (Resource.Layout.lowercase_custom, null); + } + + CollectGarbage (times: 3); + + int grefAfter = Java.Interop.Runtime.GlobalReferenceCount; + int delta = grefAfter - grefBefore; + + // Each inflate creates a LinearLayout + CustomButton via TypeManager.Activate. + // If global refs are leaking during activation, delta will be >= 10. + // Allow a small delta for noise (cached objects, etc.) + Assert.IsTrue (delta <= 5, + $"Global reference leak detected: {delta} extra global refs after inflating/GC'ing 10 custom views. Before={grefBefore}, After={grefAfter}"); + + static void CollectGarbage (int times) + { + for (int i = 0; i < times; i++) { + GC.Collect (); + GC.WaitForPendingFinalizers (); + } + } + } } public class CustomButton : Button