在受控的 XAML 應用程式中使用 Win2D 控件時,必須小心避免參考計數循環,防止垃圾收集器無法回收這些控件。
如果你有問題的話...
- 您使用的是 .NET 語言的 Win2D,例如 C# (不是原生C++)
- 您可以使用其中一個 Win2D XAML 控制項:
- 您訂閱 Win2D 控件的事件(例如
Draw、CreateResources、SizeChanged...) - 您的應用程式在多個 XAML 頁面之間來回移動
如果符合所有這些條件,參考計數週期會讓 Win2D 控件無法進行垃圾收集。 每次應用程式移至不同的頁面時,都會配置新的 Win2D 資源,但舊資源永遠不會釋放,因此記憶體會流失。 若要避免這種情況,您必須新增程式代碼來明確中斷迴圈。
修正方式
若要中斷參考計數週期,並讓您的頁面可被垃圾回收:
- 掛鉤包含 Win2D 控制項的 XAML 頁面
Unloaded事件 - 在
Unloaded處理程式中,呼叫 Win2D 控制件上的RemoveFromVisualTree - 在
Unloaded處理程式中,釋放 Win2D 控件的任何明確參照並將其設定為null
範例程式代碼:
void page_Unloaded(object sender, RoutedEventArgs e)
{
this.canvas.RemoveFromVisualTree();
this.canvas = null;
}
如需正常運作的範例,請參閱任何 範例資源庫 示範頁面。
如何測試循環滲漏
若要測試您的應用程式是否正確地中斷 refcount 循環,請將完成項方法新增至包含 Win2D 控制件的任何 XAML 頁面:
~MyPage()
{
System.Diagnostics.Debug.WriteLine("~" + GetType().Name);
}
App在您的建構函式中,設定定時器,以確保垃圾收集會定期進行:
var gcTimer = new DispatcherTimer();
gcTimer.Tick += (sender, e) => { GC.Collect(); };
gcTimer.Interval = TimeSpan.FromSeconds(1);
gcTimer.Start();
導航到該頁面,然後轉至其他頁面。 如果所有迴圈都中斷,您將會在一到兩秒內在 Visual Studio 的輸出窗格中看到 Debug.WriteLine 輸出。
請注意,呼叫 GC.Collect 會造成干擾並損害效能,因此您應該在完成洩漏測試后立即移除此測試程序代碼!
血腥的細節
當物件 A 具有 B 的參考,同時 B 也有 A 的參考時,就會發生迴圈。或者,當 A 參考 B 和 B 參考 C 時,而 C 則參考 A 等。
訂閱 XAML 控制件的事件時,這種循環幾乎不可避免:
- XAML 頁面會保存其中的所有控制項的參考
- 控件會保存已訂閱其事件的處理程式委派參考
- 每個代表都會持有其目標實例的參考
- 事件處理程式通常是 XAML 頁面類別的實例方法,因此其目標實例參考會指向 XAML 頁面,建立迴圈
如果所有涉及的物件都是在 .NET 中實作的,這類迴圈不會成為問題,因為 .NET 具備垃圾收集功能,垃圾收集演算法能夠識別並回收即使是在循環中連結的物件群組。
不同於 .NET,C++藉由參考計數來管理記憶體,而無法偵測和回收對象的迴圈。 儘管有這項限制,但使用 Win2D 的 C++ 應用程式沒有任何問題,因為 C++ 事件處理常式預設會保留弱式而非強式參考來指向其目標實例。 因此,頁面會參考控件,而控件會參考事件處理程式委派,但此委派不會參考回頁面,因此沒有迴圈。
當 .NET 應用程式使用C++ WinRT 元件,例如 Win2D 時,就會發生此問題:
- XAML 頁面是應用程式的一部分,因此會使用資源回收
- Win2D 控制件會在 C++中實作,因此會使用參考計數
- 事件處理委託是應用程式的一部分,因此會使用垃圾收集,並對其目標實例保留強引用。
迴圈存在,但參與此迴圈的 Win2D 物件不會使用 .NET 垃圾收集。 這表示垃圾回收器無法檢視整個鏈,因此無法偵測或回收這些物件。 發生此情況時,應用程式必須通過明確地打破循環來協助解決。 這可以藉由釋放頁面中到控件的所有參考(如上所述的建議),或釋放控件中指向頁面的事件處理程式委派的所有參考來完成(使用頁面卸載事件取消訂閱所有事件處理程式)。