20

私はこの質問を言い換えました。

.netオブジェクトがCOMiteropを介してCOMクライアントに公開されると、CCW(COM Callable Wrapper)が作成され、これはCOMクライアントとマネージド.netオブジェクトの間に配置されます。

COMの世界では、オブジェクトは他のオブジェクトが参照している数をカウントします。その参照カウントがゼロになると、オブジェクトは削除/解放/収集されます。これは、COMオブジェクトの終了が決定論的であることを意味します(決定論的終了には.netのUsing / IDisposeを使用し、オブジェクトファイナライザーは非決定論的です)。

各CCWはCOMオブジェクトであり、他のCOMオブジェクトと同様に参照カウントされます。CCWが停止すると(参照カウントがゼロになる)、GCはCCWがラップしたCLRオブジェクトを見つけることができず、CLRオブジェクトは収集の対象になります。幸せな日々、すべてが世界とうまくいっています。

私がやりたいのは、CCWが停止したとき(つまり、参照カウントがゼロになったとき)をキャッチし、これをCLRオブジェクトに通知することです(たとえば、管理対象オブジェクトでDisposeメソッドを呼び出すことによって)。

では、 CLRクラスのCOM呼び出し可能ラッパーの参照カウントがいつゼロになるかを知ることは可能ですか?
および/または
.netでCCWのAddRefおよびReleaseRefの実装を提供することは可能ですか?

そうでない場合は、これらのDLLをATLに実装することもできます(ATLのサポートは必要ありません。ありがとうございます)。ロケット科学ではありませんが、実世界のC ++やATLを社内で開発しているのは私だけなので、やりたくありません。

背景
私はいくつかの古いVB6ActiveXDLLを.netで書き直しています(正確にはC#ですが、これはC#の問題というよりも.net / COMの相互運用の問題です)。古いVB6オブジェクトの一部は、オブジェクトが終了したときにアクションを実行するために参照カウントに依存しています(上記の参照カウントの説明を参照)。これらのDLLには重要なビジネスロジックは含まれていません。これらは、VBScriptを使用して統合するクライアントに提供するユーティリティおよびヘルパー関数です。

私がやろうとしていないこと

  • ガベージコレクタを使用する代わりに、カウント.netオブジェクトを参照します。私はGCに非常に満足しています、私の問題はGCにありません。
  • オブジェクトファイナライザーを使用します。ファイナライザーは非決定論的です。この場合、決定論的終了が必要です(.netのUsing / IDisposeイディオムなど)
  • アンマネージC++でIUnknownを実装
    するC++ルートを使用する場合は、ATLを使用します。ありがとうございます。
  • Vb6を使用してこれを解決するか、VB6オブジェクトを再利用します。この演習の全体的なポイントは、Vb6へのビルドの依存関係を取り除くことです。

ありがとう
BW

受け入れられた回答は、唯一の(おそらく実行可能な).netベースの回答を考案したSteve Steinerと、非常に単純なATLソリューションを考案したEarwicker
に感謝します。

しかし、受け入れられた答えはBigtoeに行きます。彼は、.netオブジェクトをVbScriptオブジェクト(私は正直だとは思っていませんでした)でラップし、VbScriptの問題に対する単純なVbScriptソリューションを効果的に提供することを提案しています。

ありがとうございます。

4

10 に答える 10

6

これはやや古い質問だと思いますが、実際に作業を依頼されたのは少し前のことです。

作成されたオブジェクトのVTBLのReleaseを、すべての参照が解放されたときにDisposeを呼び出すカスタム実装に置き換えます。これが常に機能するという保証はないことに注意してください。主な前提は、標準CCWのすべてのインターフェイスのすべてのReleaseメソッドが同じメソッドであるということです。

自己責任。:)

/// <summary>
/// I base class to provide a mechanism where <see cref="IDisposable.Dispose"/>
/// will be called when the last reference count is released.
/// 
/// </summary>
public abstract class DisposableComObject: IDisposable
{
    #region Release Handler, ugly, do not look

    //You were warned.


    //This code is to enable us to call IDisposable.Dispose when the last ref count is released.
    //It relies on one things being true:
    // 1. That all COM Callable Wrappers use the same implementation of IUnknown.


    //What Release() looks like with an explit "this".
    private delegate int ReleaseDelegate(IntPtr unk);

    //GetFunctionPointerForDelegate does NOT prevent GC ofthe Delegate object, so we'll keep a reference to it so it's not GC'd.
    //That would be "bad".
    private static ReleaseDelegate myRelease = new ReleaseDelegate(Release);
    //This is the actual address of the Release function, so it can be called by unmanaged code.
    private static IntPtr myReleaseAddress = Marshal.GetFunctionPointerForDelegate(myRelease);


    //Get a Delegate that references IUnknown.Release in the CCW.
    //This is where we assume that all CCWs use the same IUnknown (or at least the same Release), since
    //we're getting the address of the Release method for a basic object.
    private static ReleaseDelegate unkRelease = GetUnkRelease();
    private static ReleaseDelegate GetUnkRelease()
    {
        object test = new object();
        IntPtr unk = Marshal.GetIUnknownForObject(test);
        try
        {
            IntPtr vtbl = Marshal.ReadIntPtr(unk);
            IntPtr releaseAddress = Marshal.ReadIntPtr(vtbl, 2 * IntPtr.Size);
            return (ReleaseDelegate)Marshal.GetDelegateForFunctionPointer(releaseAddress, typeof(ReleaseDelegate));
        }
        finally
        {
            Marshal.Release(unk);
        }
    }

    //Given an interface pointer, this will replace the address of Release in the vtable
    //with our own. Yes, I know.
    private static void HookReleaseForPtr(IntPtr ptr)
    {
        IntPtr vtbl = Marshal.ReadIntPtr(ptr);
        IntPtr releaseAddress = Marshal.ReadIntPtr(vtbl, 2 * IntPtr.Size);
        Marshal.WriteIntPtr(vtbl, 2 * IntPtr.Size, myReleaseAddress);
    }

    //Go and replace the address of CCW Release with the address of our Release
    //in all the COM visible vtables.
    private static void AddDisposeHandler(object o)
    {
        //Only bother if it is actually useful to hook Release to call Dispose
        if (Marshal.IsTypeVisibleFromCom(o.GetType()) && o is IDisposable)
        {
            //IUnknown has its very own vtable.
            IntPtr comInterface = Marshal.GetIUnknownForObject(o);
            try
            {
                HookReleaseForPtr(comInterface);
            }
            finally
            {
                Marshal.Release(comInterface);
            }
            //Walk the COM-Visible interfaces implemented
            //Note that while these have their own vtables, the function address of Release
            //is the same. At least in all observed cases it's the same, a check could be added here to
            //make sure the function pointer we're replacing is the one we read from GetIUnknownForObject(object)
            //during initialization
            foreach (Type intf in o.GetType().GetInterfaces())
            {
                if (Marshal.IsTypeVisibleFromCom(intf))
                {
                    comInterface = Marshal.GetComInterfaceForObject(o, intf);
                    try
                    {
                        HookReleaseForPtr(comInterface);
                    }
                    finally
                    {
                        Marshal.Release(comInterface);
                    }
                }
            }
        }
    }

    //Our own release. We will call the CCW Release, and then if our refCount hits 0 we will call Dispose.
    //Note that is really a method int IUnknown.Release. Our first parameter is our this pointer.
    private static int Release(IntPtr unk)
    {
        int refCount = unkRelease(unk);
        if (refCount == 0)
        {
            //This is us, so we know the interface is implemented
            ((IDisposable)Marshal.GetObjectForIUnknown(unk)).Dispose();
        }
        return refCount;
    }
    #endregion

    /// <summary>
    /// Creates a new <see cref="DisposableComObject"/>
    /// </summary>
    protected DisposableComObject()
    {
        AddDisposeHandler(this);
    }

    /// <summary>
    /// Calls <see cref="Dispose"/> with false.
    /// </summary>
    ~DisposableComObject()
    {
        Dispose(false);
    }

    /// <summary>
    /// Override to dispose the object, called when ref count hits or during GC.
    /// </summary>
    /// <param name="disposing"><b>true</b> if called because of a 0 refcount</param>
    protected virtual void Dispose(bool disposing)
    {

    }

    void IDisposable.Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}
于 2011-08-01T22:18:12.160 に答える
5

OK 皆さん、これは別の試みです。実際には、「Windows スクリプト コンポーネント」を使用して .NET COM オブジェクトをラップし、その方法でファイナライズすることができます。値を加算できる単純な .NET Calculator を使用した完全なサンプルを次に示します。そこから概念を理解できると確信しています。これにより、VB ランタイム、ATL の問題が完全に回避され、すべての主要な WIN32/WIN64 プラットフォームで利用可能な Windows Scripting Host が使用されます。

DemoLib という名前空間に、Calculator という単純な COM .NET クラスを作成しました。これは IDisposable を実装していることに注意してください。ここでは、デモ目的で、画面に何かを配置して、それが終了したことを示しています。ここでは、単純にするために .NET とスクリプトで完全に vb にこだわっていますが、.NET 部分は C# などにすることができます。このファイルを保存するときは、regsvr32 に登録する必要があります。保存する必要があります。 CalculatorLib.wsc のようなものとして。

<ComClass(Calculator.ClassId, Calculator.InterfaceId, Calculator.EventsId)> _
Public Class Calculator
    Implements IDisposable
#Region "COM GUIDs"
    ' These  GUIDs provide the COM identity for this class 
    ' and its COM interfaces. If you change them, existing 
    ' clients will no longer be able to access the class.
    Public Const ClassId As String = "68b420b3-3aa2-404a-a2d5-fa7497ad0ebc"
    Public Const InterfaceId As String = "0da9ab1a-176f-49c4-9334-286a3ad54353"
    Public Const EventsId As String = "ce93112f-d45e-41ba-86a0-c7d5a915a2c9"
#End Region
    ' A creatable COM class must have a Public Sub New() 
    ' with no parameters, otherwise, the class will not be 
    ' registered in the COM registry and cannot be created 
    ' via CreateObject.
    Public Sub New()
        MyBase.New()
    End Sub
    Public Function Add(ByVal x As Double, ByVal y As Double) As Double
        Return x + y
    End Function
    Private disposedValue As Boolean = False        ' To detect redundant calls
    ' IDisposable
    Protected Overridable Sub Dispose(ByVal disposing As Boolean)
        If Not Me.disposedValue Then
            If disposing Then
                MsgBox("Disposed called on .NET COM Calculator.")
            End If
        End If
        Me.disposedValue = True
    End Sub
#Region " IDisposable Support "
    ' This code added by Visual Basic to correctly implement the disposable pattern.
    Public Sub Dispose() Implements IDisposable.Dispose
        ' Do not change this code.  Put cleanup code in Dispose(ByVal disposing As Boolean) above.
        Dispose(True)
        GC.SuppressFinalize(Me)
    End Sub
#End Region
End Class

次に、Calculator.Lib という Windows スクリプト コンポーネントを作成します。このコンポーネントには、.NET 数学ライブラリを公開する VB スクリプト COM クラスを返す単一のメソッドがあります。ここでは、Construction と Destruction 中に画面に何かをポップアップ表示します。Destruction では、.NET ライブラリの Dispose メソッドを呼び出してリソースを解放していることに注意してください。Lib() 関数を使用して、.NET Com Calculator を呼び出し元に返すことに注意してください。

<?xml version="1.0"?>
<component>
<?component error="true" debug="true"?>
<registration
    description="Demo Math Library Script"
    progid="Calculator.Lib"
    version="1.00"
    classid="{0df54960-4639-496a-a5dd-a9abf1154772}"
>
</registration>
<public>
  <method name="GetMathLibrary">
  </method>
</public>
<script language="VBScript">
<![CDATA[
Option Explicit
'-----------------------------------------------------------------------------------------------------
' public Function to return back a logger.
'-----------------------------------------------------------------------------------------------------
function GetMathLibrary()
    Set GetMathLibrary = New MathLibrary
end function
Class MathLibrary
    private dotNetMatFunctionLib
  private sub class_initialize()
    MsgBox "Created."
    Set dotNetMatFunctionLib = CreateObject("DemoLib.Calculator")
  end sub
  private sub class_terminate()
        dotNetMatFunctionLib.Dispose()
        Set dotNetMatFunctionLib = nothing
    MsgBox "Terminated."
  end sub
  public function Lib()
    Set Lib = dotNetMatFunctionLib
  End function
end class
]]>
</script>
</component>

最後に、これらすべてをまとめるために、VB スクリプトのサンプルを示します。このスクリプトでは、作成、計算、.NET ライブラリで呼び出された破棄、最後に .NET コンポーネントを公開する COM コンポーネントでの終了を示すダイアログが表示されます。

dim comWrapper
dim vbsCalculator
set comWrapper = CreateObject("Calculator.Lib")
set vbsCalculator = comWrapper.GetMathLibrary()
msgbox "10 + 10 = " & vbsCalculator.lib.Add(10, 10)
msgbox "20 + 20 = " & vbsCalculator.lib.Add(20, 20)
set vbsCalculator = nothing
MsgBox("Dispose & Terminate should have been called before here.")
于 2010-02-12T14:30:17.290 に答える
4

私はこれを確認していませんが、ここに私が試してみることがあります:

まず、clr の IMarshal の既定の実装に関するCBrumme ブログの記事を次に示します。ユーティリティが複数の COM アパートメントで使用されている場合、VB6 の直接ポートから CLR への適切な com 動作が得られません。CLR によって実装された Com オブジェクトは、VB6 が公開したアパートメント スレッド モデルではなく、フリー スレッド マーシャラーを集約したかのように動作します。

IMarshal を実装できます (com オブジェクトとして公開している clr クラスに)。私の理解では、(相互運用プロキシではなく) COM プロキシの作成を制御できるようになります。これにより、UnmarshalInterface から返されたオブジェクトで Release 呼び出しをトラップし、元のオブジェクトにシグナルを返すことができると思います。標準マーシャラーをラップし (例: pinvoke CoGetStandardMarshaler )、すべての呼び出しをそれに転送します。私は、オブジェクトの寿命が CCW の寿命に結びついていると信じています。

繰り返します...これは、C#で解決する必要がある場合に試したいことです。

一方、この種のソリューションは、ATL で実装するよりも本当に簡単でしょうか? 魔法の部分が C# で記述されているからといって、ソリューションが単純になるわけではありません。上記の提案で問題が解決する場合は、何が起こっているのかを説明する非常に大きなコメントを書く必要があります.

于 2010-02-14T03:30:53.047 に答える
3

ここで説明されているように、プレビューハンドラーのサーバーの有効期間を正しくするために、私もこれに苦労してきました。

プロセス外のサーバーに入れる必要がありましたが、突然、ライフタイム コントロールの問題が発生しました。

アウト プロセス サーバーに入る方法は、興味のある人のためにここで説明されています: RegistrationSrvices.RegisterTypeForComClients コミュニティ コンテンツ

ファイナライザーを実装してみましたが、最終的にはオブジェクトが解放されましたが、オブジェクトを呼び出すサーバーの使用パターンが原因で、サーバーが永遠にハングアップすることになりました。また、ワークアイテムをスピンオフして、スリープ後に強制的にガベージ コレクションを実行しようとしましたが、それは非常に面倒でした。

代わりに、Release をフックすることになりました (Release の戻り値が信頼できないため、AddRef もフックします)。

(この投稿で見つかりました: http://blogs.msdn.com/b/oldnewthing/archive/2007/04/24/2252261.aspx#2269675 )

オブジェクトのコンストラクターで行ったことは次のとおりです。

//  Get the CCW for the object
_myUnknown = Marshal.GetIUnknownForObject(this);
IntPtr _vtable = Marshal.ReadIntPtr(_myUnknown);

// read out the AddRef/Release implementation
_CCWAddRef = (OverrideAddRef)
    Marshal.GetDelegateForFunctionPointer(Marshal.ReadIntPtr(_vtable, 1 * IntPtr.Size), typeof(OverrideAddRef));

_CCWRelease = (OverrideRelease)
    Marshal.GetDelegateForFunctionPointer(Marshal.ReadIntPtr(_vtable, 2 * IntPtr.Size), typeof(OverrideRelease)); 
_MyRelease = new OverrideRelease(NewRelease);
_MyAddRef = new OverrideAddRef(NewAddRef);


Marshal.WriteIntPtr(_vtable, 1 * IntPtr.Size, Marshal.GetFunctionPointerForDelegate(_MyAddRef)); 
Marshal.WriteIntPtr(_vtable, 2 * IntPtr.Size, Marshal.GetFunctionPointerForDelegate(_MyRelease));

and the declarations:


int _refCount; 

delegate int OverrideAddRef(IntPtr pUnknown);
OverrideAddRef _CCWAddRef; 
OverrideAddRef _MyAddRef;


delegate int OverrideRelease(IntPtr pUnknown); 
OverrideRelease _CCWRelease;
OverrideRelease _MyRelease;

IntPtr _myUnknown;

protected int NewAddRef(IntPtr pUnknown) 
{
    Interlocked.Increment(ref _refCount);
    return _CCWAddRef(pUnknown); 
}


protected int NewRelease(IntPtr pUnknown) 
{
    int ret = _CCWRelease(pUnknown);

    if (Interlocked.Decrement(ref _refCount) == 0)
    {
        ret = _CCWRelease(pUnknown);
        ComServer.Unlock();
    }

    return ret; 
}
于 2011-05-23T20:35:18.100 に答える
2

私が知る限り、この主題の最良の報道は、Alan Gordon 著の The .NET and COM Interoperability Handbookであり、そのリンクは Google Books の関連ページに移動するはずです。(残念ながら私は持っていません。代わりにトロールセンの本を手に入れました。)

Releaseそこのガイダンスは、CCW で /reference カウントにフックする明確に定義された方法がないことを意味します。代わりに、C# クラスを破棄可能にし、COM クライアント (あなたの場合は VBScript の作成者) が確定的Disposeなファイナライズを行いたいときに呼び出すことを推奨することをお勧めします。

しかし幸いなことに、抜け穴がありますIDispatch。これは、VBScript を使用してすべてのオブジェクト呼び出しを行うため、クライアントが遅延バインディング COM クライアントであるためです。

C# クラスが COM 経由で公開されているとします。最初にそれを機能させます。

ここで、ATL/C++ で、ATL シンプル オブジェクト ウィザードを使用してラッパー クラスを作成し、オプション ページで [インターフェイス: デュアル] ではなく [カスタム] を選択します。これにより、ウィザードは独自のIDispatchサポートを配置しなくなります。

クラスのコンストラクターで、CoCreateInstance を使用して C# クラスのインスタンスを作成します。クエリをIDispatch実行し、メンバー内のそのポインターを保持します。

IDispatchラッパー クラスの継承リストに追加し、IDispatchストレート スルーの 4 つのメソッドすべてを、コンストラクターに隠したポインターに転送します。

ラッパーのFinalReleaseで、レイト バインディング手法 ( ) を使用して C# オブジェクトのメソッドInvokeを呼び出します。Dispose

これで、VBScript クライアントは CCW 経由で C# クラスと通信していますが、最終リリースをインターセプトしてDisposeメソッドに転送することができます。

ATL ライブラリが「実際の」C# クラスごとに個別のラッパーを公開するようにします。ここでコードを適切に再利用するには、おそらく継承またはテンプレートを使用することをお勧めします。サポートする各 C# クラスには、ATL ラッピング コードで数行しか必要ありません。

于 2010-02-15T12:40:54.047 に答える
2

.Net Framework の動作は異なります。参照:
.NET Framework は、COM ベースの世界でのメモリ管理の方法とは異なるメモリ管理技術を提供します。COM のメモリ管理は、参照カウントによって行われました。.NET は、参照トレースを含む自動メモリ管理手法を提供します。この記事では、共通言語ランタイム CLR で使用されるガベージ コレクション手法について説明します。

やるべきことは何もない

[編集済み] もう 1 ラウンド...

この代替案を見てみましょうImporting a Type Library as an Assembly 、
あなた自身が言ったように、 CCW を使用すると、従来の COM の方法で参照カウントにアクセスできます

[編集済み] 永続性は美徳ですWinAPIOverride32
をご存知ですか? それを使用すると、それがどのように機能するかをキャプチャして研究できます。役立つ別のツールはDeviare COM Spy Consoleです。 これは簡単ではありません。 幸運を。

于 2010-02-12T16:40:34.800 に答える
0

これが不可能な理由は、refcount が 0 であっても、オブジェクトが使用されていないことを意味しないためだと思います。

VB_Object
   |
   V
   |
Managed1 -<- Managed2

この場合、VB オブジェクトがそのオブジェクトへの参照を削除し、その refcount が 0 であっても、オブジェクト Managed1 は引き続き使用されます。

あなたの言うことを本当に実行する必要がある場合は、refcount が 0 になったときに Dispose メソッドを呼び出すラッパー クラスをアンマネージド C++ で作成できると思います。これらのクラスはおそらくメタデータからコード生成される可能性がありますが、私はまったく経験がありません。この種のことを実装する方法で。

于 2010-02-09T11:48:14.757 に答える
-1

なぜパラダイムをシフトしないのですか?公開された独自の集計を作成し、通知メソッドで拡張するのはどうですか。ATLだけでなく、.Netでも実行できます。

編集済み:別の方法で説明できるリンクがあります(http://msdn.microsoft.com/en-us/library/aa719740(VS.71).aspx)。しかし、次の手順は上記の私の考えを説明しています。

レガシー インターフェイス (ILegacy) を実装する新しい .Net クラスを作成し、新しいインターフェイス (ISendNotify) を単一のメソッドで作成します。

interface ISendNotify 
{
     void SetOnDestroy(IMyListener );
}

class MyWrapper : ILegacy, ISendNotify, IDisposable{ ...

MyClass 内で実際のレガシー オブジェクトのインスタンスを作成し、MyClass からのすべての呼び出しをこのインスタンスに委任します。これは集計です。そのため、集計の有効期間は MyClass に依存するようになりました。MyClass は IDisposable であるため、インスタンスが削除されたときに傍受できるようになったため、IMyListener で通知を送信できます

EDIT2:そこに撮影(http://vb.mvps.org/hardcore/html/countingreferences.htm)イベントを送信するIUnknownの最も単純な実装

Class MyRewritten
    ...
    Implements IUnknown
    Implements ILegacy
    ...
    Sub IUnknown_AddRef()
        c = c + 1
    End Sub

    Sub IUnknown_Release()
        c = c - 1
        If c = 0 Then
            RaiseEvent Me.Class_Terminate
            Erase Me
        End If
    End Sub
于 2010-02-08T16:49:26.577 に答える
-1

私の知る限り、GC はすでにあなたがやろうとしていることをサポートしています。これをファイナライズと呼びます。純粋に管理された世界では、GC のパフォーマンスと操作に悪影響を与える可能性があるいくつかの副作用があるため、ファイナライズを避けることがベスト プラクティスです。IDisposable インターフェイスは、オブジェクトのファイナライズをバイパスし、マネージド コード内からマネージド リソースとアンマネージド リソースの両方をクリーンアップするクリーンでマネージドな方法を提供します。

あなたの場合、すべての管理されていない参照が解放されたら、管理されたリソースのクリーンアップを開始する必要があります。ファイナライズは、ここで問題を解決するのに優れているはずです。GC は、ファイナライザーが存在する場合、ファイナライズ可能なオブジェクトへの最後の参照がどのように解放されたかに関係なく、常にオブジェクトをファイナライズします。.NET 型にファイナライザーを実装する (単にデストラクタを実装する) 場合、GC はそれをファイナライズ キューに配置します。GC コレクション サイクルが完了すると、ファイナライズ キューが処理されます。デストラクタで実行するクリーンアップ作業は、ファイナライズ キューが処理されると発生します。

ファイナライズ可能な .NET 型に、ファイナライズが必要な他の .NET オブジェクトへの参照が含まれている場合は、長い GC コレクションを呼び出すか、一部のオブジェクトがファイナライズなしよりも長く存続する可能性があることに注意してください (つまり、それらはコレクションを生き残り、収集される頻度が低くなる次の世代に到達します。) ただし、CCW を使用する .NET オブジェクトのクリーンアップ作業がどのような形でも時間に敏感ではなく、メモリ使用量が大きな問題ではない場合は、いくつかの余分な寿命は関係ありません。ファイナライズ可能なオブジェクトは注意して作成する必要があり、他のオブジェクトへのクラス インスタンス レベルの参照を最小限に抑えるか削除すると、GC を介した全体的なメモリ管理を改善できることに注意してください。

ファイナライズの詳細については、http: //msdn.microsoft.com/en-us/magazine/bb985010.aspxの記事を参照してください。.NET 1.0 が最初にリリースされたときのかなり古い記事ですが、GC の基本的なアーキテクチャはまだ変更されていません (GC の最初の重要な変更は .NET 4.0 で行われますが、それらはより多くのことに関連しています。アプリケーション スレッドをフリーズせずに同時 GC を実行するよりも、基本的な操作を変更する必要があります)。

于 2010-02-14T04:33:10.467 に答える