Summary
Under the trimmable typemap (NativeAOT), R8 shrinking removes user-authored AndroidJavaSource Java classes that aren't referenced from the managed dependency graph. This breaks multidex behavior and, more importantly, can break real apps that call user Java via JNI/reflection (which ILC's managed graph can't see).
Reproduced via BuildAfterMultiDexIsNotRequired(NativeAOT):
Expected: file exists
But was: ".../obj/Release/android/bin/classes2.dex"
Diagnosis
CreateMultiDexRequiredApplication adds two AndroidJavaSource files (ManyMethods.java, ManyMethods2.java), each with 32768 methods (Bind=False, unreferenced) → 65536 methods > the 64K dex limit → must produce classes2.dex.
Local repro (NativeAOT, arm64):
- The Java is compiled:
obj/Release/.../android/bin/classes/ManyMethods.class is 1.7 MB each.
- The classes zip that D8/R8 consumes (
classes.zip, 3.9 MB) contains ManyMethods.class / ManyMethods2.class.
- But the produced
classes.dex is only ~275 KB and contains none of those methods, and no classes2.dex is produced.
Because AndroidEnableMultiDex=True selects R8 (Xamarin.Android.D8.targets, _UseR8), and _R8EnableShrinking=True for AndroidLinkTool=r8, R8 shrinks the unreferenced ManyMethods classes away. The trimmable NativeAOT proguard config (GenerateNativeAotProguardConfiguration, built from the ILC DGML + acw-map) keeps ACW/JCW types and android.R/android.Manifest, but not user AndroidJavaSource classes — so R8 drops them.
The legacy/CoreCLR path keeps them (the CoreCLR variant of the test passes).
Why this matters beyond the test
AndroidJavaSource (Bind=False) classes are user-authored Java, often invoked from native/JNI or reflection. R8 shrinking them based on the managed reachability graph is unsound and can silently remove Java the app needs at runtime.
Fix direction (needs design)
- Keep user
AndroidJavaSource-derived classes in the trimmable NativeAOT proguard config (emit -keep for them), or
- Don't apply R8 shrinking to the user Java side under NativeAOT (the managed side is already trimmed by ILC; the Java side's reachability isn't captured by the managed graph), or
- Treat the app's own compiled Java classes as kept program classes.
Acceptance criteria
References
- Repro test:
BuildAfterMultiDexIsNotRequired (src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs), CreateMultiDexRequiredApplication.
- Code:
src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets (_UseR8, _R8EnableShrinking), src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets (GenerateNativeAotProguardConfiguration).
Summary
Under the trimmable typemap (NativeAOT), R8 shrinking removes user-authored
AndroidJavaSourceJava classes that aren't referenced from the managed dependency graph. This breaks multidex behavior and, more importantly, can break real apps that call user Java via JNI/reflection (which ILC's managed graph can't see).Reproduced via
BuildAfterMultiDexIsNotRequired(NativeAOT):Diagnosis
CreateMultiDexRequiredApplicationadds twoAndroidJavaSourcefiles (ManyMethods.java,ManyMethods2.java), each with 32768 methods (Bind=False, unreferenced) → 65536 methods > the 64K dex limit → must produceclasses2.dex.Local repro (NativeAOT, arm64):
obj/Release/.../android/bin/classes/ManyMethods.classis 1.7 MB each.classes.zip, 3.9 MB) containsManyMethods.class/ManyMethods2.class.classes.dexis only ~275 KB and contains none of those methods, and noclasses2.dexis produced.Because
AndroidEnableMultiDex=Trueselects R8 (Xamarin.Android.D8.targets,_UseR8), and_R8EnableShrinking=TrueforAndroidLinkTool=r8, R8 shrinks the unreferencedManyMethodsclasses away. The trimmable NativeAOT proguard config (GenerateNativeAotProguardConfiguration, built from the ILC DGML + acw-map) keeps ACW/JCW types andandroid.R/android.Manifest, but not userAndroidJavaSourceclasses — so R8 drops them.The legacy/CoreCLR path keeps them (the
CoreCLRvariant of the test passes).Why this matters beyond the test
AndroidJavaSource(Bind=False) classes are user-authored Java, often invoked from native/JNI or reflection. R8 shrinking them based on the managed reachability graph is unsound and can silently remove Java the app needs at runtime.Fix direction (needs design)
AndroidJavaSource-derived classes in the trimmable NativeAOT proguard config (emit-keepfor them), orAcceptance criteria
BuildAfterMultiDexIsNotRequired(NativeAOT)passes (classes2.dexproduced when required, absent when not).AndroidJavaSourceclasses survive into the dex on NativeAOT.References
BuildAfterMultiDexIsNotRequired(src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs),CreateMultiDexRequiredApplication.src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets(_UseR8,_R8EnableShrinking),src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets(GenerateNativeAotProguardConfiguration).