diff --git a/osu.Android/Linker.xml b/osu.Android/Linker.xml
index 6e1c6b2038b5..3325da1c3c4f 100644
--- a/osu.Android/Linker.xml
+++ b/osu.Android/Linker.xml
@@ -104,13 +104,16 @@
"to silence the warnings" allows ILLink to freely trim JNI-only constructors
from AndroidGameActivity, crashing the app on launch.
- ADDITIONAL DEFENCE (v2026.508.227+): OsuGameActivity now also declares its own
- explicit (IntPtr, JniHandleOwnership) constructor that chains to base. Because
- osu.Android is unambiguously preserve="all" and is always resolvable at
- descriptor-processing time, this makes the JNI-activation constructor available
- in the concrete type regardless of whether the IL2007-deferred rule for
- osu.Framework.Android is honoured. The v2026.508.226 field crash confirmed
- that the deferred rule alone is not always sufficient. -->
+ ADDITIONAL DEFENCE (v2026.508.228+): OsuGameActivity carries a [DynamicDependency]
+ attribute that explicitly roots AndroidGameActivity's constructors from the
+ osu.Android assembly. Because osu.Android is unambiguously preserve="all" and is
+ always resolvable at descriptor-processing time, ILLink processes this root
+ reference before it can trim the JNI-activation constructor, regardless of whether
+ the IL2007-deferred rule for osu.Framework.Android is honoured. This supersedes
+ the v2026.508.227 approach (explicit OsuGameActivity(IntPtr, JniHandleOwnership)
+ constructor) which did not compile because AndroidGameActivity does not expose an
+ explicit (IntPtr, JniHandleOwnership) constructor — only its SDLActivity grandparent
+ does, and C# base() invocations resolve only against the direct parent class. -->
diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs
index 8689971de8ec..54f3f8beb9b3 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -15,6 +15,7 @@
using System.Threading;
using System.Threading.Tasks;
using System;
+using System.Diagnostics.CodeAnalysis;
using Uri = Android.Net.Uri;
using ManagedBass; // Required for Bass.AndroidAAudio + Bass.DevicePeriod startup init (FLAG_BASS_AAUDIO_ENABLED path in OnCreate)
using osu.Android.Input;
@@ -88,19 +89,13 @@ protected override osu.Framework.Game CreateGame()
return game;
}
- // JNI activation constructor. Android's TypeManager.Activate resolves this
- // constructor via reflection when recreating the managed wrapper from a JNI
- // handle (e.g. after process restore or across configuration changes). Without
- // an explicit declaration here the linker might only find the inherited version
- // in osu.Framework.Android — an assembly that ILLink cannot resolve during
- // descriptor processing (IL2007), making the preserve="all" rule unreliable for
- // inherited members. Declaring it in osu.Android (which is unambiguously
- // preserve="all") guarantees the constructor survives trimming.
- protected OsuGameActivity(IntPtr handle, JniHandleOwnership transfer)
- : base(handle, transfer)
- {
- }
-
+ // Preserve AndroidGameActivity's (IntPtr, JniHandleOwnership) JNI-activation constructor.
+ // The [assembly: preserve="all"] descriptor for osu.Framework.Android in Linker.xml is deferred
+ // (IL2007) because the framework assembly is not yet in ILLink's search path at descriptor time.
+ // Placing [DynamicDependency] on this constructor (which lives in osu.Android — always resolvable)
+ // roots the AndroidGameActivity constructor in ILLink's walk before it can be trimmed away.
+ [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors,
+ "osu.Framework.Android.AndroidGameActivity", "osu.Framework.Android")]
public OsuGameActivity()
{
game = new OsuGameAndroid(this);