.Net Framework 3.0帶了個System.Speech.dll,裝個語音包,然后就可以實現文字朗讀等功能。最近在使用的時候,發現隨著程序的運行,程序占用的內存一直在增長,直到程序崩潰。
用WinDbg抓了個Dump,然后看了下,里面一堆沒有釋放的SPVTEXTFRAG、AudioDeviceOut+InItem、WAVEHDR、WaveHeader對象,于是寫了一小段來測試:
1: //happyhippy.cnblogs.com
2: using System.Speech.Synthesis;
3: using (SpeechSynthesizer ss = new SpeechSynthesizer())
4: {5: for (int i = 0; i < int.MaxValue; i++)
6: { 7: ss.Speak(i.ToString()); 8: } 9: }跑了幾個小時,然后抓了個內存,開始干活:
1. 看下內存中有哪些對象
1: 0:000> .load clr20/sos.dll 2: 0:000> !dumpheap -stat3: PDB symbol for mscorwks.dll not loaded
4: total 275193 objects 5: Statistics: 6: MT Count TotalSize Class Name 7: 70441ff8 1 12 System.RuntimeTypeHandle 8: 7043fc7c 1 12 System.__Filters9: //。。。。。。。。。。。。中間省略了一堆。。。。。。。。。。。
10: 704431a8 18 1008 System.Collections.Hashtable 11: 7042c938 65 1560 System.Security.Policy.StrongNameMembershipCondition 12: 70441cd4 81 1620 System.RuntimeType 13: 70441784 16 1820 System.Char[] 14: 7042c6e4 232 6496 System.Security.SecurityElement 15: 70442b84 278 6672 System.Collections.ArrayList 16: 704435c4 17 75056 System.Byte[] 17: 5610529c 4297 85940 System.Speech.Internal.AlphabetConverter+PhoneMapData+ConversionUnit 18: 704432a4 18 181896 System.Collections.Hashtable+bucket[] 19: 70414324 376 319064 System.Object[] 20: 70440b54 13185 396696 System.String 21: 5610ab38 7538 693496 System.Speech.Synthesis.TtsEngine.SPVTEXTFRAG 22: 56105c50 63264 1012224 System.Speech.Internal.Synthesis.AudioDeviceOut+InItem 23: 560f2694 65652 2626080 System.Speech.Internal.Synthesis.WAVEHDR 24: 56105b6c 63264 3289728 System.Speech.Internal.Synthesis.WaveHeader 25: 002be948 56465 172964660 Free 26: Total 275193 objects 27: Fragmented blocks larger than 0.5 MB: 28: Addr Size Followed by 29: 1228c6d8 0.5MB 12311d7c System.Speech.Synthesis.TtsEngine.SPVTEXTFRAG 30: 123293e0 0.6MB 123baa64 System.Speech.Synthesis.TtsEngine.SPVTEXTFRAG 31: 124be36c 1.9MB 1269d898 System.Speech.Synthesis.TtsEngine.SPVTEXTFRAG 32: 126a39f0 1.3MB 127e6644 System.Speech.Synthesis.TtsEngine.SPVTEXTFRAG
列表中的第25行,172M的Free狀態的內存。
最后4行(29~32)的內存碎片,用DumpObj看了下,都是Free狀態。
2. 看看終結隊列中有哪些對象
1: 0:000> !FinalizeQueue -detail 2: SyncBlocks to be cleaned up: 0 3: MTA Interfaces to be released: 0 4: STA Interfaces to be released: 0 5: ---------------------------------- 6: generation 0 has 2973 finalizable objects (068fdec4->06900d38) 7: generation 1 has 11 finalizable objects (068fde98->068fdec4) 8: generation 2 has 60304 finalizable objects (068c3058->068fde98)9: Ready for finalization 0 objects (06900d38->06900d38)
10: Statistics: 11: MT Count TotalSize Class Name 12: 7043a304 1 16 System.WeakReference 13: 7002d1c8 1 20 System.ComponentModel.AsyncOperation 14: 5610a9c0 1 20 System.Speech.Synthesis.SpeechSynthesizer 15: 5610576c 1 20 System.Speech.Internal.ObjectTokens.RegistryDataKey 16: 5610581c 1 24 System.Speech.Internal.ObjectTokens.ObjectToken 17: 56105ae8 1 56 System.Speech.Internal.Synthesis.AudioDeviceOut 18: 70427b90 4 80 Microsoft.Win32.SafeHandles.SafeWaitHandle 19: 561092e0 1 176 System.Speech.Internal.Synthesis.VoiceSynthesis 20: 7043b370 9 180 Microsoft.Win32.SafeHandles.SafeRegistryHandle 21: 70441128 4 224 System.Threading.Thread 22: 56105b6c 63264 3289728 System.Speech.Internal.Synthesis.WaveHeader 23: Total 63288 objects我靠,內存中3289728個WaveHeader全部掛在終結隊列中。一個可能的原因就是:WaveHeader對象中實現了析構函數,并且程序使用完WaveHeader對象后,沒有手動釋放(Dispose),而是等著GC來自動回收。
3. 用Reflector看WaveHeader的源代碼
1: internal sealed class WaveHeader : IDisposable
2: {3: // Fields
4: internal int _dwBufferLength;
5: private GCHandle _gcHandle = new GCHandle();
6: private GCHandle _gcHandleWaveHdr = new GCHandle();
7: private WAVEHDR _waveHdr = new WAVEHDR();
8: internal const int WAVE_FORMAT_PCM = 1;
9: internal const int WHDR_BEGINLOOP = 4;
10: internal const int WHDR_DONE = 1;
11: internal const int WHDR_ENDLOOP = 8;
12: internal const int WHDR_INQUEUE = 0x10;
13: internal const int WHDR_PREPARED = 2;
14: 15: // Methods
16: internal WaveHeader(byte[] buffer)
17: {18: this._dwBufferLength = buffer.Length;
19: this._gcHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned);//申請內存
20: } 21: 22: public void Dispose()
23: {24: this.Dispose(true);
25: GC.SuppressFinalize(this);
26: } 27: 28: private void Dispose(bool disposing)
29: {30: if (disposing)
31: {32: this.ReleaseData();
33: if (this._gcHandleWaveHdr.IsAllocated)
34: {35: this._gcHandleWaveHdr.Free();
36: } 37: } 38: } 39: 40: ~WaveHeader() 41: {42: this.Dispose(false);
43: } 44: 45: internal void ReleaseData()
46: {47: if (this._gcHandle.IsAllocated)
48: {49: this._gcHandle.Free();
50: } 51: } 52: 53: // Properties
54: internal int SizeHDR
55: { 56: get 57: {58: return Marshal.SizeOf(this._waveHdr);
59: } 60: } 61: 62: internal GCHandle WAVEHDR
63: { 64: get 65: {66: if (!this._gcHandleWaveHdr.IsAllocated)
67: {68: this._waveHdr.lpData = this._gcHandle.AddrOfPinnedObject();
69: this._waveHdr.dwBufferLength = (uint) this._dwBufferLength;
70: this._waveHdr.dwBytesRecorded = 0;
71: this._waveHdr.dwUser = 0;
72: this._waveHdr.dwFlags = 0;
73: this._waveHdr.dwLoops = 0;
74: this._waveHdr.lpNext = IntPtr.Zero;
75: this._gcHandleWaveHdr = GCHandle.Alloc(this._waveHdr, GCHandleType.Pinned);//申請內存
76: }77: return this._gcHandleWaveHdr;
78: } 79: } 80: }如我們所料,的確定義了一個析構函數(~WaveHeader()),WaveHealder實現了Dispose模式。
4. 進一步分析,看看WaveHeader都申請了啥資源,為啥要實現Dispose模式
WaveHeader的構造函數中,申請了一片Pinned狀態的內存this._gcHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned),并且其WAVEHDR屬性的實現中,也通過延遲加載的方式申請了一片Pinned狀態的內存this._gcHandleWaveHdr = GCHandle.Alloc(this._waveHdr, GCHandleType.Pinned)。
MSDN對GCHander的闡述如下:(FROM MSDN)
GCHandle 類與 GCHandleType 枚舉結合使用以創建對應于任何托管對象的句柄。此句柄可為以下四種類型之一:Weak、WeakTrackResurrection、Normal 或 Pinned。分配了句柄以后,在非托管客戶端保留唯一的引用時,可以使用它防止垃圾回收器回收托管對象。如果沒有這樣的句柄,則在該對象代表非托管客戶端完成工作以前,有可能被垃圾回收器回收。
用 GCHandle 創建一個固定對象,該對象返回??個內存地址,并防止垃圾回收器在內存中移動該對象。
超出范圍時,您必須通過調用Free方法顯式釋放它;否則,可能會發生內存泄漏。當您釋放固定的句柄時,如果沒有對關聯對象的其他引用,則關聯對象將解除固定,并可以被當成垃圾回收。
也就是說,通過GCHandler.Alloc申請的內存,必須通過調用GCHandler.Free方法來手動釋放。表面上看來,WaveHeader的實現中,的確有手動釋放這些申請的內存。
5. WaveHeader在哪兒使用
SpeechSynthesizer封裝了一對東西,有點兒繞,我順著Speak方法找了N久,沒有找到使用WaveHeader的地方。
只好換另一個思路:在上面的第1步中,我們可以看到,WaveHeader的MT Address為56105b6c,于是:
0:000> !dumpheap -MT 56105b6c
//。。。省卻6萬多行。。。 127fabdc 56105b6c 52
12800a64 56105b6c 52 //找到一個還活著的對象
total 63264 objects
Statistics:
MT Count TotalSize Class Name
56105b6c 63264 3289728 System.Speech.Internal.Synthesis.WaveHeader
Total 63264 objects
然后找到一個還活著的對象(地址為12800a64),看看它被誰引用了:
0:000> !gcroot 12800a64
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 0 OSTHread 3668
ESP:1df238:Root:01f67e1c(System.Speech.Internal.Synthesis.VoiceSynthesis)->
01f6841c(System.Speech.Internal.Synthesis.AudioDeviceOut)->
01f68570(System.Collections.Generic.List`1[[System.Speech.Internal.Synthesis.AudioDeviceOut+InItem, System.Speech]])->
02f91c10(System.Object[])->
020e4088(System.Speech.Internal.Synthesis.AudioDeviceOut+InItem)->
12800a64(System.Speech.Internal.Synthesis.WaveHeader)
Scan Thread 2 OSTHread 2c5c
Scan Thread 3 OSTHread 1160
Scan Thread 4 OSTHread 211c
Scan Thread 6 OSTHread 22d4
Scan Thread 7 OSTHread 2fcc
Scan Thread 8 OSTHread 377c
SpeechSynthesizer
->VoiceSynthesis
->AudioDeviceOut
-> List<AudioDeviceOut + InItem>
-> AudioDeviceOut + InItem(內部類)
-> WaveHeader
終于找到目標了:AudioDeviceOut。
6. 怎么使用WaveHeader
AudioDeviceOut的代碼頁有點長,這里只截出部分使用WaveHeader的代碼:
1: internal override void Play(byte[] buffer)
2: {3: if (this._deviceOpen)
4: {5: int length = buffer.Length;
6: this._bytesWritten += length;
7: WaveHeader waveHeader = new WaveHeader(buffer);//上面第3節提到,這里通過GCHandler.Alloc申請了內存
8: GCHandle wAVEHDR = waveHeader.WAVEHDR;//上面第3節提到,這里通過GCHandler.Alloc申請了內存9: MMSYSERR errorCode = SafeNativeMethods.waveOutPrepareHeader(this._hwo, wAVEHDR.AddrOfPinnedObject(), waveHeader.SizeHDR);
10: if (errorCode != MMSYSERR.NOERROR)
11: {12: throw new AudioException(errorCode);
13: }14: lock (this._noWriteOutLock)
15: {16: if (!base._aborted)
17: {18: lock (this._queueIn)
19: {20: InItem item = new InItem(waveHeader);//InItem封裝了一下WaveHeader,也沒啥
21: this._queueIn.Add(item); //將Item加入到List中
22: this._evt.Reset();
23: }24: errorCode = SafeNativeMethods.waveOutWrite(this._hwo, wAVEHDR.AddrOfPinnedObject(), waveHeader.SizeHDR);//P/Invoke向設備輸出數據
25: if (errorCode != MMSYSERR.NOERROR)
26: {27: lock (this._queueIn)
28: {29: this._queueIn.RemoveAt(this._queueIn.Count - 1);//如果出錯了,從List刪除一個Item
30: throw new AudioException(errorCode);
31: } 32: } 33: } 34: } 35: } 36: } 在這里,Play方法了面,終于實例化了WaveHeader和WaveHDR(GCHandler);但是實例化的WaveHeader又被InItem封裝了一下,加入到List<InItem> _queueIn中。
Play方法里面創建了WaveHeader,但是卻沒有看到釋放WaveHeader的代碼。
那我們再來看下,其他地方有沒有釋放WaveHeader的代碼:既然InItem/WaveHeader被加入到_queueIn中了,程序應該還要使用WaveHeader;那么當InItem從該_queueIn中移出的時候,應該就會釋放WaveHeader了吧。順著這個思路繼續往下看。。。
很不幸,就在Play方法里面:當判斷播放出錯的時候,會把InItem從_queueIn中移出(上面第29行),但是卻沒有釋放InItem/WaveHeader。
另一個將InItem從_queueIn中移出的地方是CallBackProc,AudioDeviceOut在構造函數中初始化了一個委托:this._delegate = new SafeNativeMethods.WaveOutProc(this.CallBackProc); 從字面上理解,應該是輸出聲音完畢后,調用這個回調函數,來釋放內存:
1: private void CallBackProc(IntPtr hwo, MM_MSG uMsg, IntPtr dwInstance, IntPtr dwParam1, IntPtr dwParam2)
2: {3: if (uMsg == MM_MSG.MM_WOM_DONE)
4: {5: lock (this._queueIn)
6: {7: InItem item = this._queueIn[0];
8: item.ReleaseData();//釋放InItem/WaveHeader9: this._queueIn.RemoveAt(0); //移除InItem
10: this._queueOut.Add(item);
11: while (this._queueIn.Count > 0)
12: {13: item = this._queueIn[0];
14: if (item._waveHeader != null)
15: {16: break;
17: }18: if ((this._asyncDispatch != null) && !base._aborted)
19: {20: this._asyncDispatch.Post(item._userData);
21: }22: this._queueIn.RemoveAt(0);//移除InItem,但是卻沒有釋放InItem/WaveHeader
23: } 24: }25: if (this._queueIn.Count == 0)
26: {27: this._evt.Set();
28: } 29: } 30: }代碼中,可以看到,當uMsg == MM_MSG.MM_WOM_DONE,才會執行釋放操作。MM_MSG枚舉有三個可選值:MM_WOM_CLOSE = 0x3bc, MM_WOM_DONE = 0x3bd, MM_WOM_OPEN = 0x3bb,暫時不太清楚是回調函數的uMsg參數 否可能會傳入其他值;如果傳入了其他值,顯然InItem/WaveHeader有沒有被釋放。
即使uMsg == MM_MSG.MM_WOM_DONE,我們來看下釋放的代碼,上面第8~9行,從_queueIn中移除InItem/WaveHeader的時候,的確執行了ReleaseData()來釋放內存,但是看該方法的源代碼:
1: //happyhippy.cnblogs.com
2: //InItem
3: internal void ReleaseData()
4: {5: if (this._waveHeader != null)
6: {7: this._waveHeader.ReleaseData();
8: } 9: }10: //WaveHeader
11: internal void ReleaseData()
12: {13: if (this._gcHandle.IsAllocated)
14: {15: this._gcHandle.Free();
16: } 17: } 18: 里面調用WaveHeader.ReleaseData()來釋放內存,不是調用Dispose來釋放內存。上面提到,Play方法中,第8行GCHandle wAVEHDR = waveHeader.WAVEHDR申請了內存,但是ReleaseData()方法并沒有釋放該內存。應該調用Dispose來釋放內存,而不是調用ReleaseData()。
然后在回過頭來看CallBackProc方法,從源代碼中可以看到,其只釋放了_queueIn中第一個InItem/WaveHeader,后續的InItem/WaveHeader從_queueIn中移除的時候,并沒有釋放內存。
GCHandler.Alloc申請的內存,必須通過調用Free方法顯式釋放它;否則,可能會發生內存泄漏。
7. 解決辦法
無解。
那封裝的一大坨,都是Internal。
浙公網安備 33010602011771號