Skip to content

Commit f99fc81

Browse files
[Xamarin.Android.Build.Tasks, monodroid] Optimize member remapping (#7059)
Fixes: #7020 Context: f6f11a5 Commit f6f11a5 introduced type & member remapping. The problem with its approach was that it used XML as the representation format, which needed to be parsed during process startup. This would contribute to app startup slowdowns if there were any remappings, but even if there weren't any remappings, the minimum App size increased by ~490KB, due to the added dependencies on `System.Private.Xml.dll` & more. This app size increase is not ideal. Remove most of the `.apk` size increases by moving the remapping info into `libxamarin-app.so`. If no remappings are used, then the size increase to `libxamarin-app.so` is negligible, and the `.apk` size is only 61KB larger than pre-f6f11a5a. ~~ API Changes ~~ `Android.Runtime.AndroidTypeManager` gains the following members: partial class AndroidTypeManager { struct JniRemappingReplacementMethod { public string target_type, target_name; public bool is_static; } static extern byte* _monodroid_lookup_replacement_type ( string jniSimpleReference ); static extern JniRemappingReplacementMethod* _monodroid_lookup_replacement_method_info ( string jniSourceType, string jniMethodName, string jniMethodSignature ); } `AndroidTypeManager._monodroid_lookup_replacement_type()` replaces the `JNIEnv.ReplacementTypes` dictionary from f6f11a5. `AndroidTypeManager._monodroid_lookup_replacement_method_info()` replaces the `JNIEnv.ReplacementMethods` dictionary from f6f11a5. Both `_monodroid_lookup_replacement_type()` and `_monodroid_lookup_replacement_method_info()` are P/Invokes into `libxamarin-app.so`. ~~ `libxamarin-app.so` Changes ~~ The contents of the `@(_AndroidRemapMembers)` item group are now stored within `libxamarin-app.so`, with the following structure: const uint32_t jni_remapping_replacement_method_index_entry_count; const JniRemappingIndexTypeEntry jni_remapping_method_replacement_index[]; const uint32_t jni_remapping_replacement_type_count; const JniRemappingTypeReplacementEntry jni_remapping_type_replacements[]; struct JniRemappingString { const uint32_t length; const char *str; }; struct JniRemappingReplacementMethod { const char *target_type, *target_name; const bool is_static; }; struct JniRemappingIndexMethodEntry { JniRemappingString name, signature; JniRemappingReplacementMethod replacement }; struct struct JniRemappingIndexTypeEntry { JniRemappingString name; uint32_t method_count; JniRemappingIndexMethodEntry *methods; }; struct JniRemappingTypeReplacementEntry { JniRemappingString name; const char *replacement; }; const char * _monodroid_lookup_replacement_type (const char *); const JniRemappingReplacementMethod* _monodroid_lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature); Referring to the `<replacements/>` XML from f6f11a5 in `@(_AndroidRemapMembers)`: * `//replace-type/@from` fills `JniRemappingTypeReplacementEntry::name`, `//replace-type/@to` fills `JniRemappingTypeReplacementEntry::replacement`, and `_monodroid_lookup_replacement_type()` performs a linear search over `jni_remapping_type_replacements`. * `//replace-method/@source-type` fills `JniRemappingIndexTypeEntry::name`, `//replace-method/@source-method-name` fills `JniRemappingIndexMethodEntry::name`, `//replace-method/@source-method-signature` fills `JniRemappingIndexMethodEntry::signature`, `//replace-method/@target-type` fills `JniRemappingReplacementMethod::target_type`, `//replace-method/@target-method-name` fills `JniRemappingReplacementMethod::target_name`, `//replace-method/@target-method-signature` and `//replace-method/@target-method-parameter-count` are *ignored*, `//replace-method/@target-method-instance-to-static` fills `JniRemappingReplacementMethod::is_static`, and `_monodroid_lookup_replacement_method_info()` performs a search over `jni_remapping_method_replacement_index` looking for entries with "matching" type names, method names, and method signatures, and once a match is found it returns a pointer to the `JniRemappingReplacementMethod` instance. Co-authored-by: Jonathan Peppers <[email protected]>
1 parent 226d750 commit f99fc81

35 files changed

+1091
-316
lines changed

src/Mono.Android/Android.Runtime/AndroidRuntime.cs

Lines changed: 33 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,13 @@ public override void DeleteWeakGlobalReference (ref JniObjectReference value)
243243
}
244244

245245
class AndroidTypeManager : JniRuntime.JniTypeManager {
246+
struct JniRemappingReplacementMethod
247+
{
248+
public string target_type;
249+
public string target_name;
250+
public bool is_static;
251+
};
252+
246253
bool jniAddNativeMethodRegistrationAttributePresent;
247254

248255
public AndroidTypeManager (bool jniAddNativeMethodRegistrationAttributePresent)
@@ -317,123 +324,57 @@ protected override IEnumerable<string> GetSimpleReferences (Type type)
317324
};
318325
}
319326

327+
[DllImport (AndroidRuntime.InternalDllName, CallingConvention = CallingConvention.Cdecl)]
328+
static extern IntPtr _monodroid_lookup_replacement_type (string jniSimpleReference);
329+
320330
protected override string? GetReplacementTypeCore (string jniSimpleReference)
321331
{
322-
if (JNIEnv.ReplacementTypes == null) {
332+
if (!JNIEnv.jniRemappingInUse) {
323333
return null;
324334
}
325-
if (JNIEnv.ReplacementTypes.TryGetValue (jniSimpleReference, out var v)) {
326-
return v;
335+
336+
IntPtr ret = _monodroid_lookup_replacement_type (jniSimpleReference);
337+
if (ret == IntPtr.Zero) {
338+
return null;
327339
}
328-
return null;
340+
341+
return Marshal.PtrToStringAnsi (ret);
329342
}
330343

344+
[DllImport (AndroidRuntime.InternalDllName, CallingConvention = CallingConvention.Cdecl)]
345+
static extern IntPtr _monodroid_lookup_replacement_method_info (string jniSourceType, string jniMethodName, string jniMethodSignature);
346+
331347
protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, string jniMethodName, string jniMethodSignature)
332348
{
333-
if (JNIEnv.ReplacementMethods == null) {
349+
if (!JNIEnv.jniRemappingInUse) {
334350
return null;
335351
}
336-
#if !STRUCTURED
337-
if (!JNIEnv.ReplacementMethods.TryGetValue (CreateReplacementMethodsKey (jniSourceType, jniMethodName, jniMethodSignature), out var r) &&
338-
!JNIEnv.ReplacementMethods.TryGetValue (CreateReplacementMethodsKey (jniSourceType, jniMethodName, GetMethodSignatureWithoutReturnType ()), out r) &&
339-
!JNIEnv.ReplacementMethods.TryGetValue (CreateReplacementMethodsKey (jniSourceType, jniMethodName, null), out r)) {
340-
return null;
341-
}
342-
ReadOnlySpan<char> replacementInfo = r;
343-
344-
var targetType = GetNextString (ref replacementInfo);
345-
var targetName = GetNextString (ref replacementInfo);
346-
var targetSig = GetNextString (ref replacementInfo);
347-
var paramCountStr = GetNextString (ref replacementInfo);
348-
var isStaticStr = GetNextString (ref replacementInfo);
349352

350-
int? paramCount = null;
351-
if (!paramCountStr.IsEmpty) {
352-
if (!int.TryParse (paramCountStr, 0, System.Globalization.CultureInfo.InvariantCulture, out var count)) {
353-
return null;
354-
}
355-
paramCount = count;
353+
IntPtr retInfo = _monodroid_lookup_replacement_method_info (jniSourceType, jniMethodName, jniMethodSignature);
354+
if (retInfo == IntPtr.Zero) {
355+
return null;
356356
}
357357

358-
bool isStatic = false;
359-
if (isStaticStr.Equals ("true", StringComparison.Ordinal)) {
360-
isStatic = true;
361-
}
358+
var method = new JniRemappingReplacementMethod ();
359+
method = Marshal.PtrToStructure<JniRemappingReplacementMethod>(retInfo);
362360

363-
if (targetSig.IsEmpty && isStatic) {
364-
paramCount = paramCount ?? JniMemberSignature.GetParameterCountFromMethodSignature (jniMethodSignature);
365-
paramCount++;
361+
int? paramCount = null;
362+
if (method.is_static) {
363+
paramCount = JniMemberSignature.GetParameterCountFromMethodSignature (jniMethodSignature) + 1;
366364
jniMethodSignature = $"(L{jniSourceType};" + jniMethodSignature.Substring ("(".Length);
367365
}
368366

369367
return new JniRuntime.ReplacementMethodInfo {
370368
SourceJniType = jniSourceType,
371369
SourceJniMethodName = jniMethodName,
372370
SourceJniMethodSignature = jniMethodSignature,
373-
TargetJniType = targetType.IsEmpty ? jniSourceType : new string (targetType),
374-
TargetJniMethodName = targetName.IsEmpty ? jniMethodName : new string (targetName),
375-
TargetJniMethodSignature = targetSig.IsEmpty ? jniMethodSignature : new string (targetSig),
371+
TargetJniType = method.target_type,
372+
TargetJniMethodName = method.target_name,
373+
TargetJniMethodSignature = jniMethodSignature,
376374
TargetJniMethodParameterCount = paramCount,
377-
TargetJniMethodInstanceToStatic = isStatic,
375+
TargetJniMethodInstanceToStatic = method.is_static,
378376
};
379-
#else
380-
if (!JNIEnv.ReplacementMethods.TryGetValue ((jniSourceType, jniMethodName, jniMethodSignature), out var r) &&
381-
!JNIEnv.ReplacementMethods.TryGetValue ((jniSourceType, jniMethodName, GetMethodSignatureWithoutReturnType ()), out r) &&
382-
!JNIEnv.ReplacementMethods.TryGetValue ((jniSourceType, jniMethodName, null), out r)) {
383-
return null;
384-
}
385-
var targetSig = r.TargetSignature;
386-
var paramCount = r.ParamCount;
387-
if (targetSig == null && r.TurnStatic) {
388-
targetSig = $"(L{jniSourceType};" + jniMethodSignature.Substring ("(".Length);
389-
paramCount = paramCount ?? JniMemberSignature.GetParameterCountFromMethodSignature (jniMethodSignature);
390-
paramCount++;
391-
}
392-
return new JniRuntime.ReplacementMethodInfo {
393-
SourceJniType = jniSourceType,
394-
SourceJniMethodName = jniMethodName,
395-
SourceJniMethodSignature = jniMethodSignature,
396-
TargetJniType = r.TargetType ?? jniSourceType,
397-
TargetJniMethodName = r.TargetName ?? jniMethodName,
398-
TargetJniMethodSignature = targetSig ?? jniMethodSignature,
399-
TargetJniMethodParameterCount = paramCount,
400-
TargetJniMethodInstanceToStatic = r.TurnStatic,
401-
};
402-
#endif // !STRUCTURED
403-
404-
string GetMethodSignatureWithoutReturnType ()
405-
{
406-
int i = jniMethodSignature.IndexOf (')');
407-
return jniMethodSignature.Substring (0, i+1);
408-
}
409-
410-
string GetValue (string? value)
411-
{
412-
return value == null ? "null" : $"\"{value}\"";
413-
}
414-
415-
ReadOnlySpan<char> GetNextString (ref ReadOnlySpan<char> info)
416-
{
417-
int index = info.IndexOf ('\t');
418-
var r = info;
419-
if (index >= 0) {
420-
r = info.Slice (0, index);
421-
info = info.Slice (index+1);
422-
return r;
423-
}
424-
info = default;
425-
return r;
426-
}
427377
}
428-
429-
static string CreateReplacementMethodsKey (string? sourceType, string? methodName, string? methodSignature) =>
430-
new StringBuilder ()
431-
.Append (sourceType)
432-
.Append ('\t')
433-
.Append (methodName)
434-
.Append ('\t')
435-
.Append (methodSignature)
436-
.ToString ();
437378
#endif // NET
438379

439380
delegate Delegate GetCallbackHandler ();

src/Mono.Android/Android.Runtime/JNIEnv.cs

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,6 @@
1515
using Java.Interop.Tools.TypeNameMappings;
1616
using System.Diagnostics.CodeAnalysis;
1717

18-
#if NET
19-
using ReplacementTypesDict = System.Collections.Generic.Dictionary<string, string>;
20-
using ReplacementMethodsDict = System.Collections.Generic.Dictionary<string, string>;
21-
#endif // NET
22-
2318
namespace Android.Runtime {
2419
#pragma warning disable 0649
2520
struct JnienvInitializeArgs {
@@ -40,8 +35,7 @@ struct JnienvInitializeArgs {
4035
public int packageNamingPolicy;
4136
public byte ioExceptionType;
4237
public int jniAddNativeMethodRegistrationAttributePresent;
43-
public IntPtr mappingXml;
44-
public int mappingXmlLen;
38+
public bool jniRemappingInUse;
4539
}
4640
#pragma warning restore 0649
4741

@@ -55,6 +49,7 @@ public static partial class JNIEnv {
5549
static int androidSdkVersion;
5650

5751
static bool AllocObjectSupported;
52+
internal static bool jniRemappingInUse;
5853

5954
static IntPtr grefIGCUserPeer_class;
6055

@@ -68,11 +63,6 @@ public static partial class JNIEnv {
6863
static AndroidRuntime? androidRuntime;
6964
static BoundExceptionType BoundExceptionType;
7065

71-
#if NET
72-
internal static ReplacementTypesDict? ReplacementTypes;
73-
internal static ReplacementMethodsDict? ReplacementMethods;
74-
#endif // NET
75-
7666
[ThreadStatic]
7767
static byte[]? mvid_bytes;
7868

@@ -167,6 +157,7 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args)
167157

168158
gref_gc_threshold = args->grefGcThreshold;
169159

160+
jniRemappingInUse = args->jniRemappingInUse;
170161
java_vm = args->javaVm;
171162

172163
version = args->version;
@@ -178,13 +169,6 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args)
178169
gref_class = args->grefClass;
179170
mid_Class_forName = new JniMethodInfo (args->Class_forName, isStatic: true);
180171

181-
#if NET
182-
if (args->mappingXml != IntPtr.Zero) {
183-
var xml = Encoding.UTF8.GetString ((byte*) args->mappingXml, args->mappingXmlLen);
184-
(ReplacementTypes, ReplacementMethods) = MamXmlParser.ParseStrings (xml);
185-
}
186-
#endif // NET
187-
188172
if (args->localRefsAreIndirect == 1)
189173
IdentityHash = v => _monodroid_get_identity_hash_code (Handle, v);
190174
else

src/Mono.Android/Mono.Android.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,6 @@
253253
<Compile Include="Android.Runtime\JObjectRefType.cs" />
254254
<Compile Include="Android.Runtime\JValue.cs" />
255255
<Compile Include="Android.Runtime\Logger.cs" />
256-
<Compile Include="Android.Runtime\MamXmlParser.cs" />
257256
<Compile Include="Android.Runtime\NamespaceMappingAttribute.cs" />
258257
<Compile Include="Android.Runtime\OutputStreamAdapter.cs" />
259258
<Compile Include="Android.Runtime\OutputStreamInvoker.cs" />
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
using System;
2+
using System.IO;
3+
using System.Collections.Generic;
4+
using System.Xml;
5+
6+
using Microsoft.Build.Framework;
7+
using Microsoft.Build.Utilities;
8+
using Microsoft.Android.Build.Tasks;
9+
10+
namespace Xamarin.Android.Tasks
11+
{
12+
public class GenerateJniRemappingNativeCode : AndroidTask
13+
{
14+
internal const string JniRemappingNativeCodeInfoKey = ".:!JniRemappingNativeCodeInfo!:.";
15+
16+
internal sealed class JniRemappingNativeCodeInfo
17+
{
18+
public int ReplacementTypeCount { get; }
19+
public int ReplacementMethodIndexEntryCount { get; }
20+
21+
public JniRemappingNativeCodeInfo (int replacementTypeCount, int replacementMethodIndexEntryCount)
22+
{
23+
ReplacementTypeCount = replacementTypeCount;
24+
ReplacementMethodIndexEntryCount = replacementMethodIndexEntryCount;
25+
}
26+
}
27+
28+
public override string TaskPrefix => "GJRNC";
29+
30+
public ITaskItem RemappingXmlFilePath { get; set; }
31+
32+
[Required]
33+
public string OutputDirectory { get; set; }
34+
35+
[Required]
36+
public string [] SupportedAbis { get; set; }
37+
38+
public bool GenerateEmptyCode { get; set; }
39+
40+
public override bool RunTask ()
41+
{
42+
if (!GenerateEmptyCode) {
43+
if (RemappingXmlFilePath == null) {
44+
throw new InvalidOperationException ("RemappingXmlFilePath parameter is required");
45+
}
46+
47+
Generate ();
48+
} else {
49+
GenerateEmpty ();
50+
}
51+
52+
return !Log.HasLoggedErrors;
53+
}
54+
55+
void GenerateEmpty ()
56+
{
57+
Generate (new JniRemappingAssemblyGenerator (), typeReplacementsCount: 0);
58+
}
59+
60+
void Generate ()
61+
{
62+
var typeReplacements = new List<JniRemappingTypeReplacement> ();
63+
var methodReplacements = new List<JniRemappingMethodReplacement> ();
64+
65+
var readerSettings = new XmlReaderSettings {
66+
XmlResolver = null,
67+
};
68+
69+
using (var reader = XmlReader.Create (File.OpenRead (RemappingXmlFilePath.ItemSpec), readerSettings)) {
70+
if (reader.MoveToContent () != XmlNodeType.Element || reader.LocalName != "replacements") {
71+
Log.LogError ($"Input file `{RemappingXmlFilePath.ItemSpec}` does not start with `<replacements/>`");
72+
} else {
73+
ReadXml (reader, typeReplacements, methodReplacements);
74+
}
75+
}
76+
77+
Generate (new JniRemappingAssemblyGenerator (typeReplacements, methodReplacements), typeReplacements.Count);
78+
}
79+
80+
void Generate (JniRemappingAssemblyGenerator jniRemappingGenerator, int typeReplacementsCount)
81+
{
82+
jniRemappingGenerator.Init ();
83+
84+
foreach (string abi in SupportedAbis) {
85+
string baseAsmFilePath = Path.Combine (OutputDirectory, $"jni_remap.{abi.ToLowerInvariant ()}");
86+
string llFilePath = $"{baseAsmFilePath}.ll";
87+
88+
using (var sw = MemoryStreamPool.Shared.CreateStreamWriter ()) {
89+
jniRemappingGenerator.Write (GeneratePackageManagerJava.GetAndroidTargetArchForAbi (abi), sw, llFilePath);
90+
sw.Flush ();
91+
Files.CopyIfStreamChanged (sw.BaseStream, llFilePath);
92+
}
93+
}
94+
95+
BuildEngine4.RegisterTaskObjectAssemblyLocal (
96+
JniRemappingNativeCodeInfoKey,
97+
new JniRemappingNativeCodeInfo (typeReplacementsCount, jniRemappingGenerator.ReplacementMethodIndexEntryCount),
98+
RegisteredTaskObjectLifetime.Build
99+
);
100+
}
101+
102+
void ReadXml (XmlReader reader, List<JniRemappingTypeReplacement> typeReplacements, List<JniRemappingMethodReplacement> methodReplacements)
103+
{
104+
bool haveAllAttributes;
105+
106+
while (reader.Read ()) {
107+
if (reader.NodeType != XmlNodeType.Element) {
108+
continue;
109+
}
110+
111+
haveAllAttributes = true;
112+
if (String.Compare ("replace-type", reader.LocalName, StringComparison.Ordinal) == 0) {
113+
haveAllAttributes &= GetRequiredAttribute ("from", out string from);
114+
haveAllAttributes &= GetRequiredAttribute ("to", out string to);
115+
if (!haveAllAttributes) {
116+
continue;
117+
}
118+
119+
typeReplacements.Add (new JniRemappingTypeReplacement (from, to));
120+
} else if (String.Compare ("replace-method", reader.LocalName, StringComparison.Ordinal) == 0) {
121+
haveAllAttributes &= GetRequiredAttribute ("source-type", out string sourceType);
122+
haveAllAttributes &= GetRequiredAttribute ("source-method-name", out string sourceMethodName);
123+
haveAllAttributes &= GetRequiredAttribute ("target-type", out string targetType);
124+
haveAllAttributes &= GetRequiredAttribute ("target-method-name", out string targetMethodName);
125+
haveAllAttributes &= GetRequiredAttribute ("target-method-instance-to-static", out string targetIsStatic);
126+
127+
if (!haveAllAttributes) {
128+
continue;
129+
}
130+
131+
if (!Boolean.TryParse (targetIsStatic, out bool isStatic)) {
132+
Log.LogError ($"Attribute 'target-method-instance-to-static' in element '{reader.LocalName}' value '{targetIsStatic}' cannot be parsed as boolean; {RemappingXmlFilePath.ItemSpec} line {GetCurrentLineNumber ()}");
133+
continue;
134+
}
135+
136+
string sourceMethodSignature = reader.GetAttribute ("source-method-signature");
137+
methodReplacements.Add (
138+
new JniRemappingMethodReplacement (
139+
sourceType, sourceMethodName, sourceMethodSignature,
140+
targetType, targetMethodName, isStatic
141+
)
142+
);
143+
}
144+
}
145+
146+
bool GetRequiredAttribute (string attributeName, out string attributeValue)
147+
{
148+
attributeValue = reader.GetAttribute (attributeName);
149+
if (!String.IsNullOrEmpty (attributeValue)) {
150+
return true;
151+
}
152+
153+
Log.LogError ($"Attribute '{attributeName}' missing from element '{reader.LocalName}'; {RemappingXmlFilePath.ItemSpec} line {GetCurrentLineNumber ()}");
154+
return false;
155+
}
156+
157+
int GetCurrentLineNumber () => ((IXmlLineInfo)reader).LineNumber;
158+
}
159+
}
160+
}

0 commit comments

Comments
 (0)