TL;DR:
It would appear that Android sometimes garbage-collects the Java side of framework peer objects, in our case the Bundle. This results in a NullReferenceException when the Bundle is later used in the .NET code, where it is not eligible for garbage collection.
This contradicts the helpful article that claims that the Xamarin machinery makes sure Java cannot GC an object that is not GC-able on the .NET side.
This started after we refactored the Bundle storage from a public field in a class into a read-only auto property. I wonder if compliler-generated auto-properties interfere with Xamarin reference tracking machinery?
We have a very small and simple Xamarin Android app (non-Forms).
The main activity has a ViewPager where it displays instances of the same fragment, LabelFragment, via an Android.Support.V13.App.FragmentPagerAdapter.
The adapter always assigns Arguments to all fragments it creates in its GetItem.
We have been receiving crash reports recently that clearly show that every now and then, a LabelFragment finds its this.Arguments to be null and crashes with a NullReferenceException in its OnCreate.
Through extensive logging (see the editing history for the full story; not really important), we were able to pinpoint that the fragment arguments are lost in the FragmentPagerAdapter.GetItem() when creating a new fragment:
public override Android.App.Fragment GetItem(int position)
{
PageDescriptor info = m_Tabs[position];
Android.Util.Log.Debug(LOG_TAG, "GetItem({0}), creating t: {1}, a: {2}", position, info.TypeOfFragment.Name, info.FragmentArguments != null);
var f = (Android.App.Fragment)Activator.CreateInstance(info.TypeOfFragment);
f.Arguments = info.FragmentArguments;
Android.Util.Log.Debug(LOG_TAG, "GetItem({0}), created t: {1}, a: {2}", position, f.GetType().Name, f.Arguments != null);
return f;
}
which produced the following logcat in the crash report:
D LabelsAdapter: GetItem(5), creating t: LabelFragment, a: True
D LabelsAdapter: GetItem(5), created t: LabelFragment, a: False
That is, we assign a proven non-null Bundle to f.Arguments, and right after that, f.Arguments is null.
The problem is not easily reproducible. It happens a couple of times a day in an app that is under heavy use, but it does happen regularly.
We are baffled by this and don't know what to do.
Are we doing something blatantly wrong?
Is this a known issue in Android?
Is this a known issue in Xamarin?
How do we work around it?
We also find it suspicious that in all cases of the crash, there is always a specific logcat of garbage collection quoted below.
The very next call to GetItem() after that GC demonstrates the problem. The call does not have to take place immediately after or concurrently with the GC run, just some time after (even minutes after).
Timestamps edited for brevity.
10:46:18 I ame.oursmallap: Explicit concurrent copying GC freed 73676(5134KB) AllocSpace objects, 4(84KB) LOS objects, 49% free, 7529KB/14MB, paused 98us total 51.603ms
10:46:18 W System : A resource failed to call close.
10:46:18 I chatty : uid=10146(com.companyname.oursmallapp) FinalizerDaemon identical 39 lines
10:46:18 W System : A resource failed to call close.
10:46:18 W System : A resource failed to call close.
10:46:18 I chatty : uid=10146(com.companyname.oursmallapp) FinalizerDaemon identical 6 lines
10:46:18 W System : A resource failed to call close.
10:47:19 D LabelsAdapter: GetItem(5), creating t: LabelFragment, a: True
10:47:19 D LabelsAdapter: GetItem(5), created t: LabelFragment, a: False
EDIT:
It would appear our hunch about the GC logcat was correct.
We have been able to reproduce the problem by randomly calling Dispose on our stored Bundle instances that we assign to Fragments (yes, we create the bundles in advance, and only much later the Fragment instances).
The fragment's Arguments property is not a dumb get-setter:
public unsafe Bundle Arguments
{
[Register("getArguments", "()Landroid/os/Bundle;", "GetGetArgumentsHandler")] get => Java.Lang.Object.GetObject<Bundle>(Fragment._members.InstanceMethods.InvokeNonvirtualObjectMethod("getArguments.()Landroid/os/Bundle;", (IJavaPeerable) this, (JniArgumentValue*) null).Handle, JniHandleOwnership.TransferLocalRef);
[Register("setArguments", "(Landroid/os/Bundle;)V", "GetSetArguments_Landroid_os_Bundle_Handler")] set
{
JniArgumentValue* parameters = stackalloc JniArgumentValue[1];
parameters[0] = new JniArgumentValue(value == null ? IntPtr.Zero : value.Handle);
Fragment._members.InstanceMethods.InvokeVirtualVoidMethod("setArguments.(Landroid/os/Bundle;)V", (IJavaPeerable) this, parameters);
}
}
It will return null when the link is broken between the .NET wrapper and the Java object, which is when the Java object is garbage collected.
The question then is, how could Java garbage-collect an object that still has references on the .NET side? (We are storing them in a .NET list.) The GC article claims that it is not possible.
Now that I think about it, the only change (after which this started happening) was that we refactored a field in which the Bundle was stored into a read-only auto property. I wonder if the implementation of auto properties is not compatible with the Xamarin reference-preservation mechanisms?