4

私は現在、小さな 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:定期的に実行されるクリーンアップ関数を追加することにより、方法 3 を拡張します (たとえば、新しい ImageAndTextCell オブジェクトの初期化中)。クリーンアップ機能は、格納されたオブジェクトの保持カウントをチェックします。1 の保持カウントは、呼び出し元がそれを解放したことを意味するため、同様に保持する必要があります。理論上は漏れをなくすはずです。

方法 5:基本型で copyWithZone メソッドの呼び出しを試み、結果のハンドルを使用して新しい ImageAndTextView オブジェクトを構築します。正しいことをしているようです (基本データが複製されます)。内部的には、NSObject はこのように構築されたオブジェクトの保持カウントを増加させるため、使用されなくなったオブジェクトを解放するために PeriodicCleanup 関数も使用します。

上記に基づいて、基本型データの真に正しいコピーが得られる唯一の方法であるため、方法 5 が最良の方法であると考えていますが、この方法が本質的に危険であるかどうかはわかりません (私も作成しています)。 NSObject の基本的な実装に関するいくつかの仮定)。これまでのところ「まだ」悪いことは何も起きていませんが、誰かが私の分析を精査できるなら、私は前進することにもっと自信を持つでしょう.

4

2 に答える 2

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 が指摘しているように、マネージ型の参照をどこかに保持することも必要です。参照がない場合、オブジェクトはメソッドの最後でガベージ コレクションの対象になります。オブジェクトの管理されていない部分は、コピー セレクターへの呼び出しからの保持カウントが正であるため、返されたときに安全です。参照を静的リストに格納することを選択し、コードの他の部分から定期的にクリーンアップ メソッドを呼び出して、リストを走査し、対応するアンマネージ オブジェクトに他の所有者がいるかどうかを確認し、そうでない場合はオブジェクトを破棄します。コピーされたオブジェクトは実際には 2 回保持されているため、0 ではなく 1 のカウントをチェックしていることに注意してください。

于 2012-11-25T05:00:48.577 に答える
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;
}

メモリから、最後の例のコードは完全ではありません。2 つの例を結合MyObjectし、リスト (または他のコレクション) で新しいものも追跡する必要があります。

于 2012-11-04T07:53:52.863 に答える