Skip to content

Commit

Permalink
[Xamarin.Android.Build.Tasks] Add $(AndroidLinkResources) (dotnet#5317)
Browse files Browse the repository at this point in the history
The generated `Resource` class in `Resource.designer.cs` can be quite
large.

When doing "Release" builds, update the linker to *remove* the
`Resource` class and its nested types.  We do this by examining all
the locations where the fields are used and replacing those IL calls
with a call which specifies the actual resource id value directly.

This will remove the need to call
`global::Android.Runtime.ResourceIdManager.UpdateIdValues();` from
the static constructors in the `Resource` class and its nested types,
which will improve app launch times (as `UpdateIdValues()` involves
Reflection, which is not "fast").

Due to an odd linker failure we cannot completely remove the
`Resource` class itself.  It *will* be completely empty.

"Debug" builds will *not* remove the `Resource` type, and will retain
the call to `UpdateIdValues()`, in order to ensure that app rebuild
and redeploy times are as fast as possible.

The new `$(AndroidLinkResources)` MSBuild property has been added to
control this feature.  It is disabled by default for now.

`$(AndroidLinkResources)`=True appears to save between 100-280kb for
an `.apk`.  This is dependent on how many assemblies are Android
assemblies and contain or use Resources.

Xamarin.Forms base app `apkdiff` results

	Size difference in bytes ([*1] apk1 only, [*2] apk2 only):
	  -          10 assemblies/Mono.Android.dll
	  -       4,001 assemblies/Xamarin.Essentials.dll
	  -      45,426 assemblies/Xamarin.Forms.Platform.Android.dll
	  -     102,839 assemblies/formstest.Android.dll
	Summary:
	  +           0 Other entries 0.00% (of 887,808)
	  +           0 Dalvik executables 0.00% (of 3,341,152)
	  -     152,276 Assemblies -3.23% (of 4,709,077)
	  +           0 Shared libraries 0.00% (of 41,693,796)
	  -     151,552 Package size difference -0.73% (of 20,820,695)

Maps sample `apkdiff` results

	Size difference in bytes ([*1] apk1 only, [*2] apk2 only):
	  -           4 lib/armeabi-v7a/libxamarin-app.so
	  -          14 assemblies/Mono.Android.dll
	  -       4,757 assemblies/Xamarin.Essentials.dll
	  -      46,051 assemblies/Xamarin.Forms.Platform.dll
	  -      47,586 assemblies/Xamarin.Forms.Platform.Android.dll
	  -      48,470 assemblies/Xamarin.Forms.Maps.Android.dll
	  -     135,589 assemblies/FormsMapsSample.Android.dll
	Summary:
	  +           0 Other entries 0.00% (of 1,137,209)
	  +           0 Dalvik executables 0.00% (of 2,519,972)
	  -     282,467 Assemblies -5.51% (of 5,123,833)
	  -           4 Shared libraries -0.00% (of 41,843,596)
	  -     282,624 Package size difference -1.34% (of 21,075,664)
  • Loading branch information
dellis1972 authored Mar 2, 2021
1 parent 905878b commit 9e6ce03
Show file tree
Hide file tree
Showing 14 changed files with 377 additions and 41 deletions.
21 changes: 21 additions & 0 deletions Documentation/guides/building-apps/build-properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,27 @@ used in Android Application projects. The default value is
<AndroidLinkMode>SdkOnly</AndroidLinkMode>
```

## AndroidLinkResources

When `true` this will make the build system link out the Nested Types
of the Resource.Designer.cs `Resource` class in all assemblies. The
IL code that uses those types will be updated to use the values
directly rather than accessing fields.

This can have a small impact on reducing the apk size, it might also
help slightly with startup performance. This will only effect "Release"
based builds.

***Experimental***. Only designed to work with code such as

```
var view = FindViewById(Resources.Ids.foo);
```

Any other scenarios (such as reflection) will not be supported.

Added in Xamarin.Android 11.3.

## AndroidLinkSkip

Specifies a semicolon-delimited (`;`)
Expand Down
5 changes: 5 additions & 0 deletions src/Microsoft.Android.Sdk.ILLink/SetupStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ protected override void Process ()
if (Context.TryGetCustomData ("AddKeepAlivesStep", out addKeepAlivesStep) && bool.TryParse (addKeepAlivesStep, out var bv) && bv)
InsertAfter (new AddKeepAlivesStep (cache), "CleanStep");

string androidLinkResources;
if (Context.TryGetCustomData ("AndroidLinkResources", out androidLinkResources) && bool.TryParse (androidLinkResources, out var linkResources) && linkResources) {
InsertAfter (new RemoveResourceDesignerStep (), "CleanStep");
InsertAfter (new GetAssembliesStep (), "CleanStep");
}
InsertAfter (new StripEmbeddedLibraries (), "CleanStep");
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Mono.Cecil;
using Mono.Linker;
using Mono.Linker.Steps;
using System;
using System.Linq;
using Xamarin.Android.Tasks;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Mono.Cecil.Cil;

namespace MonoDroid.Tuner
{
public class AndroidLinkConfiguration {
public List<AssemblyDefinition> Assemblies { get; private set; } = new List<AssemblyDefinition> ();

static ConditionalWeakTable<LinkContext, AndroidLinkConfiguration> configurations = new ConditionalWeakTable<LinkContext, AndroidLinkConfiguration> ();

public static AndroidLinkConfiguration GetInstance (LinkContext context)
{
if (!configurations.TryGetValue (context, out AndroidLinkConfiguration config)) {
config = new AndroidLinkConfiguration ();
configurations.Add (context, config);
}
return config;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;

using Mono.Cecil;
using Mono.Cecil.Cil;

using Mono.Linker;

Expand Down Expand Up @@ -327,5 +328,39 @@ public static bool TryGetMarshalMethod (this MethodDefinition method, string nat

return false;
}

public static Instruction CreateLoadArraySizeOrOffsetInstruction (int intValue)
{
if (intValue < 0)
throw new ArgumentException ($"{nameof (intValue)} cannot be negative");

if (intValue < 9) {
switch (intValue) {
case 0:
return Instruction.Create (OpCodes.Ldc_I4_0);
case 1:
return Instruction.Create (OpCodes.Ldc_I4_1);
case 2:
return Instruction.Create (OpCodes.Ldc_I4_2);
case 3:
return Instruction.Create (OpCodes.Ldc_I4_3);
case 4:
return Instruction.Create (OpCodes.Ldc_I4_4);
case 5:
return Instruction.Create (OpCodes.Ldc_I4_5);
case 6:
return Instruction.Create (OpCodes.Ldc_I4_6);
case 7:
return Instruction.Create (OpCodes.Ldc_I4_7);
case 8:
return Instruction.Create (OpCodes.Ldc_I4_8);
}
}

if (intValue < 128)
return Instruction.Create (OpCodes.Ldc_I4_S, (sbyte)intValue);

return Instruction.Create (OpCodes.Ldc_I4, intValue);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Mono.Cecil;
using Mono.Linker;
using Mono.Linker.Steps;
using System;
using System.Linq;
using Xamarin.Android.Tasks;
using System.Collections.Generic;
using Mono.Cecil.Cil;

namespace MonoDroid.Tuner
{
public class GetAssembliesStep : BaseStep
{
AndroidLinkConfiguration config = null;

protected override void Process ()
{
config = AndroidLinkConfiguration.GetInstance (Context);
}

protected override void ProcessAssembly (AssemblyDefinition assembly)
{
if (config == null)
return;
config.Assemblies.Add (assembly);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ static Pipeline CreatePipeline (LinkerOptions options)
pipeline.AppendStep (new LoadI18nAssemblies (options.I18nAssemblies));

pipeline.AppendStep (new BlacklistStep ());

foreach (var desc in options.LinkDescriptions)
pipeline.AppendStep (new ResolveFromXmlStep (new XPathDocument (desc)));

Expand Down Expand Up @@ -121,6 +121,11 @@ static Pipeline CreatePipeline (LinkerOptions options)
pipeline.AppendStep (new StripEmbeddedLibraries ());
if (options.AddKeepAlives)
pipeline.AppendStep (new AddKeepAlivesStep (cache));

if (options.LinkResources) {
pipeline.AppendStep (new GetAssembliesStep ());
pipeline.AppendStep (new RemoveResourceDesignerStep ());
}
// end monodroid specific
pipeline.AppendStep (new RegenerateGuidStep ());
pipeline.AppendStep (new OutputStepWithTimestamps ());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ class LinkerOptions
public bool AddKeepAlives { get; set; }
public bool PreserveJniMarshalMethods { get; set; }
public bool DeterministicOutput { get; set; }
public bool LinkResources { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ void UpdateMagicPrefill (TypeDefinition magicType)
var instructions = methodPrefill.Body.Instructions;
instructions.Clear ();

instructions.Add (CreateLoadArraySizeOrOffsetInstruction (marshalTypes.Count));
instructions.Add (Extensions.CreateLoadArraySizeOrOffsetInstruction (marshalTypes.Count));
instructions.Add (Instruction.Create (OpCodes.Newobj, magicType.Module.ImportReference (genericMethodDictionaryCtor)));
instructions.Add (Instruction.Create (OpCodes.Stsfld, fieldTypesMap));

Expand All @@ -197,7 +197,7 @@ void UpdateMagicPrefill (TypeDefinition magicType)
foreach (var type in marshalTypes) {
instructions.Add (Instruction.Create (OpCodes.Ldsfld, fieldTypesMap));
instructions.Add (Instruction.Create (OpCodes.Ldstr, type.FullName.Replace ("/__<$>_jni_marshal_methods", "").Replace ("/","+")));
instructions.Add (CreateLoadArraySizeOrOffsetInstruction (idx++));
instructions.Add (Extensions.CreateLoadArraySizeOrOffsetInstruction (idx++));
instructions.Add (Instruction.Create (OpCodes.Callvirt, importedMethodSetItem));
}

Expand Down Expand Up @@ -265,40 +265,6 @@ static bool IsLdcI4 (Instruction instruction, out int intValue)
return true;
}

static Instruction CreateLoadArraySizeOrOffsetInstruction (int intValue)
{
if (intValue < 0)
throw new ArgumentException ($"{nameof (intValue)} cannot be negative");

if (intValue < 9) {
switch (intValue) {
case 0:
return Instruction.Create (OpCodes.Ldc_I4_0);
case 1:
return Instruction.Create (OpCodes.Ldc_I4_1);
case 2:
return Instruction.Create (OpCodes.Ldc_I4_2);
case 3:
return Instruction.Create (OpCodes.Ldc_I4_3);
case 4:
return Instruction.Create (OpCodes.Ldc_I4_4);
case 5:
return Instruction.Create (OpCodes.Ldc_I4_5);
case 6:
return Instruction.Create (OpCodes.Ldc_I4_6);
case 7:
return Instruction.Create (OpCodes.Ldc_I4_7);
case 8:
return Instruction.Create (OpCodes.Ldc_I4_8);
}
}

if (intValue < 128)
return Instruction.Create (OpCodes.Ldc_I4_S, (sbyte)intValue);

return Instruction.Create (OpCodes.Ldc_I4, intValue);
}

bool UpdateMarshalRegisterMethod (MethodDefinition method, HashSet<string> markedMethods)
{
var instructions = method.Body.Instructions;
Expand All @@ -310,7 +276,7 @@ bool UpdateMarshalRegisterMethod (MethodDefinition method, HashSet<string> marke
if (!arraySizeUpdated && idx + 1 < instructions.Count) {
int length;
if (IsLdcI4 (instructions [idx++], out length) && instructions [idx].OpCode == OpCodes.Newarr) {
instructions [idx - 1] = CreateLoadArraySizeOrOffsetInstruction (markedMethods.Count);
instructions [idx - 1] = Extensions.CreateLoadArraySizeOrOffsetInstruction (markedMethods.Count);
idx++;
arraySizeUpdated = true;
continue;
Expand Down Expand Up @@ -351,7 +317,7 @@ bool UpdateMarshalRegisterMethod (MethodDefinition method, HashSet<string> marke
continue;

if (markedMethods.Contains (mr.Name)) {
instructions [offsetIdx] = CreateLoadArraySizeOrOffsetInstruction (arrayOffset++);
instructions [offsetIdx] = Extensions.CreateLoadArraySizeOrOffsetInstruction (arrayOffset++);
continue;
}

Expand Down Expand Up @@ -436,7 +402,7 @@ protected override void DoAdditionalTypeProcessing (TypeDefinition type)
foreach (MethodReference method in type.Methods)
MarkMethod (method);
}

private void PreserveRegisteredMethod (TypeDefinition type, string member)
{
var type_ptr = type;
Expand Down
Loading

0 comments on commit 9e6ce03

Please sign in to comment.