如何在MonoMac C#中的派生对象上实现Cocoa copyWithZone?

时间:2012-11-04 07:45:03

标签: c# objective-c cocoa mono monomac

我目前正在移植一个基于Winforms的小型.NET应用程序,以使用MonoMac的原生Mac前端。该应用程序有一个带有图标和文本的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;
}

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

方法2:保存托管对象的引用。非托管对象保持不变,并且呼叫者在合理的时间调用dealloc。此时,托管对象被释放并处置。这似乎是合理的,但在不利方面,基本类型的dealloc将无法运行(我认为?)

方法3:增加保留计数并保存参考。非托管和托管对象永远泄密。

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

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

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

2 个答案:

答案 0 :(得分: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:问题。在这里,存储   本地克隆的对象不是一个选项,这会泄漏大量内存   很快。相反,您需要在克隆对象上调用retain。   不幸的是,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

答案 1 :(得分: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构造函数。处理/收集管理方时,Monomac运行时系统将负责处理非托管对象。