-
Notifications
You must be signed in to change notification settings - Fork 561
[Xamarin.Android.Build.Tasks] Microsoft.Resource.Designer Assembly #6427
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
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.csgeneration 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.csfile 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
resourcein the project AND any
resourcedeclared in a referenced assembly or.aar file. This can result in 1000's of fields ending up in each
Resourceclass.
Because we only know the final
Idvalues 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
UpdateIdValuesmethod. 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
UpdateIdValuesmethod ends up referencingEVERY field. So the linker has nothing to work with. Custom linker steps were
introduced via the
AndroidLinkResourcesMSBuild feature, but this is not agreat solution and can be quite unstable since in relies on searching for specific
ILpatterns. 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 Assemblywhich will contain similar classes to those in a
Resource.designer.csfile.The main difference is this will be properties rather than fields. This
Reference Assemblydoes 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 Assemblythe 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.
UpdateIdValuesmethod, start up time will bereduced.
reduces application size.
The Details
The new system will make use of
Mono.Cecilto generate the IL for the assembly. Whilethis new system could be written to generate source code and then call the
csccompilermanually, 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.csfile.The only difference is these will use properties rather than fields. The
Resourceproperties will be generated directly from the
R.txtwhichaapt2emits as part ofthe build process.
For applications however we need to generate two
Resourceclasses. This is for historicaland backward compatibility reasons. The old system generated
constvalues for the applicationResource.designer.csfile rather than thestaticvalues for libraries. This was so thatthe
constvalues 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
constfields in theResourcenested classes so the entire system works. We do this bygenerating two classes. The one called
Resourcecontains 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
ResourceConstantwhich containsconstfields and is ONLY used bythe final application. This way ALL the
ResourceConstantvalues 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
Resourceclasswas generated in the
RootNamespaceof 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
Resourceclass 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.targetsfile. The entry point for this set of targets is
_BuildResourceDesigner. Thiswill only be run if the global MSbuild property
AndroidUseDesignerAssemblyisset to
true, as it will be for .NET 8+.New tasks are as follows.
GenerateRtxtis responsible for scanning the resource directory and generatingan
aapt2compatibleR.txtfile. This will be used by theGenerateResourceDesignerAssembly.GenerateResourceCaseMapis responsible for generating acasemap.txtfilewhich 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
GenerateResourceDesignerAssemblytaskwhen generating the IL in
_Microsoft.Android.Resource.Designer.dll. It isalso used by the old system to generate the same file.
GenerateResourceDesignerIntermediateClassis responsible for generatingthe
__Microsoft.Android.Resource.Designer.csfile in the$(IntermediateOutputPath).GenerateResourceDesignerAssemblyis the key to the whole system. This task willread the
R.xtfile and generate a_Microsoft.Android.Resource.Designer.dllassemblyin 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
CoreCompileand onlyfor design time builds.
It is also called in
_UpdateAndroidResgenwhich happens as part of the build and runsjust after
aapt2is called. This makes sure we always use the most up to dateversion of
R.txtto generate the new assembly.Because we are using the
R.txtfile to drive the generation of the new assembly,we needed some way for that to work when
aapt2was not being run. This usuallyhappens on a first time design time build. The current system has a class
GenerateResourceDesignerwhich is responsible for both scanning the resourcesand generating a design time
Resource.designer.csfile. 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
GenerateRtxtwhich is responsiblefor scanning the resources and generating an
R.txtfile. This is only usedwhen we are not doing a full build with
aapt2. This new task lets us generatethe needed
R.txtwhich can then be used by both the old and new system to generatetheir respective outputs.
As part of this we have two other classes
RtxtReaderandRtxtWriter. TheRtxtReaderunify's the code which was used to read the values of theR.txtinto one class which can be used by both systems. The
RtxtWriteris responsiblefor writing the
R.txtfile for design time builds. Again it will be used byboth the old and new system.
The
_AddResourceDesignerFilesis a new target which is responsible for makingsure that the new assembly and
__Microsoft.Android.Resource.Designer.csgetadded 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.fsfile 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
_AddResourceDesignerToPublishFilestarget. This target makes surethat the new assembly is added to the
@(ResolvedFileToPublish)ItemGroup. It alsoadds the require MetaData items such as
IsTrimmableandPostprocessAssemblywhichare 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