Xamarin.iOS 中的 Objective-C 选择器

Objective-C 语言基于选择器。 选择器是可以发送到对象或类的消息Xamarin.iOS 将实例选择器映射到实例方法,并将类选择器映射到静态方法。

与普通 C 函数(以及 C++ 成员函数)不同,无法使用 P/Invoke 直接调用选择器,而是要使用以下函数将选择器发送到 Objective-C 类或实例:objc_msgSend 函数。

有关 Objective-C 中的消息的详细信息,请参阅 Apple 的使用对象指南。

示例

假设你想调用 NSString 上的 sizeWithFont:forWidth:lineBreakMode: 选择器。 声明(摘自 Apple 文档)是:

- (CGSize)sizeWithFont:(UIFont *)font forWidth:(CGFloat)width lineBreakMode:(UILineBreakMode)lineBreakMode

此 API 具有以下特征:

  • Unified API 的返回类型为 CGSize
  • font 参数是一个 UIFont(以及从 NSObject 派生的类型(间接)),并映射到 System.IntPtr
  • width 参数(一个 CGFloat)映射到 nfloat
  • lineBreakMode 参数(一个 UILineBreakMode)已在 Xamarin.iOS 中绑定为 UILineBreakMode 枚举。

将所有内容放在一起后,objc_msgSend 声明应匹配:

CGSize objc_msgSend(
    IntPtr target,
    IntPtr selector,
    IntPtr font,
    nfloat width,
    UILineBreakMode mode
);

声明如下:

[DllImport (Constants.ObjectiveCLibrary, EntryPoint="objc_msgSend")]
static extern CGSize cgsize_objc_msgSend_IntPtr_float_int (
    IntPtr target,
    IntPtr selector,
    IntPtr font,
    nfloat width,
    UILineBreakMode mode
);

若要调用此方法,请使用如下代码:

NSString target = ...
Selector selector = new Selector ("sizeWithFont:forWidth:lineBreakMode:");
UIFont font = ...
nfloat width = ...
UILineBreakMode mode = ...

CGSize size = cgsize_objc_msgSend_IntPtr_float_int(
    target.Handle,
    selector.Handle,
    font == null ? IntPtr.Zero : font.Handle,
    width,
    mode
);

如果返回值是小于 8 字节的结构(例如切换到 Unified API 之前使用的较旧 SizeF),则上述代码将在模拟器上运行,但在设备上会崩溃。 若要调用返回小于 8 位的值的选择器,请声明 objc_msgSend_stret 函数:

[DllImport (MonoTouch.Constants.ObjectiveCLibrary, EntryPoint="objc_msgSend_stret")]
static extern void cgsize_objc_msgSend_stret_IntPtr_float_int (
    out CGSize retval,
    IntPtr target,
    IntPtr selector,
    IntPtr font,
    nfloat width,
    UILineBreakMode mode
);

若要调用此方法,请使用如下代码:

NSString      target = ...
Selector    selector = new Selector ("sizeWithFont:forWidth:lineBreakMode:");
UIFont          font = ...
nfloat          width = ...
UILineBreakMode mode = ...

CGSize size;

if (Runtime.Arch == Arch.SIMULATOR)
    size = cgsize_objc_msgSend_IntPtr_float_int(
        target.Handle,
        selector.Handle,
        font == null ? IntPtr.Zero : font.Handle,
        width,
        mode
    );
else
    cgsize_objc_msgSend_stret_IntPtr_float_int(
        out size,
        target.Handle, selector.Handle,
        font == null ? IntPtr.Zero: font.Handle,
        width,
        mode
    );

调用选择器

调用选择器需要执行三个步骤:

  1. 获取选择器目标。
  2. 获取选择器名称。
  3. 结合相应的参数调用 objc_msgSend

选择器目标

选择器目标是对象实例或 Objective-C 类。 如果目标是实例并且来自绑定的 Xamarin.iOS 类型,请使用 ObjCRuntime.INativeObject.Handle 属性。

如果目标是类,请使用 ObjCRuntime.Class 获取对类实例的引用,然后使用 Class.Handle 属性。

选择器名称

Apple 文档中列出了选择器名称。 例如,NSString 包括 sizeWithFont:sizeWithFont:forWidth:lineBreakMode: 选择器。 嵌入的冒号和尾部的冒号是选择器名称的一部分,不能省略。

获取选择器名称后,可为其创建一个 ObjCRuntime.Selector 实例。

调用 objc_msgSend

objc_msgSend 向对象发送消息(选择器)。 该函数系列至少采用两个必需参数:选择器目标(实例或类句柄)、选择器本身,以及选择器所需的任何参数。 实例和选择器参数必须为 System.IntPtr,并且所有剩余参数必须与选择器预期的类型匹配,例如,nint 匹配 int,或者 System.IntPtr 匹配所有 NSObject 派生类型。 使用 NSObject.Handle 属性,用于获取 Objective-C 类型实例的 IntPtr

有多个 objc_msgSend 函数:

  • 对于返回结构的选择器,请使用 objc_msgSend_stret。 在 ARM 上,这包括所有非枚举的返回类型或任何 C 内置类型(charshortintlongfloatdouble)的返回类型。 在 x86(模拟器)上,此方法需用于大于 8 字节的所有结构(CGSize 是 8 字节,在模拟器中不使用 objc_msgSend_stret)。
  • 对于仅在 x86 上返回浮点值的选择器,请使用 objc_msgSend_fpret。 在 ARM 上不需要使用该函数;请改用 objc_msgSend
  • objc_msgSend 函数用于所有其他选择器。

确定需要调用哪个 objc_msgSend 函数(模拟器和设备可能各自需要不同的方法)后,可以使用普通的 [DllImport] 方法来声明该函数以供稍后调用。

可以在 ObjCRuntime.Messaging 中找到一组预制的 objc_msgSend 声明。

模拟器和设备上的不同调用

如上所述,Objective-C 具有三种 objc_msgSend 方法:一种用于常规调用,一种用于返回浮点值(仅限 x86)的调用,一种用于返回结构值的调用。 后者在 ObjCRuntime.Messaging 中包含后缀 _stret

如果调用的方法将返回某些结构(规则如下所述),则必须以 out 值的形式调用该方法,并将返回值作为第一个参数:

// The following returns a PointF structure:
PointF ret;
Messaging.PointF_objc_msgSend_stret_PointF_IntPtr (out ret, this.Handle, selConvertPointFromWindow.Handle, point, window.Handle);

有关在 x86 和 ARM 上何时使用 _stret_ 方法的规则有所不同。 如果你希望绑定在模拟器和设备上都能正常工作,请添加如下代码:

if (Runtime.Arch == Arch.DEVICE)
{
    PointF ret;
    Messaging.PointF_objc_msgSend_stret_PointF_IntPtr (out ret, myHandle, selector.Handle);
    return ret;
}
else
{
    return Messaging.PointF_objc_msgSend_PointF_IntPtr (myHandle, selector.Handle);
}

使用 objc_msgSend_stret 方法

为 ARM 生成时,请将 objc_msgSend_stret 用于表示非枚举的任何值类型或枚举的任何基类型(intbyteshortlongdoublefloat)。

为 x86 生成时,请将 objc_msgSend_stret 用于任何非枚举的值类型或枚举的、其原生大小大于 8 字节的任何基类型(intbyteshortlongdoublefloat)。

创建自己的签名

如果需要,可以使用以下 gist 创建你自己的签名。