Skip to content

Commit 82a4092

Browse files
[Xamarin.Android.Build.Tasks] add $(AndroidStripILAfterAOT) (#8172)
Context: xamarin/monodroid@388bf4b Context: 59ec488 Context: c929289 Context: 88215f9 Context: dotnet/runtime#86722 Context: dotnet/runtime#44855 Once Upon A Time™ we had a brilliant thought: if AOT pre-compiles C# methods, do we need the managed method anymore? Removing the C# method body would allow assemblies to be smaller. ("Even better", iOS does this too! Why Can't Android™?!) While the idea is straightforward, implementation was not: iOS uses ["Full" AOT][0], which AOT's *all* methods into a form that doesn't require a runtime JIT. This allowed iOS to run [`cil-strip`][1], removing all method bodies from all managed types. At the time, Xamarin.Android only supported "normal" AOT, and normal AOT requires a JIT for certain constructs such as generic types and generic methods. This meant that attempting to run `cil-strip` would result in runtime errors if a method body was removed that was actually required at runtime. (This was particularly bad because `cil-strip` could only remove *all* method bodies, not some!) This limitation was relaxed with the introduction of "Hybrid" AOT, which is "Full AOT while supporting a JIT". This meant that *all* methods could be AOT'd without requiring a JIT, which allowed method bodies to be removed; see xamarin/monodroid@388bf4b3. Unfortunately, this wasn't a great long-term solution: 1. Hybrid AOT was restricted to Visual Studio Enterprise customers. 2. Enabling Hybrid AOT would slow down Release configuration builds. 3. Hybrid AOT would result in larger apps. 4. As a consequence of (1), it didn't get as much testing 5. `cil-strip` usage was dropped as part of the .NET 5+ migration (c929289) Re-intoduce IL stripping for .NET 8. Add a new `$(AndroidStripILAfterAOT)` MSBuild property. When true, the `<MonoAOTCompiler/>` task will track which method bodies were actually AOT'd, storing this information into `%(_MonoAOTCompiledAssemblies.MethodTokenFile)`, and the new `<ILStrip/>` task will update the input assemblies, removing all method bodies that can be removed. By default setting `$(AndroidStripILAfterAOT)`=true will *override* the default `$(AndroidEnableProfiledAot)` setting, allowing all trimmable AOT'd methods to be removed. Profiled AOT and IL stripping can be used together by explicitly setting both within the `.csproj`: <PropertyGroup> <AndroidStripILAfterAOT>true</AndroidStripILAfterAOT> <AndroidEnableProfiledAot>true</AndroidEnableProfiledAot> </PropertyGroup> `.apk` size results for a `dotnet new android` app: | `$(AndroidStripILAfterAOT)` | `$(AndroidEnableProfiledAot)` | `.apk` size | | --------------------------- | ----------------------------- | ------------- | | true | true | 7.7MB | | true | false | 8.1MB | | false | true | 7.7MB | | false | false | 8.4MB | Note that `$(AndroidStripILAfterAOT)`=false and `$(AndroidEnableProfiledAot)`=true is the *default* Release configuration environment, for 7.7MB. A project that *only* sets `$(AndroidStripILAfterAOT)`=true implicitly sets `$(AndroidEnableProfiledAot)`=false, resulting in an 8.1MB app. Co-authored-by: Fan Yang <[email protected]> [0]: https://www.mono-project.com/docs/advanced/aot/#full-aot [1]: https://github.com/mono/mono/tree/2020-02/mcs/tools/cil-strip
1 parent 380c31c commit 82a4092

File tree

6 files changed

+72
-2
lines changed

6 files changed

+72
-2
lines changed

Documentation/guides/building-apps/build-properties.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1303,6 +1303,20 @@ This is only used when building `system` applications.
13031303

13041304
Support for this property was added in Xamarin.Android 11.3.
13051305

1306+
## AndroidStripILAfterAOT
1307+
1308+
A bool property that specifies whether or not the *method bodies* of AOT compiled methods will be removed.
1309+
1310+
The default value is `false`, and the method bodies of AOT compiled methods will *not* be removed.
1311+
1312+
When set to `true`, [`$(AndroidEnableProfiledAot)`](#androidenableprofiledaot) is set to `false` by default.
1313+
This means that in Release configuration builds -- in which
1314+
[`$(RunAOTCompilation)`](#runaotcompilation) is `true` by default -- AOT is enabled for *everything*.
1315+
This can result in increased app sizes. This behavior can be overridden by explicitly setting
1316+
`$(AndroidEnableProfiledAot)` to `true` within your project file.
1317+
1318+
Support for this property was added in .NET 8.
1319+
13061320
## AndroidSupportedAbis
13071321

13081322
A string property that contains a

src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Aot.targets

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,25 @@ They run in a context of an inner build with a single $(RuntimeIdentifier).
123123
LLVMPath="$(_LLVMPath)"
124124
LdName="$(_LdName)"
125125
LdFlags="$(_LdFlags)"
126+
CollectTrimmingEligibleMethods="$(AndroidStripILAfterAOT)"
127+
TrimmingEligibleMethodsOutputDirectory="$(IntermediateOutputPath)tokens"
126128
WorkingDirectory="$(MSBuildProjectDirectory)"
127129
AotArguments="$(AndroidAotAdditionalArguments)">
128130
<Output TaskParameter="CompiledAssemblies" ItemName="_MonoAOTCompiledAssemblies" />
129131
<Output TaskParameter="FileWrites" ItemName="FileWrites" />
130132
</MonoAOTCompiler>
133+
<ILStrip
134+
Condition=" '$(AndroidStripILAfterAOT)' == 'true' "
135+
TrimIndividualMethods="true"
136+
Assemblies="@(_MonoAOTCompiledAssemblies)"
137+
DisableParallelStripping="$(_DisableParallelAot)">
138+
<Output TaskParameter="TrimmedAssemblies" ItemName="_ILStripTrimmedAssemblies" />
139+
</ILStrip>
140+
<Move
141+
Condition=" '$(AndroidStripILAfterAOT)' == 'true' "
142+
SourceFiles="@(_ILStripTrimmedAssemblies->'%(TrimmedAssemblyFileName)')"
143+
DestinationFiles="@(_ILStripTrimmedAssemblies)"
144+
/>
131145
<WriteLinesToFile
132146
File="$(_AndroidStampDirectory)_AndroidAot.stamp"
133147
Lines="@(_MonoAOTCompiledAssemblies->'%(LibraryFile)')"

src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.DefaultProperties.targets

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@
9393
<_AndroidXA1029 Condition=" '$(AotAssemblies)' != '' ">true</_AndroidXA1029>
9494
<_AndroidXA1030 Condition=" '$(RunAOTCompilation)' == 'true' and '$(PublishTrimmed)' == 'false' ">true</_AndroidXA1030>
9595
<AotAssemblies>$(RunAOTCompilation)</AotAssemblies>
96-
<AndroidEnableProfiledAot Condition=" '$(AndroidEnableProfiledAot)' == '' and '$(RunAOTCompilation)' == 'true' ">true</AndroidEnableProfiledAot>
96+
<AndroidEnableProfiledAot Condition=" '$(AndroidEnableProfiledAot)' == '' and '$(RunAOTCompilation)' == 'true' and '$(AndroidStripILAfterAOT)' != 'true' ">true</AndroidEnableProfiledAot>
9797

9898
<!--
9999
Runtime libraries feature switches defaults

src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1973,7 +1973,7 @@ because xbuild doesn't support framework reference assemblies.
19731973

19741974
<!-- Shrink Mono.Android.dll by removing attribute only needed for GenerateJavaStubs -->
19751975
<RemoveRegisterAttribute
1976-
Condition="'$(AndroidLinkMode)' != 'None' AND '$(AndroidIncludeDebugSymbols)' != 'true'"
1976+
Condition="'$(AndroidLinkMode)' != 'None' AND '$(AndroidIncludeDebugSymbols)' != 'true' AND '$(AndroidStripILAfterAOT)' != 'true'"
19771977
ShrunkFrameworkAssemblies="@(_ShrunkAssemblies)" />
19781978

19791979
<MakeDir Directories="$(MonoAndroidIntermediateAssemblyDir)shrunk" />

tests/MSBuildDeviceIntegration/MSBuildDeviceIntegration.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
</ItemGroup>
3838

3939
<ItemGroup>
40+
<Reference Include="Xamarin.Android.Cecil">
41+
<HintPath>$(MicrosoftAndroidSdkOutDir)Xamarin.Android.Cecil.dll</HintPath>
42+
</Reference>
4043
<PackageReference Include="NodaTime" Version="2.4.5" />
4144
<PackageReference Include="MSBuild.StructuredLogger" Version="2.1.787" />
4245
<PackageReference Include="ICSharpCode.Decompiler" Version="7.2.1.6856" />

tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Text.RegularExpressions;
77
using System.Xml.Linq;
88
using System.Xml.XPath;
9+
using Mono.Cecil;
910
using NUnit.Framework;
1011
using Xamarin.ProjectTools;
1112

@@ -1075,5 +1076,43 @@ public void SupportDesugaringStaticInterfaceMethods ()
10751076
);
10761077
}
10771078

1079+
[Test]
1080+
public void EnableAndroidStripILAfterAOT ([Values (false, true)] bool profiledAOT)
1081+
{
1082+
var proj = new XamarinAndroidApplicationProject {
1083+
ProjectName = nameof (EnableAndroidStripILAfterAOT),
1084+
RootNamespace = nameof (EnableAndroidStripILAfterAOT),
1085+
IsRelease = true,
1086+
EnableDefaultItems = true,
1087+
};
1088+
proj.SetProperty("AndroidStripILAfterAOT", "true");
1089+
proj.SetProperty("AndroidEnableProfiledAot", profiledAOT.ToString ());
1090+
// So we can use Mono.Cecil to open assemblies directly
1091+
proj.SetProperty ("AndroidEnableAssemblyCompression", "false");
1092+
1093+
var builder = CreateApkBuilder ();
1094+
Assert.IsTrue (builder.Build (proj), "`dotnet build` should succeed");
1095+
1096+
var apk = Path.Combine (Root, builder.ProjectDirectory, proj.OutputPath, $"{proj.PackageName}-Signed.apk");
1097+
FileAssert.Exists (apk);
1098+
var helper = new ArchiveAssemblyHelper (apk);
1099+
Assert.IsTrue (helper.Exists ($"assemblies/{proj.ProjectName}.dll"), $"{proj.ProjectName}.dll should exist in apk!");
1100+
using (var stream = helper.ReadEntry ($"assemblies/{proj.ProjectName}.dll")) {
1101+
stream.Position = 0;
1102+
using var assembly = AssemblyDefinition.ReadAssembly (stream);
1103+
var type = assembly.MainModule.GetType ($"{proj.RootNamespace}.MainActivity");
1104+
var method = type.Methods.FirstOrDefault (p => p.Name == "OnCreate");
1105+
Assert.IsNotNull (method, $"{proj.RootNamespace}.MainActivity.OnCreate should exist!");
1106+
Assert.IsTrue (!method.HasBody || method.Body.Instructions.Count == 0, $"{proj.RootNamespace}.MainActivity.OnCreate should have no body!");
1107+
}
1108+
1109+
RunProjectAndAssert (proj, builder);
1110+
1111+
WaitForPermissionActivity (Path.Combine (Root, builder.ProjectDirectory, "permission-logcat.log"));
1112+
bool didLaunch = WaitForActivityToStart (proj.PackageName, "MainActivity",
1113+
Path.Combine (Root, builder.ProjectDirectory, "logcat.log"), 30);
1114+
Assert.IsTrue(didLaunch, "Activity should have started.");
1115+
}
1116+
10781117
}
10791118
}

0 commit comments

Comments
 (0)