Skip to content

Commit

Permalink
[Xamarin.Android.Build.Tasks] _Microsoft.Android.Resource.Designer (d…
Browse files Browse the repository at this point in the history
…otnet#6427)

Fixes: dotnet#6310

Context: dotnet/runtime@60d9b98
Context: dotnet/fsharp#12640
Context: 103b5a7   Optimize ResourceIdManager.UpdateIdValues() invocations
Context: 9e6ce03   Adds $(AndroidLinkResource)
Context: 522d7fb
Context: 9c04378   (AndroidEnablePreloadAssemblies crash)
Context: d521ac0   (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 `@(AndroidResource)` 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] (9e6ce03, d521ac0).

  * 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. d521ac0.  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
has 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, as 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
  • Loading branch information
dellis1972 authored Jan 5, 2023
1 parent b7138f1 commit dc3ccf2
Show file tree
Hide file tree
Showing 74 changed files with 2,772 additions and 331 deletions.
29 changes: 29 additions & 0 deletions Documentation/guides/building-apps/build-properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -1364,6 +1364,35 @@ To suppress the default AOT profiles, set the property to `false`.

Added in Xamarin.Android 10.1.

## AndroidUseDesignerAssembly

A bool property which controls if the build system will generate an
`_Microsoft.Android.Resource.Designer.dll` as apposed to a `Resource.Designer.cs` file. The benefits of this are smaller applications and
faster startup time.

The default value is `true` in .NET 8.

This setting is not backward compatible with Classic Xamarin.Android.
As a Nuget Author it is recommended that you ship three versions of
the assembly if you want to maintain backward compatibility.
One for MonoAndroid, one for net6.0-android and
one for net8.0-android. You can do this by using [Xamarin.Legacy.Sdk](https://www.nuget.org/packages/Xamarin.Legacy.Sdk). This is only required if your Nuget Library
project makes use of `AndroidResource` items in the project or via a dependency.

```
<TargetFrameworks>monoandroid90;net6.0-android;net8.0-android</TargetFrameworks>
```

Alternatively turn this setting off until such time as both Classic and
net7.0-android have been deprecated.

.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.

Added in .NET 8. Unsupported in Classic Xamarin.Android.

## AndroidUseInterpreter

A boolean property that causes the `.apk` to contain the mono
Expand Down
16 changes: 16 additions & 0 deletions Documentation/guides/messages/xa1034.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
title: Xamarin.Android error XA1034
description: XA1034 error code
ms.date: 13/12/2022
---
# Xamarin.Android error XA1034

## Example messages

```
Your project references 'Foo.dll' which uses the `_Microsoft.Android.Resource.Designer` assembly, but you do not have this feature enabled. Please set the `AndroidUseDesignerAssembly` MSBuild property to `true` in your project file.
```

## Solution

Edit your csproj directly and change the 'AndroidUseDesignerAssembly' to `True`.
1 change: 1 addition & 0 deletions build-tools/installers/create-installers.targets
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.CSharp.targets" ExcludeFromAndroidNETSdk="true" />
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.D8.targets" />
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.Designer.targets" />
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.Resource.Designer.targets" />
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.DesignTime.targets" />
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.EmbeddedResource.targets" />
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.FSharp.targets" ExcludeFromAndroidNETSdk="true" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.IO;

namespace Xamarin.Android.Prepare
{
[TPN]
class StrongNameSigner_TPN : ThirdPartyNotice
{
static readonly Uri url = new Uri ("https://github.com/brutaldev/StrongNameSigner/");

public override string LicenseFile => string.Empty;
public override string Name => "brutaldev/StrongNameSigner";
public override Uri SourceUrl => url;
public override string LicenseText => @"
Copyright (c) Werner van Deventer ([email protected]). All rights reserved.
Licensed under the Apache License, Version 2.0 (the 'License'); you
may not use this file except in compliance with the License. You may
obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an 'AS IS' BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied. See the License for the specific language governing permissions
and limitations under the License.
";

public override bool Include (bool includeExternalDeps, bool includeBuildDeps) => includeExternalDeps;
}
}
Loading

0 comments on commit dc3ccf2

Please sign in to comment.