2012-11-04 22 views
4

我目前正在移植一個基於Winforms的小型.NET應用程序,使用MonoMac的本地Mac前端。該應用程序有一個帶有圖標和文本的TreeControl,它在Cocoa中不存在。如何在MonoMac C#中的派生對象上實現Cocoa copyWithZone?

到目前爲止,我移植了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是最好的方法,因爲它應該是唯一一個能夠生成基本類型數據的真正正確副本的方法,但我不知道方法是否具有內在危險性(我也對NSObject的底層實現做了一些假設)。到目前爲止,沒有什麼不好的事情發生在「尚未」,但如果任何人都能夠審查我的分析,那麼我會更有信心前行。

+0

您可以將手放在'retain'選擇器上並在圖像上調用它。而FWIW將一個被管理對象的引用返回給運行時,而不用自己堅持,它會讓你最終獲得成功。 GC不能跟蹤參考。 –

+0

'-copyWithZone:'應該將一個實例複製到內存的* zone *中。我不太瞭解Mono,但是有沒有處理NSZone的具體方法? – CodaFi

+0

@CodaFi問題在於Cocoa(Touch)中的''''''參數已被棄用,並且被忽略,您不必過多地佔用自己。 – 2012-11-04 07:52:23

回答

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運行時系統將負責在託管端處置/收集時處置非託管對象。

+0

夢幻般的研究。做得好。 – johnrubythecat

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

+0

第二個代碼塊似乎在實踐中解決了我的問題,但我不明白你最後的評論。保留不夠單獨嗎?如果我確實將對象存儲在某個列表中,那麼將如何發佈引用?這些物體被克隆在可可的腸子裏,我幾乎從不會看到它們。 –

+0

我相信(並且我對此的理解並不是那麼棒),即任何對您返回到本機運行時的託管對象的引用在您的應用程序中都必須至少有一個引用。否則,GC會認爲該物體已經成熟。當對象引用回到單聲道時,它指的是一個不再存在的對象,並且繁榮! –

+0

調用copy方法的非託管代碼是否可以保證在複製的對象上調用Dispose(通過destroy或dealloc或其他)?這是我能想到的唯一可以清理全局對象參考池的地方。 –