Skip to content

Commit 9e6ce03

Browse files
authored
[Xamarin.Android.Build.Tasks] Add $(AndroidLinkResources) (#5317)
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)
1 parent 905878b commit 9e6ce03

File tree

14 files changed

+377
-41
lines changed

14 files changed

+377
-41
lines changed

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,27 @@ used in Android Application projects. The default value is
696696
<AndroidLinkMode>SdkOnly</AndroidLinkMode>
697697
```
698698

699+
## AndroidLinkResources
700+
701+
When `true` this will make the build system link out the Nested Types
702+
of the Resource.Designer.cs `Resource` class in all assemblies. The
703+
IL code that uses those types will be updated to use the values
704+
directly rather than accessing fields.
705+
706+
This can have a small impact on reducing the apk size, it might also
707+
help slightly with startup performance. This will only effect "Release"
708+
based builds.
709+
710+
***Experimental***. Only designed to work with code such as
711+
712+
```
713+
var view = FindViewById(Resources.Ids.foo);
714+
```
715+
716+
Any other scenarios (such as reflection) will not be supported.
717+
718+
Added in Xamarin.Android 11.3.
719+
699720
## AndroidLinkSkip
700721

701722
Specifies a semicolon-delimited (`;`)

src/Microsoft.Android.Sdk.ILLink/SetupStep.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ protected override void Process ()
6565
if (Context.TryGetCustomData ("AddKeepAlivesStep", out addKeepAlivesStep) && bool.TryParse (addKeepAlivesStep, out var bv) && bv)
6666
InsertAfter (new AddKeepAlivesStep (cache), "CleanStep");
6767

68+
string androidLinkResources;
69+
if (Context.TryGetCustomData ("AndroidLinkResources", out androidLinkResources) && bool.TryParse (androidLinkResources, out var linkResources) && linkResources) {
70+
InsertAfter (new RemoveResourceDesignerStep (), "CleanStep");
71+
InsertAfter (new GetAssembliesStep (), "CleanStep");
72+
}
6873
InsertAfter (new StripEmbeddedLibraries (), "CleanStep");
6974
}
7075

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using Mono.Cecil;
2+
using Mono.Linker;
3+
using Mono.Linker.Steps;
4+
using System;
5+
using System.Linq;
6+
using Xamarin.Android.Tasks;
7+
using System.Collections.Generic;
8+
using System.Runtime.CompilerServices;
9+
using Mono.Cecil.Cil;
10+
11+
namespace MonoDroid.Tuner
12+
{
13+
public class AndroidLinkConfiguration {
14+
public List<AssemblyDefinition> Assemblies { get; private set; } = new List<AssemblyDefinition> ();
15+
16+
static ConditionalWeakTable<LinkContext, AndroidLinkConfiguration> configurations = new ConditionalWeakTable<LinkContext, AndroidLinkConfiguration> ();
17+
18+
public static AndroidLinkConfiguration GetInstance (LinkContext context)
19+
{
20+
if (!configurations.TryGetValue (context, out AndroidLinkConfiguration config)) {
21+
config = new AndroidLinkConfiguration ();
22+
configurations.Add (context, config);
23+
}
24+
return config;
25+
}
26+
}
27+
}

src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/Extensions.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33

44
using Mono.Cecil;
5+
using Mono.Cecil.Cil;
56

67
using Mono.Linker;
78

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

328329
return false;
329330
}
331+
332+
public static Instruction CreateLoadArraySizeOrOffsetInstruction (int intValue)
333+
{
334+
if (intValue < 0)
335+
throw new ArgumentException ($"{nameof (intValue)} cannot be negative");
336+
337+
if (intValue < 9) {
338+
switch (intValue) {
339+
case 0:
340+
return Instruction.Create (OpCodes.Ldc_I4_0);
341+
case 1:
342+
return Instruction.Create (OpCodes.Ldc_I4_1);
343+
case 2:
344+
return Instruction.Create (OpCodes.Ldc_I4_2);
345+
case 3:
346+
return Instruction.Create (OpCodes.Ldc_I4_3);
347+
case 4:
348+
return Instruction.Create (OpCodes.Ldc_I4_4);
349+
case 5:
350+
return Instruction.Create (OpCodes.Ldc_I4_5);
351+
case 6:
352+
return Instruction.Create (OpCodes.Ldc_I4_6);
353+
case 7:
354+
return Instruction.Create (OpCodes.Ldc_I4_7);
355+
case 8:
356+
return Instruction.Create (OpCodes.Ldc_I4_8);
357+
}
358+
}
359+
360+
if (intValue < 128)
361+
return Instruction.Create (OpCodes.Ldc_I4_S, (sbyte)intValue);
362+
363+
return Instruction.Create (OpCodes.Ldc_I4, intValue);
364+
}
330365
}
331366
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using Mono.Cecil;
2+
using Mono.Linker;
3+
using Mono.Linker.Steps;
4+
using System;
5+
using System.Linq;
6+
using Xamarin.Android.Tasks;
7+
using System.Collections.Generic;
8+
using Mono.Cecil.Cil;
9+
10+
namespace MonoDroid.Tuner
11+
{
12+
public class GetAssembliesStep : BaseStep
13+
{
14+
AndroidLinkConfiguration config = null;
15+
16+
protected override void Process ()
17+
{
18+
config = AndroidLinkConfiguration.GetInstance (Context);
19+
}
20+
21+
protected override void ProcessAssembly (AssemblyDefinition assembly)
22+
{
23+
if (config == null)
24+
return;
25+
config.Assemblies.Add (assembly);
26+
}
27+
}
28+
}

src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/Linker.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ static Pipeline CreatePipeline (LinkerOptions options)
7777
pipeline.AppendStep (new LoadI18nAssemblies (options.I18nAssemblies));
7878

7979
pipeline.AppendStep (new BlacklistStep ());
80-
80+
8181
foreach (var desc in options.LinkDescriptions)
8282
pipeline.AppendStep (new ResolveFromXmlStep (new XPathDocument (desc)));
8383

@@ -121,6 +121,11 @@ static Pipeline CreatePipeline (LinkerOptions options)
121121
pipeline.AppendStep (new StripEmbeddedLibraries ());
122122
if (options.AddKeepAlives)
123123
pipeline.AppendStep (new AddKeepAlivesStep (cache));
124+
125+
if (options.LinkResources) {
126+
pipeline.AppendStep (new GetAssembliesStep ());
127+
pipeline.AppendStep (new RemoveResourceDesignerStep ());
128+
}
124129
// end monodroid specific
125130
pipeline.AppendStep (new RegenerateGuidStep ());
126131
pipeline.AppendStep (new OutputStepWithTimestamps ());

src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/LinkerOptions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,6 @@ class LinkerOptions
2525
public bool AddKeepAlives { get; set; }
2626
public bool PreserveJniMarshalMethods { get; set; }
2727
public bool DeterministicOutput { get; set; }
28+
public bool LinkResources { get; set; }
2829
}
2930
}

src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/MonoDroidMarkStep.cs

Lines changed: 5 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ void UpdateMagicPrefill (TypeDefinition magicType)
188188
var instructions = methodPrefill.Body.Instructions;
189189
instructions.Clear ();
190190

191-
instructions.Add (CreateLoadArraySizeOrOffsetInstruction (marshalTypes.Count));
191+
instructions.Add (Extensions.CreateLoadArraySizeOrOffsetInstruction (marshalTypes.Count));
192192
instructions.Add (Instruction.Create (OpCodes.Newobj, magicType.Module.ImportReference (genericMethodDictionaryCtor)));
193193
instructions.Add (Instruction.Create (OpCodes.Stsfld, fieldTypesMap));
194194

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

@@ -265,40 +265,6 @@ static bool IsLdcI4 (Instruction instruction, out int intValue)
265265
return true;
266266
}
267267

268-
static Instruction CreateLoadArraySizeOrOffsetInstruction (int intValue)
269-
{
270-
if (intValue < 0)
271-
throw new ArgumentException ($"{nameof (intValue)} cannot be negative");
272-
273-
if (intValue < 9) {
274-
switch (intValue) {
275-
case 0:
276-
return Instruction.Create (OpCodes.Ldc_I4_0);
277-
case 1:
278-
return Instruction.Create (OpCodes.Ldc_I4_1);
279-
case 2:
280-
return Instruction.Create (OpCodes.Ldc_I4_2);
281-
case 3:
282-
return Instruction.Create (OpCodes.Ldc_I4_3);
283-
case 4:
284-
return Instruction.Create (OpCodes.Ldc_I4_4);
285-
case 5:
286-
return Instruction.Create (OpCodes.Ldc_I4_5);
287-
case 6:
288-
return Instruction.Create (OpCodes.Ldc_I4_6);
289-
case 7:
290-
return Instruction.Create (OpCodes.Ldc_I4_7);
291-
case 8:
292-
return Instruction.Create (OpCodes.Ldc_I4_8);
293-
}
294-
}
295-
296-
if (intValue < 128)
297-
return Instruction.Create (OpCodes.Ldc_I4_S, (sbyte)intValue);
298-
299-
return Instruction.Create (OpCodes.Ldc_I4, intValue);
300-
}
301-
302268
bool UpdateMarshalRegisterMethod (MethodDefinition method, HashSet<string> markedMethods)
303269
{
304270
var instructions = method.Body.Instructions;
@@ -310,7 +276,7 @@ bool UpdateMarshalRegisterMethod (MethodDefinition method, HashSet<string> marke
310276
if (!arraySizeUpdated && idx + 1 < instructions.Count) {
311277
int length;
312278
if (IsLdcI4 (instructions [idx++], out length) && instructions [idx].OpCode == OpCodes.Newarr) {
313-
instructions [idx - 1] = CreateLoadArraySizeOrOffsetInstruction (markedMethods.Count);
279+
instructions [idx - 1] = Extensions.CreateLoadArraySizeOrOffsetInstruction (markedMethods.Count);
314280
idx++;
315281
arraySizeUpdated = true;
316282
continue;
@@ -351,7 +317,7 @@ bool UpdateMarshalRegisterMethod (MethodDefinition method, HashSet<string> marke
351317
continue;
352318

353319
if (markedMethods.Contains (mr.Name)) {
354-
instructions [offsetIdx] = CreateLoadArraySizeOrOffsetInstruction (arrayOffset++);
320+
instructions [offsetIdx] = Extensions.CreateLoadArraySizeOrOffsetInstruction (arrayOffset++);
355321
continue;
356322
}
357323

@@ -436,7 +402,7 @@ protected override void DoAdditionalTypeProcessing (TypeDefinition type)
436402
foreach (MethodReference method in type.Methods)
437403
MarkMethod (method);
438404
}
439-
405+
440406
private void PreserveRegisteredMethod (TypeDefinition type, string member)
441407
{
442408
var type_ptr = type;

0 commit comments

Comments
 (0)