Description
Context: http://github.com/xamarin/monodroid/commit/c0ad930298013425e4797f89b0c9243c22827152
Context: #858
From xamarin/monodroid@c0ad930298013425e4797f89b0c9243c22827152 (2011-Nov-28):
Remember xamarin/monodroid@62a4766, and how the issue was an intermixing of static and
instance data on the interface Invokers?One of the related questions was why the
Invoker.class_ref
field was
an instance field in the first place. We use staticclass_ref
fields
everywhere else, so being an instance field was certainly different,
but my efforts to figure out why via the commit messages and
git blame
didn't pan out...The reason
Invoker.class_ref
was an instance field is as follows:
consider the following set of Java interfaces:package android.view; public interface ViewManager { void addView(View view, ViewGroup.LayoutParams params); void updateViewLayout(View view, ViewGroup.LayoutParams params); void removeView(View view); } public interface WindowManager extends ViewManager { Display getDefaultDisplay(); void removeViewImmediate(View view); }The generated C#
IWindowManagerInvoker
type is (more or less):[global::Android.Runtime.Register ("android/view/WindowManager", DoNotGenerateAcw=true)] internal partial class IWindowManagerInvoker : Java.Lang.Object, IWindowManager { static IntPtr class_ref = JNIEnv.FindClass ("android/view/WindowManager"); // ... static IntPtr id_addView; public void AddView (Android.Views.View view, Android.Views.ViewGroup.LayoutParams @params) { if (id_addView == IntPtr.Zero) id_addView = JNIEnv.GetMethodID (class_ref, "addView", "(Landroid/view/View;Landroid/view/ViewGroup$LayoutParams;)V"); JNIEnv.CallVoidMethod (Handle, id_addView, new JValue (JNIEnv.ToJniHandle (view)), new JValue (JNIEnv.ToJniHandle (@params))); } }Nice. Straightforward. So let's invoke
IViewManager.AddView()
via an
IWindowManager
instance (seeHello.cs
):var die = new TextView (this) { Text = "Added!", }; WindowManager.AddView (die, new WindowManagerLayoutParams ());What could possibly go wrong™?
D/dalvikvm( 6645): GetMethodID: method not found: Landroid/view/WindowManager;.addView:(Landroid/view/View;Landroid/view/ViewGroup$LayoutParams;)V I/MonoDroid( 6645): UNHANDLED EXCEPTION: Java.Lang.NoSuchMethodError: Exception of type 'Java.Lang.NoSuchMethodError' was thrown. I/MonoDroid( 6645): at Android.Runtime.JNIEnv.GetMethodID (intptr,string,string) <0x0007c> I/MonoDroid( 6645): at Android.Views.IWindowManagerInvoker.AddView (Android.Views.View,Android.Views.ViewGroup/LayoutParams) <0x00067> I/MonoDroid( 6645): at Mono.Samples.Hello.HelloActivity.OnCreate (Android.OS.Bundle) <0x005ab> I/MonoDroid( 6645): at Android.App.Activity.n_OnCreate_Landroid_os_Bundle_ (intptr,intptr,intptr) <0x00057> I/MonoDroid( 6645): at (wrapper dynamic-method) object.ecadbe0b-9124-445e-a498-f351075f6c89 (intptr,intptr,intptr) <0x00033>
Ouch.
What goes wrong is that the
addView()
method is declared in the
android.view.ViewManager
interface, not theandroid.view.WindowManager
interface, but we're doing aGetMethodID()
onWindowManager
. This
sounds like it should work --WindowManager
does implement the
ViewManager
interface, after all -- butGetMethodID()
doesn't like it
and bombs accordingly.This is why
Invoker.class_ref
was an instance field: because we need
to callGetMethodID()
against the actual runtime type of the target
object, not against the (compile-time) interface type.Consequently, in order to fix the bug fixed in xamarin/monodroid@62a4766 in addition to
the bug outlined above, we need to not only makeInvoker.class_ref
an
instance field (a'la pre- xamarin/monodroid@62a47668), but we also need to make all the
GetMethodID
values instance members as well (to fix the issue that
xamarin/monodroid@62a4766 fixed).
The structure of that fix are still with us today; consider: https://github.com/xamarin/java.interop/blob/dcdcce13220d98a848ae3d3598b9b87b22a62c84/tests/generator-Tests/expected/java.lang.Enum/Java.Lang.IComparable.cs
IComparableInvoker.class_ref
is an instance field.IComparableInvoker.id_compareTo_Ljava_lang_Object_
is an instance field.IComparableInvoker.CompareTo()
looks up thejmethodID
, storing intoid_compareTo_Ljava_lang_Object_
. As this is per-instance data, only subsequentIComparable.CompareTo()
invocations on this instance will be cached; other instances will be separate.
Consequently, no interface *Invoker
data is shared between instances; each instance must re-lookup jmethodID
values.
Additionally, in the context of #858, none of the infrastructure currently used by interface *Invoker
types will exist.
Thus, the proposal: optimize interface *Invoker
types by using JniPeerMembers
, one per implemented interface. For IWindowManager
, this would be:
[global::Android.Runtime.Register ("android/view/WindowManager", DoNotGenerateAcw=true)]
internal partial class IWindowManagerInvoker : Java.Lang.Object, IWindowManager {
// One JniPeerMembers value per implemented interface; as IWindowManager implements IViewManager, we need two values.
static JniPeerMembers _members_ViewManager = new XAPeerMembers ("android/view/ViewManager", typeof (IWindowManagerInvoker), isInterface: true);
static JniPeerMembers _members_WindowManager = new XAPeerMembers ("android/view/WindowManager", typeof (IWindowManagerInvoker), isInterface: true);
public override JniPeerMembers JniPeerMembers {
get => _members_WindowManager;
}
public override Type ThresholdType {
get => _members.ManagedPeerType;
}
new IntPtr class_ref;
public override IntPtr ThresholdClass {
get => class_ref;
}
public IWindowManagerInvoker (IntPtr handle, JniHandleOwnership transfer) : base (Validate (handle), transfer)
{
IntPtr local_ref = JNIEnv.GetObjectClass (((global::Java.Lang.Object) this).Handle);
this.class_ref = JNIEnv.NewGlobalRef (local_ref);
JNIEnv.DeleteLocalRef (local_ref);
}
public unsafe void AddView (Android.Views.View? view, Android.Views.ViewGroup.LayoutParams? @params)
{
const string __id = "addView.(Landroid/view/View;Landroid/view/ViewGroup$LayoutParams;)V";
JniArgumentValue* __args = stackalloc JniArgumentValue [2];
__args [0] = new JniArgumentValue ((view == null) ? IntPtr.Zero : ((global::Java.Lang.Object) view).Handle);
__args [1] = new JniArgumentValue ((@params == null) ? IntPtr.Zero : ((global::Java.Lang.Object) @params).Handle);
// As AddView() is from `IViewManager`, use `_members_ViewManager` to invoke it.
_members_ViewManager.InstanceMethods.InvokeAbstractVoidMethod (__id, this, __args);
}
// …
// Note: SKIP java_class_ref, GetObject(), Validate(); those aren't needed anymore.
}
In particular, all methods which implement the IViewManager
interface would use _members_ViewManager
, while all methods which implement the IWindowManager
interface would use _members_WindowManager
.
This should allow jmethodID
values to be cached across instances, while also allowing appropriate JNI method lookup.