Most of this also applies to Xamarin.Android, as we have nearly the same implementation.
Let's start with a simple Android API call:
Android.Util.Log.Debug ("MYTAG", "MyMessage");
If we inspect Log.Debug()
from a linked build:
[Register("d", "(Ljava/lang/String;Ljava/lang/String;)I", "")]
public unsafe static int Debug(string P_0, string P_1)
{
IntPtr native_tag = JNIEnv.NewString(P_0);
IntPtr native_msg = JNIEnv.NewString(P_1);
try
{
JniArgumentValue* __args = stackalloc JniArgumentValue[2];
*__args = new JniArgumentValue(native_tag);
__args[1] = new JniArgumentValue(native_msg);
return _members.StaticMethods.InvokeInt32Method("d.(Ljava/lang/String;Ljava/lang/String;)I", __args);
}
finally
{
JNIEnv.DeleteLocalRef(native_tag);
JNIEnv.DeleteLocalRef(native_msg);
}
}
Notice that JNIEnv.NewString()
is called and deleted for every System.String
parameter.
Next, we have:
public unsafe int InvokeInt32Method(string P_0, JniArgumentValue* P_1)
{
JniMethodInfo i = GetMethodInfo(P_0);
return JniEnvironment.StaticMethods.CallStaticIntMethod(Members.JniPeerType.PeerReference, i, P_1);
}
Then:
public unsafe static int CallStaticIntMethod(JniObjectReference P_0, JniMethodInfo P_1, JniArgumentValue* P_2)
{
if (!P_0.IsValid)
{
throw new ArgumentException("Handle must be valid.", "type");
}
if (P_1 == null)
{
throw new ArgumentNullException("method");
}
if (!P_1.IsValid)
{
throw new ArgumentException("Handle value is not valid.", "method");
}
IntPtr thrown;
int result = NativeMethods.java_interop_jnienv_call_static_int_method_a(EnvironmentPointer, out thrown, P_0.Handle, P_1.ID, (IntPtr)P_2);
Exception __e = GetExceptionForLastThrowable(thrown);
if (__e != null)
{
ExceptionDispatchInfo.Capture(__e).Throw();
}
return result;
}
The p/invoke:
[DllImport (JavaInteropLib, CallingConvention=CallingConvention.Cdecl, CharSet=CharSet.Ansi)]
internal static extern unsafe int java_interop_jnienv_call_static_int_method_a (IntPtr jnienv, out IntPtr thrown, jobject type, IntPtr method, IntPtr args);
This calls the C wrapper we have:
JI_API jint
java_interop_jnienv_call_static_int_method_a (JNIEnv *env, jthrowable *_thrown, jclass type, jstaticmethodID method, jvalue* args)
{
*_thrown = 0;
jint _r_ = (*env)->CallStaticIntMethodA (env, type, method, args);
*_thrown = (*env)->ExceptionOccurred (env);
return _r_;
}
.NET MAUI commonly has a "handler" implementation to set every property when the "native" side of controls are created.
And so on startup, dotnet/maui loops over each property and each Map*()
method called:
- https://github.com/dotnet/maui/blob/73fbb3bb056e84523864eae24fea78089ecd4d93/src/Core/src/Handlers/Label/LabelHandler.Android.cs
- https://github.com/dotnet/maui/blob/73fbb3bb056e84523864eae24fea78089ecd4d93/src/Core/src/Platform/Android/TextViewExtensions.cs
Then for example TextView.Text
is:
public unsafe ICharSequence? TextFormatted
{
[Register("setText", "(Ljava/lang/CharSequence;)V", "")]
set
{
IntPtr native_value = CharSequence.ToLocalJniHandle(value);
try
{
JniArgumentValue* __args = stackalloc JniArgumentValue[1];
*__args = new JniArgumentValue(native_value);
_members.InstanceMethods.InvokeNonvirtualVoidMethod("setText.(Ljava/lang/CharSequence;)V", this, __args);
}
finally
{
JNIEnv.DeleteLocalRef(native_value);
GC.KeepAlive(value);
}
}
}
public string? Text
{
set
{
((Java.Lang.Object)(TextFormatted = ((value == null) ? null : new Java.Lang.String(value))))?.Dispose();
}
}
-
We could make a
Java.Lang.String
pool to reuseJNIEnv.NewString()
instances. This is only helpful if the same C# strings are passed in. This might be a narrow improvement. -
We could write a "message pipe" for running many methods in Java at once from a single C# call. This would only cross the JNI boundary once instead of many times when MAUI sets many properties from C#.
An idea of what the Java side would look like would be:
public class DotNetPipe {
public final static int TEXTVIEW_TEXT = 1;
public final static int TEXTVIEW_TYPEFACE = 2;
public final static int TEXTVIEW_TEXTCOLOR = 3;
public final static int TEXTVIEW_TEXTSIZE = 4;
public static void Send (Object[] message) {
int messageId;
for (int i = 0; i < message.length;) {
messageId = (int)message[i];
if (messageId == TEXTVIEW_TEXT) {
getTextView(message, i).setText((String)message[i + 2]);
i += 3;
} else if (messageId == TEXTVIEW_TYPEFACE) {
getTextView(message, i).setTypeface((Typeface) message[i + 2]);
i += 3;
} else if (messageId == TEXTVIEW_TEXTCOLOR) {
getTextView(message, i).setTextColor((int) message[i + 2]);
i += 3;
} else if (messageId== TEXTVIEW_TEXTSIZE) {
getTextView(message, i).setTextSize((int) message[i + 2], (float) message[i + 3]);
i += 4;
} else {
// Assume unknown messages of size 3
i += 3;
}
}
}
private static TextView getTextView(Object[] message, int i)
{
return ((TextView)message[i + 1]);
}
}