我目前正在移植一个基于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的底层实现做了一些假设。到目前为止还没有发生任何不好的事情,但是如果有人能够审查我的分析,那么我会更有信心前进。
答案 0 :(得分:2)
嗯,这是一个重新计算/所有权问题:
然后在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运行时系统将负责处理非托管对象。