4

我目前正在移植一个基于 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 底层实现的一些假设)。到目前为止,“还”没有发生任何不好的事情,但如果有人能够审查我的分析,那么我会更有信心继续前进。

4

2 回答 2

2

这个问题在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在列表(或其他一些集合)中跟踪新的示例。

于 2012-11-04T07:53:52.863 回答
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.is 所指出的,还需要在某处保留托管类型的引用。如果没有引用,则该对象将在方法结束时成为垃圾回收的候选对象。对象的非托管部分在返回时是安全的,因为它在调用复制选择器时具有正的保留计数。我选择将引用存储在静态列表中,然后定期从将遍历列表的其他代码部分调用清理方法,检查相应的非托管对象是否有任何其他所有者,如果没有,则处置对象。请注意,我正在检查计数 1 而不是 0,因为我们复制的对象实际上被保留了两次:一次由复制选择器保留,一次由 NSObject 构造函数保留。

于 2012-11-25T05:00:48.577 回答