【问题标题】:How to implement Cocoa copyWithZone on derived object in MonoMac C#?如何在 MonoMac C# 的派生对象上实现 Cocoa copyWithZone?
【发布时间】:2012-11-04 07:45:03
【问题描述】:

我目前正在移植一个基于 Winforms 的小型 .NET 应用程序,以便将原生 Mac 前端与 MonoMac 一起使用。该应用程序有一个带有图标和文本的 TreeControl,这在 Cocoa 中不存在。

到目前为止,我已经移植了 Apple 的 DragNDrop 示例中几乎所有的 ImageAndTextCell 代码:https://developer.apple.com/library/mac/#samplecode/DragNDropOutlineView/Listings/ImageAndTextCell_m.html#//apple_ref/doc/uid/DTS40008831-ImageAndTextCell_m-DontLinkElementID_6,它作为自定义单元分配给 NSOutlineView。

它似乎工作得几乎完美,除了我还没有弄清楚如何正确移植copyWithZone 方法。不幸的是,这意味着 NSOutlineView 正在制作的内部副本没有图像字段,并导致图像在展开和折叠操作期间短暂消失。有问题的objective-c代码是:

- (id)copyWithZone:(NSZone *)zone {
    ImageAndTextCell *cell = (ImageAndTextCell *)[super copyWithZone:zone];
    // The image ivar will be directly copied; we need to retain or copy it.
    cell->image = [image retain];
    return cell;
}

第一行让我很头疼,因为 MonoMac 没有公开 copyWithZone 方法,我不知道如何调用它。

更新

根据当前的答案以及额外的研究和测试,我提出了多种复制对象的模型。

static List<ImageAndTextCell> _refPool = new List<ImageAndTextCell>();

// Method 1

static IntPtr selRetain = Selector.GetHandle ("retain");

[Export("copyWithZone:")]
public virtual NSObject CopyWithZone(IntPtr zone) {
    ImageAndTextCell cell = new ImageAndTextCell() {
        Title = Title,
        Image = Image,
    };

    Messaging.void_objc_msgSend (cell.Handle, selRetain);

    return cell;
}

// Method 2

[Export("copyWithZone:")]
public virtual NSObject CopyWithZone(IntPtr zone) {
    ImageAndTextCell cell = new ImageAndTextCell() {
        Title = Title,
        Image = Image,
    };

    _refPool.Add(cell);

    return cell;
}

[Export("dealloc")]
public void Dealloc ()
{
    _refPool.Remove(this);
    this.Dispose();
}

// Method 3

static IntPtr selRetain = Selector.GetHandle ("retain");

[Export("copyWithZone:")]
public virtual NSObject CopyWithZone(IntPtr zone) {
    ImageAndTextCell cell = new ImageAndTextCell() {
        Title = Title,
        Image = Image,
    };

    _refPool.Add(cell);
    Messaging.void_objc_msgSend (cell.Handle, selRetain);

    return cell;
}

// Method 4

static IntPtr selRetain = Selector.GetHandle ("retain");
static IntPtr selRetainCount = Selector.GetHandle("retainCount");

[Export("copyWithZone:")]
public virtual NSObject CopyWithZone (IntPtr zone)
{
    ImageAndTextCell cell = new ImageAndTextCell () {
        Title = Title,
        Image = Image,
    };

    _refPool.Add (cell);
    Messaging.void_objc_msgSend (cell.Handle, selRetain);

    return cell;
}

public void PeriodicCleanup ()
{
    List<ImageAndTextCell> markedForDelete = new List<ImageAndTextCell> ();

    foreach (ImageAndTextCell cell in _refPool) {
        uint count = Messaging.UInt32_objc_msgSend (cell.Handle, selRetainCount);
        if (count == 1)
            markedForDelete.Add (cell);
    }

    foreach (ImageAndTextCell cell in markedForDelete) {
        _refPool.Remove (cell);
        cell.Dispose ();
    }
}

// Method 5

static IntPtr selCopyWithZone = Selector.GetHandle("copyWithZone:");

[Export("copyWithZone:")]
public virtual NSObject CopyWithZone(IntPtr zone) {
    IntPtr copyHandle = Messaging.IntPtr_objc_msgSendSuper_IntPtr(SuperHandle, selCopyWithZone, zone);
    ImageAndTextCell cell = new ImageAndTextCell(copyHandle) {
        Image = Image,
    };

    _refPool.Add(cell);

    return cell;
}

方法一:增加非托管对象的保留计数。非托管对象将永远持续存在(我认为?dealloc 从未调用过),托管对象将提前收获。似乎是全能双输,但在实践中运行。

方法二:保存托管对象的引用。非托管对象被单独留下,调用者似乎在合理的时间调用了 dealloc。此时托管对象被释放和处置。这似乎是合理的,但不利的一面是基本类型的 dealloc 不会运行(我认为?)

方法 3: 增加保留计数并保存引用。非托管和托管对象永远泄漏。

方法 4: 通过添加定期运行的清理函数(例如在每个新 ImageAndTextCell 对象的 Init 期间)扩展方法 3。清理函数检查存储对象的保留计数。保留计数为 1 意味着调用者已释放它,所以我们也应该这样做。理论上应该消除泄漏。

方法 5: 尝试在基类型上调用 copyWithZone 方法,然后使用结果句柄构造一个新的 ImageAndTextView 对象。似乎做对了(克隆了基础数据)。在内部,NSObject 会增加这样构造的对象的保留计数,因此我们还使用 PeriodicCleanup 函数在这些对象不再使用时释放它们。

基于上述情况,我认为方法 5 是最好的方法,因为它应该是唯一能够生成真正正确的基本类型数据副本的方法,但我不知道该方法是否具有内在危险(我我也对 NSObject 的底层实现做了一些假设)。到目前为止,“还”没有发生任何不好的事情,但如果有人能够审查我的分析,那么我会更有信心继续前进。

【问题讨论】:

  • 您可以使用retain 选择器并在您的图像上调用它。并且 FWIW 将托管对象的引用返回给运行时而不自己持有它最终会让你受益。 GC 无法跟踪引用。
  • -copyWithZone: 应该将一个实例复制到该内存的 zone 中。我对 Mono 了解的不够多,但是有什么特定的方法可以处理 NSZone?
  • @CodaFi 问题是 Cocoa (Touch) 中的 zone 参数已被弃用,它应该被忽略,您不必为此过多地占用自己。
  • @H2CO3,你说得对。有人实际上试图实现涉及 NSZone 的方法,这让我大吃一惊。

标签: c# objective-c cocoa mono monomac


【解决方案1】:

Bug 1086 中详细讨论了这个问题

嗯,这是一个引用计数/所有权问题:

您在 MyDataSource.GetObjectValue() 中创建一个新的 MyObject 实例,然后 将其返回到本机代码,而不保留对它的引用。回来后, 您不再拥有该对象,但托管垃圾收集器不拥有 知道。

只需将对象存储在列表中,如下所示:

List<MyObject> list;

public MyDataSource ()
{
    list = new List<MyObject> ();
    for (int i = 0; i < 10; i++) {
        list.Add (new MyObject { Text = "My Row " + i });
    }
}

public override NSObject GetObjectValue (NSTableView tableView,
    NSTableColumn tableColumn, int row)
{
    return list [row];
}

public override int GetRowCount (NSTableView tableView)
{
    return list.Count;
}

但是,这并不能解决您的 copyWithZone: 问题。在这里,存储 在本地克隆对象不是一种选择,这会泄漏大量内存 迅速地。相反,您需要在克隆对象上调用保留。 不幸的是,NSObject.Retain() 在 MonoMac.dll 中是内部的,但你可以 只需这样做:

static IntPtr selRetain = Selector.GetHandle ("retain");
[Export("copyWithZone:")]
public NSObject CopyWithZone (IntPtr zone)
{
    var cloned = new MyObject { Text = this.Text };
    Messaging.void_objc_msgSend (cloned.Handle, selRetain);
    return cloned;
}

根据记忆,最后一个示例中的代码不完整,您必须将两个示例结合起来并在列表(或其他集合)中跟踪新的MyObject

【讨论】:

  • 第二个代码块似乎在实践中解决了我的问题,但我不明白你的最后评论。单独保留还不够吗?如果我确实将对象存储在某个列表中,那么这些引用将如何被释放?这些物体被克隆在 Cocoa 的肠子里,我几乎永远看不到它们。
  • 我相信(我对此的理解不是很好)任何对您返回到本机运行时的托管对象的引用都必须在您的应用程序中至少有一个引用。否则 GC 会认为该对象已经成熟,可以获取了。当对象引用回到 Mono 中时,它引用了一个不再存在的对象并繁荣!
  • 是否保证调用复制方法的非托管代码也可以在复制的对象上调用 Dispose(通过 destroy 或 dealloc 或其他方式)?这是我能想到的唯一可靠地清理全局对象引用池的地方。
  • @JustinAquadro 不确定,我已经有一段时间没有这样做了,但我想我在数据源中保留了一个项目列表,您可以在窗口关闭时对其进行清理。
  • 如果对象被保留,答案似乎是“否”。我已经通过对不同解决方案的一些分析更新了我的问题。在这一点上,不完全知道我的解决方案是否安全。
【解决方案2】:

到目前为止,我还没有发现任何问题的证据,所以我很乐意采用我在问题更新中概述的“方法 5”,我将在此处复制并提供一些额外的解释:

// An additional constructor
public ImageAndTextCell (IntPtr handle)
    : base(handle)
{
}

// Cocoa Selectors
static IntPtr selRetainCount = Selector.GetHandle("retainCount");
static IntPtr selCopyWithZone = Selector.GetHandle("copyWithZone:");

static List<ImageAndTextCell> _refPool = new List<ImageAndTextCell>();

// Helper method to be called at some future point in managed code to release
// managed instances that are no longer needed.
public void PeriodicCleanup ()
{
    List<ImageAndTextCell> markedForDelete = new List<ImageAndTextCell> ();

    foreach (ImageAndTextCell cell in _refPool) {
        uint count = Messaging.UInt32_objc_msgSend (cell.Handle, selRetainCount);
        if (count == 1)
            markedForDelete.Add (cell);
    }

    foreach (ImageAndTextCell cell in markedForDelete) {
        _refPool.Remove (cell);
        cell.Dispose ();
    }
}

// Overriding the copy method
[Export("copyWithZone:")]
public virtual NSObject CopyWithZone(IntPtr zone) {
    IntPtr copyHandle = Messaging.IntPtr_objc_msgSendSuper_IntPtr(SuperHandle, selCopyWithZone, zone);
    ImageAndTextCell cell = new ImageAndTextCell(copyHandle) {
        Image = Image,
    };

    _refPool.Add(cell);

    return cell;
}

通过在基础对象上调用 copyWithZone: 选择器(通过 SuperHandle),底层 Cocoa 子系统将克隆非托管对象并将句柄返回给它,其保留计数已设置为 1(标准 obj-c 复制约定)。然后可以使用克隆的对象句柄构造派生的 C# 对象,因此克隆的实例成为支持对象。然后,克隆任何属于派生类型的托管 C# 好东西就很简单了。

正如 ta.speot.所指出的,在某处保留托管类型的引用也是必要的。如果没有引用,则该对象将在方法结束时成为垃圾收集的候选对象。对象的非托管部分在返回时是安全的,因为它在调用复制选择器时具有正的保留计数。我选择将引用存储在静态列表中,然后定期从将遍历列表的其他代码部分调用清理方法,检查相应的非托管对象是否有任何其他所有者,如果没有,则处置对象。请注意,我正在检查计数 1 而不是 0,因为我们复制的对象实际上被保留了两次:一次由复制选择器保留,一次由 NSObject 构造函数保留。 Monomac 运行时系统会在托管方被处置/收集时负责处置非托管对象。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-01
    • 1970-01-01
    • 2021-12-28
    • 2014-09-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多