-
Notifications
You must be signed in to change notification settings - Fork 528
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Xamarin.Android.Build.Tasks] Microsoft.Resource.Designer Assembly #6427
Conversation
|
95e7992
to
0148ce9
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like there is a bit still WIP, but I left a couple comments.
src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesignerAssembly.cs
Outdated
Show resolved
Hide resolved
src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesignerAssembly.cs
Outdated
Show resolved
Hide resolved
...amarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets
Outdated
Show resolved
Hide resolved
0148ce9
to
38f4e44
Compare
src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesignerAssembly.cs
Outdated
Show resolved
Hide resolved
src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesignerAssembly.cs
Show resolved
Hide resolved
53f3f2e
to
9c81823
Compare
9c81823
to
cfb9fce
Compare
edaa5d8
to
35befc3
Compare
6b8aa5f
to
72ae7ad
Compare
32aa90e
to
9538a3c
Compare
For reference some performance differences https://gist.github.com/dellis1972/f5a9c9475dadea1331c6c62b1a478cd8 for .net 6 |
99fe750
to
791bfac
Compare
806f6ea
to
2c953ae
Compare
Context: #6427 The current implementation of the `RemoveResourceDesignerStep` linker step does not work under .NET 6. This is mostly down to the fact that we didn't `override` the require methods to make it work in that environment. Reworks `RemoveResourceDesignerStep` to split some of the functionality out into a new `LinkDesignerBase` base class. `LinkDesignerBase` will be used in the future by the Resource Assembly linker step #6427. `.apk` size difference in a Basic Android Application shows a effectively no reduction in Package size. This is a single App Head project with no additional libraries or Nuget references: Size difference in bytes ([*1] apk1 only, [*2] apk2 only): - 596 assemblies/assemblies.blob - 760 lib/x86/libaot-DotNet6AndroidBasic.dll.so - 764 lib/armeabi-v7a/libaot-DotNet6AndroidBasic.dll.so - 864 lib/x86_64/libaot-DotNet6AndroidBasic.dll.so - 5,032 lib/arm64-v8a/libaot-DotNet6AndroidBasic.dll.so Summary: - 596 Other entries -0.03% (of 2,347,758) + 0 Dalvik executables 0.00% (of 333,284) - 7,420 Shared libraries -0.03% (of 23,525,172) + 0 Package size difference 0.00% (of 11,745,501) `.apk` size difference in a Basic Android app which references AndroidX, shows a Package size reduction of ~164Kb. This example includes an App Head project as well as a single Android Library project. Both projects contain 2-3 `@(AndroidResource)` items and both reference AndroidX. `Resource.Designer.cs` file is 460kb in size. Size difference in bytes ([*1] apk1 only, [*2] apk2 only): - 63,664 lib/x86/libaot-DotNet6AndroidTest.App.dll.so - 63,952 lib/x86_64/libaot-DotNet6AndroidTest.App.dll.so - 83,900 assemblies/assemblies.blob - 84,408 lib/arm64-v8a/libaot-DotNet6AndroidTest.App.dll.so - 92,340 lib/armeabi-v7a/libaot-DotNet6AndroidTest.App.dll.so Summary: - 83,900 Other entries -2.83% (of 2,965,109) + 0 Dalvik executables 0.00% (of 959,460) - 304,364 Shared libraries -1.28% (of 23,781,188) - 163,840 Package size difference -1.30% (of 12,620,512) `.apk` size difference for a Basic Maui application shows a Package size reduction of ~168Kb. This is just a standard `dotnet new maui` app built for the Android platform: Size difference in bytes ([*1] apk1 only, [*2] apk2 only): + 24 lib/arm64-v8a/libaot-Microsoft.Maui.Graphics.dll.so - 48 lib/arm64-v8a/libaot-Microsoft.Maui.Controls.Xaml.dll.so - 396 lib/armeabi-v7a/libaot-Microsoft.Maui.dll.so - 448 lib/arm64-v8a/libaot-Microsoft.Maui.dll.so - 536 lib/x86/libaot-Microsoft.Maui.Controls.Compatibility.dll.so - 592 lib/x86_64/libaot-Microsoft.Maui.Controls.Compatibility.dll.so - 632 lib/armeabi-v7a/libaot-Microsoft.Maui.Controls.dll.so - 632 lib/x86/libaot-Microsoft.Maui.Controls.dll.so - 720 lib/x86_64/libaot-Microsoft.Maui.Controls.dll.so - 4,492 lib/x86/libaot-Microsoft.Maui.dll.so - 4,544 lib/x86_64/libaot-Microsoft.Maui.dll.so - 4,568 lib/arm64-v8a/libaot-Microsoft.Maui.Controls.Compatibility.dll.so - 4,628 lib/armeabi-v7a/libaot-Microsoft.Maui.Controls.Compatibility.dll.so - 4,792 lib/arm64-v8a/libaot-Microsoft.Maui.Controls.dll.so - 26,676 lib/x86/libaot-BasicMauiApp.dll.so - 27,000 lib/x86_64/libaot-BasicMauiApp.dll.so - 30,772 lib/armeabi-v7a/libaot-BasicMauiApp.dll.so - 31,096 lib/arm64-v8a/libaot-BasicMauiApp.dll.so - 124,129 assemblies/assemblies.blob Summary: - 124,129 Other entries -1.18% (of 10,513,973) + 0 Dalvik executables 0.00% (of 6,459,432) - 142,548 Shared libraries -0.28% (of 51,768,960) - 167,936 Package size difference -0.57% (of 29,604,197) For Android applications which make use of lots of resources, these changes can also impact startup times. The follow are the start up improvements on the Android Application and Library projects, both of which use AndroidX. | Before (ms) | After (ms) | Δ (%) | Notes | | ----------: | ----------: | --------: | ------------------------------------ | | 188.500 | 176.150 | -6.55% ✓ | defaults; 32-bit build | | 174.150 | 175.100 | +0.54% ✗ | defaults; profiled AOT; 32-bit build | | 187.550 | 178.300 | -4.93% ✓ | defaults; full AOT; 32-bit build | | 173.250 | 174.050 | +0.46% ✗ | defaults; 64-bit build | | 186.400 | 176.450 | -5.34% ✓ | defaults; profiled AOT; 64-bit build | | 181.350 | 174.400 | -3.83% ✓ | defaults; full AOT; 64-bit build | ~~ Known Issues ~~ `RemoveResourceDesignerStep` doesn't work properly with array resources; consider: int iconDimen = Resource.Styleable.AlertDialog[Resource.Styleable.AlertDialog_buttonIconDimen]; The `RemoveResourceDesignerStep` will *remove* the `Resource.Styleable.AlertDialog` field, but the field is still accessed (?!), resulting in a `BadImageFormatException` at runtime: android.runtime.JavaProxyThrowable: System.BadImageFormatException: Could not resolve field token 0x0400052b
2c953ae
to
6ce7304
Compare
8514ce1
to
d8c93f7
Compare
5bb500e
to
20e10bf
Compare
20e10bf
to
c9aca88
Compare
d702b62
to
f32fd8b
Compare
a5855a5
to
a2ed4fa
Compare
.NET 8 Projects which choose to turn this setting off will not be able to | ||
consume references which do use it. If you try to use an assembly | ||
which does have this feature enabled in a project that does not, you will | ||
get a `XA1034` build error. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The last concern I have is this comment. Do we need to ask other teams about this? .NET MAUI team at least?
Any existing libraries from Telerik/DevExpress, will need to be updated to work in .NET 8? (If those are mostly UI, they would likely use a value from Resource.designer.cs
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
old style Assemblies will be auto upgraded by the linker during a build. So existing NuGet packages will work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, so you only hit an issue if you turn the setting off, got it.
Updated draft commit message: [Xamarin.Android.Build.Tasks] _Microsoft.Android.Resource.Designer (#6427)
Fixes: https://github.com/xamarin/xamarin-android/issues/6310
Context: https://github.com/dotnet/runtime/commit/60d9b98938a9003d937efdaa53dfe6f0033de9bb
Context: https://github.com/dotnet/fsharp/issues/12640
Context: 103b5a755c048c6eaedb43c139cac32b920372cd Optimize ResourceIdManager.UpdateIdValues() invocations
Context: 9e6ce03ca2d72f5415a2a1650185ba46113cc3dd Adds $(AndroidLinkResource)
Context: 522d7fb61f3669d85d077ba4f2889dbe8c9c8ac3
Context: 9c0437866c7308794283b76125483adecdea9067 (AndroidEnablePreloadAssemblies crash)
Context: d521ac0280c0ad165570077a860cb1846025010b (Styleables array values)
Replace the existing `Resource.designer.cs` generation code with a
new system that relies on Reference Assemblies. This results in
smaller apps and faster startup.
~~ Bind `@(AndroidRoesource)` values as fields ~~
The original approach to binding `@(AndroidResource)` values was to
Do What Java Does™: there are two "styles" of `Resource.designer.cs`
files, one for Library projects, and one for App projects.
`Resource.designer.cs` for Library projects involves mutable read/write
fields:
[assembly: Android.Runtime.ResourceDesignerAttribute ("ExampleLib.Resource", IsApplication=false)]
namespace ExampleLib;
partial class Resource {
partial class String {
public static int app_name = 2130771968;
static String() {
global::Android.Runtime.ResourceIdManager.UpdateIdValues();
}
}
partial class Styleable {
public static int[] MyLibraryWidget = new int[]{…};
static Styleable() {
global::Android.Runtime.ResourceIdManager.UpdateIdValues();
}
}
}
`Resource.designer.cs` for App projects involves *`const`* fields:
[assembly: Android.Runtime.ResourceDesignerAttribute ("App.Resource", IsApplication=true)]
namespace App;
partial class Resource {
partial class String {
public const int app_name = 2130968576;
static String() {
global::Android.Runtime.ResourceIdManager.UpdateIdValues();
}
}
partial class Styleable {
public static int[] MyLibraryWidget = new int[]{…}; // still read+write, not const
static Styleable() {
global::Android.Runtime.ResourceIdManager.UpdateIdValues();
}
}
}
There is a field each Android `resource` in the project *and* any
`resource`s declared in a referenced assembly or `.aar` files.
This can result in 1000's of fields ending up in each `Resource` class.
Because we only know the final `Id` values at app packaging time,
library projects could not know those values at build time. This meant
that we needed to update those library values at startup with the ones
that were compiled into the final application project. This is handled
by the `Resource.UpdateIdValues()` method. This method is called by
reflection on app startup and contains code to set the read/write
fields for *all* `Resource` types from *all referenced assemblies*:
partial class Resource {
public static void UpdateIdValues() {
global::ExampleLib.Resource.String.app_name = String.app_name;
// plus all other resources
}
}
**Pros**:
* It's a "known good" construct, as it's what Java does!
(Or *did*, circa 12 years ago…)
**Cons**:
* There is a semantic difference between the use of the `Resource`
types between Library and App projects: in an App project, you
can use Resource IDs in switch `case`s, e.g.
`case Resource.String.app_name: …`.
This is not possible in Library projects.
* As the App `Resource.UpdateIdValues()` method references *all*
fields from all referenced libraries, the linker is not able to
remove any of the fields. This pattern is linker hostile.
This results in larger `.apk` sizes, though this can be optimized
via [`$(AndroidLinkResources)`][0] (9e6ce03c, d521ac02).
* As the App `Resource.UpdateIdValues()` method references *all*
fields from all referenced libraries, the method can be *huge*;
it depends on how many resources the App and all dependencies
pull in. We have seen cases where the size of
`Resource.UpdateIdValues()` would cause the interpreter to crash,
breaking certain Hot Reload scenarios.
(Fixed in dotnet/runtime@60d9b989).
* The `Resource.UpdateIdValues()` method needs to be invoked during
process startup, *before* any assemblies try to use their
`Resource.…` values, and the method is looked up via *Reflection*.
This means System.Reflection is part of the app startup path,
which has overheads.
(This overhead is also removed via `$(AndroidLinkResources)`.)
~~ Bind `@(AndroidRoesource)` values as properties ~~
Replace the "bind resources as fields" approach with a new system
with significant differences:
1. Android resource ids are bound as read-only *properties*, and
2. The `Resource` class is placed into a *separate assembly*,
`_Microsoft.Android.Resource.Designer.dll`.
The new `$(AndroidUseDesignerAssembly)` MSBuild property controls
which Android resource approach is used; if True -- the default for
.NET 8 -- then `_Microsoft.Android.Resource.Designer.dll` will be
used. If False, then the previous "bind resource ids as fields"
approach will be used. This property is only valid for Library
projects; App projects must use the property-oriented approach.
This new approach takes advantage of [Reference Assemblies][1].
Reference Assemblies are designed to be replaced at runtime, and are
generally used to provide placeholder API's which can be swapped out
later.
Library projects will generate a Reference Assembly for
`_Microsoft.Android.Resource.Designer.dll` which contains read-only
properties for each `@(AndroidResource)` within the project and all
dependencies. This is otherwise identical to the "fields" approach,
*except* that the namespace is predefined, its a new assembly, and
properties are used instead of fields, *as if* it contained:
// _Microsoft.Android.Resource.Designer.dll for Library project
[assembly: System.Runtime.CompilerServices.ReferenceAssemblyAttribute]
namespace Microsoft.Android.Resource.Designer;
public partial class Resource {
public partial class String {
public static int app_name => 0;
}
public partial class Styleable {
public static int[] MyLibraryWidget => nullptr;
}
}
Also note that `_Microsoft.Android.Resource.Designer.dll` is produced
*with Mono.Cecil* as a pre-build action; no C# source is generated.
The Library assembly references the generated
`_Microsoft.Android.Resource.Designer.dll`.
The generated `_Microsoft.Android.Resource.Designer.dll` should
***NOT*** be shipped with NuGet packages.
App projects will generate the "real"
`_Microsoft.Android.Resource.Designer.dll`, also as a pre-build step,
and the "real" assembly will contain actual values for resource ids.
The App-built `_Microsoft.Android.Resource.Designer.dll` will also
have `[assembly:InternalsVisibleToAttribute]` to the App assembly:
// _Microsoft.Android.Resource.Designer.dll for App project
[assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute ("App…")]
namespace Microsoft.Android.Resource.Designer;
public partial class Resource {
public partial class String {
public static int app_name => 2130968576;
}
public partial class Styleable {
static int[] MyLibraryWidget = new[]{…};
public static int[] MyLibraryWidget => MyLibraryWidget;
}
}
This approach has a number of benefits
1. All the property declarations are in one place and are not
duplicated (-ish… more on that later).
As a result the size of the app will be reduced.
2. Because we no longer need the `Resource.UpdateIdValues()` method,
start up time will be reduced.
3. The linker can now do its job and properly link out unused
properties. This further reduces application size.
4. F# is now fully supported. See also: dotnet/fsharp#12640.
~~ Styleable Arrays ~~
Styleable resources may be arrays; see e.g. d521ac02. Via the power
of Cecil (and not using C# as an intermediate codegen), the binding
of styleable arrays in the "Bind `@(AndroidRoesource)` values as
properties" world order involves a static field containing the array
data, and a public property which returns the private field, which
have the same name:
public partial class Resource {
public partial class Styleable {
static int[] MyLibraryWidget = new[]{…};
public static int[] MyLibraryWidget => MyLibraryWidget;
}
}
CIL-wise, *yes*, the field and the property have the same name (?!),
but because properties actually have `get_` method prefix, there will
actually be a `MyLibraryWidget` field and a `get_MyLibraryWidget()`
method, so there are no name collisions.
*Note*: ***The styleable array is not copied***. This means it is
global mutable data, i.e. one can do this:
Microsoft.Android.Resource.Designer.Resource.Styleable.MyLibraryWidget[0] = 42;
***DO NOT DO THIS***. It will introduce runtime errors.
The e.g. `Resource.Styleable.MyLibraryWidget` property must be an
`int[]` in order to maintain compatibility, e.g. these are often
passed to methods which take `int[]` as the parameter type. We thus
cannot instead use e.g. `IEnumeragble<int>` as the property type.
Additionally, the array isn't copied for performance reasons.
We do not think that this will be a problem in practice, as the
previous "Bind `@(AndroidRoesource)` values as fields" strategy
*also* had mutable `int[]` fields, and suffers from the same
safety concerns, and the world hasn't ended…
~~ Source Compatibility ~~
In the "bind resource ids as fields" approach, the `Resource` class
was in the default namespace for the Library project, set via the
[`$(RootNamespace)`][2] MSBuild property. In order to maintain
source compatibility, Library projects will have a generated
`__Microsoft.Android.Resource.Designer.cs` file which contains a new
`Resource` declaration which *inherits* from the `Resource` type in
`_Microsoft.Android.Resource.Designer.dll`:
// Generated __Microsoft.Android.Resource.Designer.cs in Library projects
namespace ExampleLib;
public class Resource : Microsoft.Android.Resource.Designer.Resource {
}
This allows existing code such as `ExampleLib.Resource.String.app_name`
to continue to compile.
App projects also expect a `Resource` class in `$(RootNamespace)`,
*and* expect the values to be `const`. To support this, the generated
`_Microsoft.Android.Resource.Designer.dll` *actually* has two sets
of `Resource` types, one with properties, and an *`internal`*
`ResourceConstant` type:
// _Microsoft.Android.Resource.Designer.dll for Library project
[assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute ("App…")]
namespace Microsoft.Android.Resource.Designer;
internal partial class ResourceConstant {
public partial class String {
public const int app_name = 2130968576;
}
}
public partial class Resource {
public partial class String {
public static int app_name => ResourceConstant.String.app_name;
}
}
App projects *also* have a generated
`__Microsoft.Android.Resource.Designer.cs`, which has a `Resource` type
which inherits from `ResourceConstant`. This is why the App-built
`_Microsoft.Android.Resource.Designer.dll` needs
`[assembly: InternalsVisibleToAttribute]`:
// Generated __Microsoft.Android.Resource.Designer.cs in App projects
namespace App;
public class Resource : Microsoft.Android.Resource.Designer.ResourceConstant {
}
This allows existing App code to use `App.Resource.String.app_name`
in `case` statements.
~~ Binary Compatibility ~~
Binary compatibility is maintained via a new
`MonoDroid.Tuner.FixLegacyResourceDesignerStep` linker step.
`FixLegacyResourceDesignerStep` rewrites Library assemblies to replace
`Resource.…` field access with property access to
`Microsoft.Android.Resource.Designer.Resource.…` in
`_Microsoft.Android.Resource.Designer.dll`. Much of this code
overlaps with the existing logic of `$(AndroidLinkResources)`, and
allows existing Library assemblies to participate in the property-
oriented system.
~~ Internals ~~
The new build system introduces a number of new Tasks and Targets to
bring this all together. It also unify's some code between the field-
oriented and property-oriented approaches which would otherwise be
duplicated. The field-oriented system will be maintained for now for
backward compatibility, however the property-oriented system will be
enabled by default for .net 8.
The property-oriented system is mostly contained in
`Xamarin.Android.Resource.Designer.targets`. The entry point for this
set of targets is `_BuildResourceDesigner`, which will only be run if
the `$(AndroidUseDesignerAssembly)` MSBuild property is `True`, as it
will be for .NET 8+.
New tasks are as follows.
- `<GenerateRtxt/>` is responsible for scanning the resource
directory and generating an `aapt2`-compatible `R.txt` file.
This will be used by `<GenerateResourceDesignerAssembly/>`.
- `<GenerateResourceCaseMap/>` is responsible for generating a
`casemap.txt` file which will map the all lower case android
resources to the casing required for the C# code. Android requires
ALL resources be lower case, but our system allows the user to
define the case using any system then want. This task handles
generating this mapping between what the android system needs and
what the user is expecting. Its output is used by the
`<GenerateResourceDesignerAssembly/>` task when generating the IL
in `_Microsoft.Android.Resource.Designer.dll`.
It is also used by the old system to generate the same file.
- `<GenerateResourceDesignerIntermediateClass/>` is responsible for
generating the `__Microsoft.Android.Resource.Designer.cs` file in
`$(IntermediateOutputPath)`.
- `<GenerateResourceDesignerAssembly/>` is the key to the whole
property-oriented approach. This task will read the `R.xt` file
and generate a `_Microsoft.Android.Resource.Designer.dll` assembly
in `$(IntermediateOutputPath)`. This task is called in two places.
The first is in `_GenerateResourceDesignerAssembly`, this is called
as part of the build which happens just before `CoreCompile` and
only for design time builds.
It is also called in `_UpdateAndroidResgen` which happens as part
of the build and runs just after `aapt2` is called. This ensures
we always use the most up to date version of `R.txt` to generate
the new assembly.
Because we are using the `R.txt` file to drive the generation of the
new assembly, we needed some way for that to work when `aapt2` was
not being run. This usually happens on a first time design time build.
The field-oriented approach has a `<GenerateResourceDesigner/>` task
which is responsible for both scanning the resources and generating a
design time `Resource.designer.cs` file. While we could have
duplicated the code it made more sense to split out the resource
scanner into its own class. We now have a new `<GenerateRtxt/>` task
which is responsible for scanning the resources and generating an
`R.txt` file. This is only used when we are not doing a full build
with `aapt2`. This new task lets us generate the needed `R.txt` which
can then be used by both the old and new system to generate their
respective outputs.
As part of this we have two other classes: `RtxtReader` and
`RtxtWriter`. The `RtxtReader` unify's the code which was used to read
the values of the `R.txt` into one class which can be used by both
approaches. The `RtxtWriter` is responsible for writing the `R.txt`
file for design time builds. Again it will be used by both the old
and new system.
The `_AddResourceDesignerFiles` target is responsible for ensuring that
the new assembly and `__Microsoft.Android.Resource.Designer.cs` get
added to the correct item groups. These are `@(ReferencePath)` for the
assembly and `@(Compile)` for the source file. In the case of F# the
`__Microsoft.Android.Resource.Designer.fs` file which gets generated
has to be added to the `@(CompileBefore)` ItemGroup, this is so that
the types are resolved in the correct order.
To ensure that the new assembly is added to the final application we
have to introduce the `_AddResourceDesignerToPublishFiles` target.
This target makes sure that the new assembly is added to the
`@(ResolvedFileToPublish)` ItemGroup. It also adds the require
MetaData items such as `%(IsTrimmable)` and `%(PostprocessAssembly)`
which are required to get the assembly linked correctly.
~~ Results ~~
Results are most visible when lots of Android Resources are used.
For a [Sample app][3] app which uses lots of resources, we see the
following improvements to the **ActivityTaskManager: Displayed** time:
| Before (ms) | After (ms) | Δ (%) | Notes |
| ----------: | ----------: | --------: | ------------------------------------ |
| 340.500 | 313.250 | -8.00% ✓ | defaults; 64-bit build |
| 341.950 | 316.200 | -7.53% ✓ | defaults; profiled AOT; 64-bit build |
| 345.950 | 324.600 | -6.17% ✓ | defaults; 32-bit build |
| 341.000 | 323.050 | -5.26% ✓ | defaults; profiled AOT; 32-bit build |
[0]: https://learn.microsoft.com/en-us/xamarin/android/deploy-test/building-apps/build-properties#androidlinkresources
[1]: https://learn.microsoft.com/en-us/dotnet/standard/assembly/reference-assemblies
[2]: https://learn.microsoft.com/en-us/visualstudio/msbuild/common-msbuild-project-properties?view=vs-2022
[3]: https://github.com/dellis1972/DotNetAndroidTest |
Context dotnet#6310 Ignore Java.Interop-Tests IntermediateDir Try StrongNaming. Based on code in https://github.com/brutaldev/StrongNameSigner/blob/master/src/Brutal.Dev.StrongNameSigner/SigningHelper.cs Use ICSharpCode.Decompiler to read Resource Designer Assembly Fix breakage Fix error with Aapt2 R.txt Use latest Xamarin.Forms for dotnet tests Disable StrongNaming for now Removed unused code and logging Remove StrongNaming support Revert "Remove StrongNaming support" This reverts commit 7f90638f1788adfa37c4ec4ab3fed9fc48569cb5. Use a cstom snk for the designer strong name Update apkdesc Change to Microsoft.Android.Resource.Designer new test Move CryptoConvert to src-ThirdParty Add StrongNameSigner code and TPN Fix missing file Fix another build error Fix a test Update and Fix the UnitTest update docs Switch to _Microsoft.Android.Resource.Designer.dll update apkdesc update apkdesc
57e6f0f
to
a765bea
Compare
Commit message was ok, just a typo in
but otherwise ok 👍 |
Awesome job @dellis1972 ! Is it planned to be released with net8, or will it be released in a service release of net7 as well? |
This will be a net8 feature. |
* main: [Xamarin.Android.Build.Tasks] skip XA1034 logic in some cases (dotnet#7680) [ci] Move OneLocBuild task to scheduled pipeline (dotnet#7679) [Mono.Android] ServerCertificateValidationCallback() and redirects (dotnet#7662) Bump to xamarin/Java.Interop/main@cf80deb7 (dotnet#7664) Localized file check-in by OneLocBuild (dotnet#7668) [api-merge] Correctly compute //method/@deprecated-since (dotnet#7645) [Xamarin.Android.Build.Tasks] _Microsoft.Android.Resource.Designer (dotnet#6427) [Xamarin.Android.Build.Tasks] downgrade d8/r8 `warning` messages to `info` (dotnet#7643) [Xamarin.Android.Build.Tasks] fix cases of missing `@(Reference)` (dotnet#7642) [Xamarin.Android.Build.Tasks] delay ToJniName calls in ManifestDocument (dotnet#7653) [Xamarin.Android.Build.Tasks] fast path for `<CheckClientHandlerType/>` (dotnet#7652)
Context #6310
The goal of this commit is to replace the existing
Resource.designer.cs
generation code with a newer Reference Assembly based system. This new
system will result in both smaller and faster starting applications.
It also allows F# users to get the same experience as C# users, this makes
the https://github.com/xamarin/Xamarin.Android.FSharp.ResourceProvider
obsolete. F# projects will no longer need to use this work around.
The Old System
The old system generated a
Resource.designer.cs
file for each libraryand application project. This means that each assembly had a copy of
the same code (almost). This code declared a field for each android
resource
in the project AND any
resource
declared in a referenced assembly or.aar file. This can result in 1000's of fields ending up in each
Resource
class.
Because we only know the final
Id
values are app packaging time libraryprojects could not know those values at build time. This meant that we
needed to update those library values at startup with the ones that were
compiled into the final application project. This is handled by the
UpdateIdValues
method. This method is called by reflection on appstartup and contains code such as
And it does this for EVERY field that is referenced by the library projects.
This can result in 1000s of fields being updated on app startup. This impacts
performance quite allot.
One of the major problems with this system in the inability to properly link
out unused fields. This is because the
UpdateIdValues
method ends up referencingEVERY field. So the linker has nothing to work with. Custom linker steps were
introduced via the
AndroidLinkResources
MSBuild feature, but this is not agreat solution and can be quite unstable since in relies on searching for specific
IL
patterns. We need a better solution.The New System
The new system takes advantage of newer technology which has been available
for a few years now, the
Reference Assembly
. These types of assembly aredesigned to be replaced at runtime, they are generally used to provide
placeholder API's which can be swapped out later. So the idea of the new system
is fairly simple. Library projects will at build time generate a
Reference Assembly
which will contain similar classes to those in a
Resource.designer.cs
file.The main difference is this will be properties rather than fields. This
Reference Assembly
does not need to shipped with the library or copied anywhere itis purely there to provide an API to which the library can compile against.
The main application will then at build time generate the final full assembly. This
is the one which will be packaged with the application. Because it will have
the same name and API as the
Reference Assembly
the libraries generated the runtimewill redirect all the calls to this new assembly. This has a number of benefits
duplicated (ish.. more on that later). As a result the size of the app will be
reduced.
UpdateIdValues
method, start up time will bereduced.
reduces application size.
The Details
The new system will make use of
Mono.Cecil
to generate the IL for the assembly. Whilethis new system could be written to generate source code and then call the
csc
compilermanually, it was deemed more efficient to generate the IL directly. For library projects
the new assembly will contain the same classes as the old
Resource.designer.cs
file.The only difference is these will use properties rather than fields. The
Resource
properties will be generated directly from the
R.txt
whichaapt2
emits as part ofthe build process.
For applications however we need to generate two
Resource
classes. This is for historicaland backward compatibility reasons. The old system generated
const
values for the applicationResource.designer.cs
file rather than thestatic
values for libraries. This was so thatthe
const
values could be used directly by the compiler in the main app. As a result userswere able to write switch statements such as
which is only possible if you use constant values. We need to maintain this feature since we
do not want to break code when people upgrade. So we need a way to use properties as well as
const
fields in theResource
nested classes so the entire system works. We do this bygenerating two classes. The one called
Resource
contains all the nested types which containsthe property based API. This will be used by ALL the library projects in the final app.
Then we have one called
ResourceConstant
which containsconst
fields and is ONLY used bythe final application. This way ALL the
ResourceConstant
values will be directly used in theapp and we will maintain backward compatibility.
One thing we haven't mentioned so far is namespaces. In the old system the
Resource
classwas generated in the
RootNamespace
of the library project. So someone could do the followingBecause ALL the resources are now in a single assembly they need to be in one
common namespace. In this case its
Microsoft.Android.Resource.Designer
.However we still need to maintain that backward compatibility. So during the
build process we generate a file
__Microsoft.Android.Resource.Designer.cs
.This file contains code like this
This file is compiled into the library or application and exposes the
Resource
class in the correct namespace. This maintains backward compatibility.
The new build system introduces a number of new Tasks and Targets to bring this
all together. It also unify's some code between the old and new system which
would otherwise be duplicated. The old system will be maintained for now for
backward compatibility, however the new system will be on by default for .net 8.
The new system is mostly contained in the
Xamarin.Android.Resource.Designer.targets
file. The entry point for this set of targets is
_BuildResourceDesigner
. Thiswill only be run if the global MSbuild property
AndroidUseDesignerAssembly
isset to
true
, as it will be for .NET 8+.New tasks are as follows.
GenerateRtxt
is responsible for scanning the resource directory and generatingan
aapt2
compatibleR.txt
file. This will be used by theGenerateResourceDesignerAssembly
.GenerateResourceCaseMap
is responsible for generating acasemap.txt
filewhich will map the all lower case android resources to the casing required
for the C# code. Android requires ALL resources be lower case, but our system
allows the user to define the case using any system then want. This task handles
generating this mapping between what the android system needs and what the user
is expecting. Its output is used by the
GenerateResourceDesignerAssembly
taskwhen generating the IL in
_Microsoft.Android.Resource.Designer.dll
. It isalso used by the old system to generate the same file.
GenerateResourceDesignerIntermediateClass
is responsible for generatingthe
__Microsoft.Android.Resource.Designer.cs
file in the$(IntermediateOutputPath)
.GenerateResourceDesignerAssembly
is the key to the whole system. This task willread the
R.xt
file and generate a_Microsoft.Android.Resource.Designer.dll
assemblyin the
$(IntermediateOutputPath)
.This task is called in two places. The first is in
_GenerateResourceDesignerAssembly
,this is called as part of the build which happens just before
CoreCompile
and onlyfor design time builds.
It is also called in
_UpdateAndroidResgen
which happens as part of the build and runsjust after
aapt2
is called. This makes sure we always use the most up to dateversion of
R.txt
to generate the new assembly.Because we are using the
R.txt
file to drive the generation of the new assembly,we needed some way for that to work when
aapt2
was not being run. This usuallyhappens on a first time design time build. The current system has a class
GenerateResourceDesigner
which is responsible for both scanning the resourcesand generating a design time
Resource.designer.cs
file. While we could haveduplicated the code it made more sense to split our the resource scanner into
its own class. So we have a new task called
GenerateRtxt
which is responsiblefor scanning the resources and generating an
R.txt
file. This is only usedwhen we are not doing a full build with
aapt2
. This new task lets us generatethe needed
R.txt
which can then be used by both the old and new system to generatetheir respective outputs.
As part of this we have two other classes
RtxtReader
andRtxtWriter
. TheRtxtReader
unify's the code which was used to read the values of theR.txt
into one class which can be used by both systems. The
RtxtWriter
is responsiblefor writing the
R.txt
file for design time builds. Again it will be used byboth the old and new system.
The
_AddResourceDesignerFiles
is a new target which is responsible for makingsure that the new assembly and
__Microsoft.Android.Resource.Designer.cs
getadded to the correct item groups. These are
@(ReferencePath)
for the assemblyand
@(Compile)
for the source file. In the case of F# the__Microsoft.Android.Resource.Designer.fs
file which gets generated has to be added to the
@(CompileBefore)
ItemGroup,this is so that the types are resolved in the correct order.
To ensure that the new assembly is added to the final application we have to
introduce the
_AddResourceDesignerToPublishFiles
target. This target makes surethat the new assembly is added to the
@(ResolvedFileToPublish)
ItemGroup. It alsoadds the require MetaData items such as
IsTrimmable
andPostprocessAssembly
whichare required to get the assembly linked correctly.
Results
Here is a summary of the kinds of results one might see in an application.
Results will vary as it will depend on how many resources are used in both libraries
and applications. However this should give a good baseline.
Basic Android Application
Apk Size changes
From this example we reduce the package size by 167kb. Allot of code is removed.
Here is another example of a release aab file.
The following show that we can get a 2% reduction in startup time on a basic
Android application.
Total init
All runs
Without slowest and fastest runs
Without the slowest runs
A more complete example of a basic android app which one SkiaSharp view
Displayed
All runs
Without slowest and fastest runs
Without the slowest runs
Native to managed
All runs
Without slowest and fastest runs
Without the slowest runs
Total init
All runs
Without slowest and fastest runs
Without the slowest runs