Is it possible for an object to "pin" itself and avoid garbage collection?
I'm trying to write a drop-in replacement for System.开发者_StackOverflowMedia.SoundPlayer
using the waveOut...
API. This API makes a callback to a method in my version of SoundPlayer when the file/stream passed it has completed playback.
If I create a form-scoped instance of my SoundPlayer and play something, everything works fine because the form keeps the object alive, so the delegate is alive to receive the callback.
If I use it like this in, say, a button click event:
SoundPlayer player = new SoundPlayer(@"C:\whatever.wav");
player.Play();
... it works fine 99% of the time, but occasionally (and frequently if the file is long) the SoundPlayer object is garbage-collected before the file completes, so the delegate is no longer there to receive the callback, and I get an ugly error.
I know how to "pin" objects using GCHandle.Alloc
, but only when something else can hang onto the handle. Is there any way for an object to pin itself internally, and then un-pin itself after a period of time (or completion of playback)? If I try GCHandle.Alloc (this, GCHandleType.Pinned);
, I get a run-time exception "Object contains non-primitive or non-blittable data."
You could just have a static
collection of all the "currently playing" sounds, and simply remove the SoundPlayer
instance when it gets the "finished playing" notification. Like this:
class SoundPlayer
{
private static List<SoundPlayer> playing = new List<SoundPlayer>();
public void Play(...)
{
...
playing.Add(this);
}
// assuming this is your callback when playing has finished
public void OnPlayingFinished(...)
{
...
playing.Remove(this);
}
}
(Obviously locking/multithreading, error checking and so on required)
Your SoundPlayer object should just be stored in a private field of your form class so that it stays referenced long enough. You probably need to dispose it when your form closes.
Fwiw, pinning doesn't work because your class is missing a [StructLayout] attribute. Not that it will work effectively with one, you would have to store the returned GCHandle somewhere so that you can unpin it later. Your form class is the only logical place to store it. Make it simple.
GCHandle is the way to go; just don't specify the Pinned enum value.
The problem with a class static member (such as shown here) is that the objects may be collected too early if the managed portion of the program no longer references them. The referenced example shows using a callback "OnPlayingFinished", but now you have to worry about keeping the delegate (the one that references OnPlayingFinished) from itself being garbage collected.
You will still need to register for OnPlayingFinished, and keep the delegate alive. However, the GCHandle is keeping your object alive, so you can keep the delegate around with:
class SoundPlayer
{
public void Play(...)
{
var h = GCHandle.Alloc(this);
SomeNativeAPI.Play(this, h.ToIntPtr());
}
// assuming this is your callback when playing has finished
delegate void FinishedCallback(IntPtr userData);
static FinishedCallback finishedCallback = OnPlayingFinished;
public static void OnPlayingFinished(IntPtr userData)
{
var h = GCHandle.FromIntPtr(userData);
SoundPlayer This = (SoundPlayer)h.Target;
h.Free();
... // use 'This' as your object
}
}
We've ensured our SoundPlayer remains reachable via the GCHandle. And as an instance of SoundPlayer remains reachable, its static members must also remain reachable.
At least, that's my best educated guess as to how you might go about it.
The best way to do this is to keep a [ThreadStatic]
list of active SoundPlayer
s in a private static field, and remove each instance from the list when the sound finishes.
For example:
[ThreadStatic]
static List<SoundPlayer> activePlayers;
public void Play() {
if(activePlayers == null) activePlayers = new List<SoundPlayer>();
activePlayers.Add(this);
//Start playing the sound
}
void OnSoundFinished() {
activePlayers.Remove(this);
}
This might sound too simple, but just make a strong reference to the SoundPlayer object in your own class. That ought to keep GC away as long as the object is alive.
I.e. instead of:
public class YourProgram
{
void Play()
{
SoundPlayer player = new SoundPlayer(@"c:\whatever.wac");
player.Play();
}
}
This:
public class YourProgram
{
private SoundPlayer player;
void Play()
{
player = new SoundPlayer(@"c:\whatever.wac");
player.Play();
}
}
Are you simply trying to prevent your object from being garbage collected? Couldn't you call GC.KeepAlive( this )
to protect it from the GC?
精彩评论