Skip to content

Enable effective R8 trimming and obfuscation for .NET Android apps #12814

Description

@simonrozsival

Android framework version

  • net11.0-android (Preview)
  • Future supported .NET for Android releases

Affected platform version

Current main / .NET 11 development builds. This is an implementation umbrella rather than a released-product regression.

Description

Enable R8 to safely remove, optimize, and obfuscate Java bytecode in trimmed .NET for Android applications, including MAUI applications.

Today, D8-only builds can package most of the transitive AndroidX, Material, Kotlin, and generated Java closure. Enabling R8 provides a large initial reduction, but the current conservative JNI naming policy prevents R8 from applying its full shrinking and obfuscation capabilities. Managed trimming and Java shrinking must also agree: when ILLink or NativeAOT removes managed C# code, the corresponding Java Callable Wrappers (JCWs), R8 roots, DEX classes, and runtime mapping entries must be removed as well.

The intended pipeline is:

C# compilation
  -> ILLink or NativeAOT reachability
  -> regenerate/prune typemap Java wrappers from surviving managed code
  -> javac
  -> one final R8 shrink/optimize/obfuscate pass
  -> convert final mapping.txt into compact JNI remapping tables
  -> native link and package

The runtime-remapping design keeps managed assemblies using their original JNI identifiers and translates lookups to R8's final identifiers at runtime. It requires:

  • Forward type remapping for managed-to-Java lookup.
  • Reverse type remapping for Java-object-to-managed-type activation through the trimmable typemap.
  • Method, constructor, field, and descriptor remapping.
  • Inherited member fallback.
  • Composition with existing remap inputs such as MAM/Intune mappings.
  • CoreCLR selection of required remaps from post-ILLink assemblies.
  • NativeAOT selection of required remaps from post-ILC JNI literals, followed by per-RID late native linking.
  • Correct clean and incremental behavior so stale .java, .class, DEX, ProGuard, or remapping outputs cannot survive.

A prototype based on #12795 and #12796 produced the following decoded DEX results using the repository's Xamarin.Forms-like CoreCLR Release size fixture:

Mode Decoded DEX Classes
No R8 9,534,068 bytes 7,352
R8 private-members 2,794,592 bytes 3,636
R8 runtime-remapping with trimmable typemap 2,047,188 bytes 2,365

This is a 78.5% DEX reduction and 67.8% class-count reduction relative to no R8, and a further 26.7% DEX reduction relative to the current private-members policy.

Existing implementation stack

R8 configuration requirements

  • Add a supported AndroidR8ObfuscationMode=runtime-remapping mode.
  • Require AndroidLinkTool=r8, PublishTrimmed=true, AndroidTypeMapImplementation=trimmable, and CoreCLR or NativeAOT.
  • Root surviving JCWs while allowing ordinary JNI-visible types and members to be obfuscated where runtime remapping is available.
  • Preserve only bootstrap/runtime identifiers that are used outside the remapping machinery.
  • Preserve and compose AAPT rules, AAR consumer rules, user ProguardConfiguration items, user-authored Java sources, and explicit remap inputs.
  • Continue protecting generated native callback declarations and descriptor classes until RegisterNatives literals can be safely rewritten after R8.

The current native callback boundary intentionally relies on the standard rule:

-keepclasseswithmembernames,includedescriptorclasses class * {
    native <methods>;
}

Generated RegisterNatives UTF-8 names and signatures remain original. Native callback names are therefore pinned rather than runtime-remapped. A future optimization may rewrite these literals and reverse-remap the callback class, but that is not required for the initial feature.

Trimming and correctness requirements

  • A managed Java peer removed by ILLink must be absent from post-trim Java sources, acw-map.txt, generated R8 roots, final DEX, and runtime remapping tables.
  • A retained peer must survive R8 shrinking and work after genuine type/member obfuscation.
  • Java-to-managed activation must reverse-map the actual R8 class name before consulting the trimmable typemap.
  • Manifest components, XML custom views, runtime-owned JNI classes, interfaces, native callbacks, and user Java entry points must remain functional.
  • Dynamically constructed JNI identifiers must have a documented explicit-remap or keep-rule mechanism.
  • Existing MAM/Intune remappings must compose correctly with final R8 names.
  • CoreCLR and NativeAOT device tests must cover constructors, overloads, fields, inherited members, reverse activation, and changed R8 inputs.
  • Multi-RID NativeAOT builds must use one final R8 mapping while producing correct ABI-specific native remapping data.

Incremental-build requirements

  • Deleting a post-trim Java wrapper invalidates javac and removes the corresponding .class and DEX class.
  • Changed or missing R8/AAPT/ProGuard inputs rerun the appropriate targets.
  • Changed mapping.txt regenerates remapping tables.
  • NativeAOT remapping changes relink without unnecessarily rerunning ILC.
  • A failed R8 invocation must not allow NativeAOT to link against a stale mapping file.
  • Clean removes all generated remapping, configuration, mapping, and native object outputs.

Product validation and rollout

  • Measure current MAUI templates and at least one representative large MAUI dependency closure, not only the repository's Xamarin.Forms-like fixture.
  • Add DEX byte-size and class-count regression coverage for no R8, private-members, and runtime-remapping configurations.
  • Validate AndroidX, Material, Firebase, Google Play Services, MAM/Intune, custom XML views, reflection/service-loader Java, and native-library JNI entry points.
  • Document supported configurations, limitations, diagnostics, and escape hatches.
  • Initially ship as an explicit opt-in.
  • Consider making it the optimized trimmed default only after ecosystem compatibility is established.

Related issues

The focused issues above can remain open for acceptance criteria that are broader than the initial runtime-remapping implementation, particularly NativeAOT pre-javac JCW pruning, Java-only retention policy, typemap DLL invalidation, and native callback registration redesign.

Steps to Reproduce

  1. Build a Release MAUI or .NET for Android application without AndroidLinkTool=r8.
  2. Inspect classes.dex and any secondary DEX files with APK Analyzer or dexdump.
  3. Rebuild with R8 enabled and compare DEX bytes and class counts.
  4. Apply a less conservative obfuscation policy without runtime JNI remapping and observe failures when managed bindings request original type, method, field, constructor, or descriptor names.
  5. Trim an unused managed binding implementor and verify whether its Java wrapper, compiled class, R8 root, DEX class, and mapping entries are consistently removed.

A prototype configuration is:

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
  <PublishTrimmed>true</PublishTrimmed>
  <AndroidTypeMapImplementation>trimmable</AndroidTypeMapImplementation>
  <AndroidLinkTool>r8</AndroidLinkTool>
  <AndroidR8ObfuscationMode>runtime-remapping</AndroidR8ObfuscationMode>
</PropertyGroup>

Did you find any workaround?

Applications can currently opt into AndroidLinkTool=r8 and use the conservative private-members mode. This provides substantial Java shrinking but preserves many JNI-visible names and therefore leaves additional size reduction unavailable.

Applications may also provide custom ProGuard keep rules, but overly broad rules retain unnecessary Java code, while overly narrow rules can break JNI lookup, Java-to-managed activation, reflection, manifest/XML entry points, or RegisterNatives.

Relevant log output

Prototype CoreCLR Release measurement:

No R8:
  Decoded DEX: 9,534,068 bytes
  Classes:     7,352

R8 private-members:
  Decoded DEX: 2,794,592 bytes
  Classes:     3,636

R8 runtime-remapping + trimmable typemap:
  Decoded DEX: 2,047,188 bytes
  Classes:     2,365

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-triageIssues that need to be assigned.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions