文章目錄
- 前言
- 一. 基礎知識回顧
- 二. 問題分析
- 三. 解決方案
- 1. 新建一個名為DeferredContentHost的控件。
- 2. 在DeferredContentHost控件中定義一個名為Content的object類型的依賴屬性,用于承載要加載的子控件。
- 3. 在DeferredContentHost控件中定義一個名為Skeleton的object類型的依賴屬性,用于在子控件加載前顯示骨架屏效果(使用加載效果也可以)。
- 4. 在DeferredContentHost控件Loaded時顯示骨架屏。
- 5. 在DeferredContentHost控件顯示骨架屏后執行Dispatcher.BeginInvoke(),將子控件顯示的代碼添加到Dispatcher消息隊列。
- 四. 運行效果
- 4.1 未優化的效果
- 4.2 優化后的效果
前言
長久以來,WPF的性能一直為人所詬病,其中很大一個原因就是因為WPF在開發過程中,稍有不慎就會阻塞UI線程,導致操作卡頓,甚至頁面停止響應。它的原因當然是多方面的,我們今天只討論比較常見的情況,并給出解決方案,讓您開發的軟件盡量減少卡頓。
一. 基礎知識回顧
我們都知道WPF是單線程模型,所有UI元素必須由創建它們的線程直接操作,并且該線程還負責處理用戶輸入(鼠標、鍵盤)、渲染界面、執行事件處理程序、管理布局和動畫等工作。所以如果UI線程一旦被阻塞,就會導致災難性后果,反應到界面上就是卡頓,鼠標無法操作,也不響應鍵盤輸入。
二. 問題分析
當加載一個Window時會執行一系列操作,其中最重要的操作就是布局的測量(Measure )與排列(Arrange),布局系統會從Window的根元素開始沿可視化樹逐個調用子級控件的Measure與Arrange方法,以確認頁面上的每個控件被渲染到正確的位置。在理想的情況下這種模式可以工作得很好,但是在實際項目中我們往往會在頁面上嵌套數量龐大的子控件來實現功能,當要渲染的控件數量達到UI線程處理的瓶頸上限或是在布局計算中耗時過多,這時就可能會導致UI線程被阻塞。
三. 解決方案
既然UI線程不能無限制處理所有請求,那我們給它排個隊,一個一個處理不就可以解決這個問題了。在WPF中所有控件都繼承自DispatcherObject類,DispatcherObject類中有一個名為Dispatcher的屬性,Dispatcher就是管理UI線程消息隊列的核心。我們只需要將控件的加載任務送入Dispatcher,讓它在合適的時機執行就可以了。以下是實現的過程:
1. 新建一個名為DeferredContentHost的控件。
2. 在DeferredContentHost控件中定義一個名為Content的object類型的依賴屬性,用于承載要加載的子控件。
3. 在DeferredContentHost控件中定義一個名為Skeleton的object類型的依賴屬性,用于在子控件加載前顯示骨架屏效果(使用加載效果也可以)。
4. 在DeferredContentHost控件Loaded時顯示骨架屏。
5. 在DeferredContentHost控件顯示骨架屏后執行Dispatcher.BeginInvoke(),將子控件顯示的代碼添加到Dispatcher消息隊列。
以上代碼的核心在于Dispatcher.BeginInvoke(DispatcherPriority priority, Delegate method)方法中的priority參數,該參數用于指定method委托在消息隊列中的執行優先級,以下為DispatcherPriority枚舉的所有值:
從上圖可以看出,為了不影響數據綁定、界面渲染、用戶輸入等操作,我們應該選擇盡量低的優先級來執行子控件顯示的代碼。這里我們使用ContextIdle。以下是完整的代碼:
[ContentProperty("Content")]
public class DeferredContentHost : FrameworkElement
{#region Fieldsprivate ContentControl _container = new ContentControl();#endregion#region Methodspublic DeferredContentHost(){this.AddVisualChild(_container);this.Loaded += DeferredContentHost_Loaded;}private void DeferredContentHost_Loaded(object sender, RoutedEventArgs e){if (IsInDesignMode){this._container.Content = this.Content;}else{this._container.Content = this.Skeleton;this.Dispatcher.BeginInvoke((Action)(() => this._container.Content = this.Content), System.Windows.Threading.DispatcherPriority.ContextIdle);}}protected override Visual GetVisualChild(int index){return _container;}protected override Size MeasureOverride(Size availableSize){_container.Measure(availableSize);if (availableSize.Width == double.PositiveInfinity || availableSize.Height == double.PositiveInfinity){return _container.DesiredSize;}return availableSize;}protected override Size ArrangeOverride(Size finalSize){_container.Arrange(new Rect(0, 0, finalSize.Width, finalSize.Height));return base.ArrangeOverride(finalSize);}#endregion#region Propertiesprotected override int VisualChildrenCount => 1;protected bool IsInDesignMode { get => DesignerProperties.GetIsInDesignMode(this); }public object Content{get { return (object)GetValue(ContentProperty); }set { SetValue(ContentProperty, value); }}// Using a DependencyProperty as the backing store for UIElement. This enables animation, styling, binding, etc...public static readonly DependencyProperty ContentProperty =DependencyProperty.Register("Content", typeof(object), typeof(DeferredContentHost));public object Skeleton{get { return (object)GetValue(SkeletonProperty); }set { SetValue(SkeletonProperty, value); }}// Using a DependencyProperty as the backing store for Skeleton. This enables animation, styling, binding, etc...public static readonly DependencyProperty SkeletonProperty =DependencyProperty.Register("Skeleton", typeof(object), typeof(DeferredContentHost));#endregion
}
四. 運行效果
我們用大圖片來模擬阻塞UI線程的情況,下面是兩種效果對比。
4.1 未優化的效果
<DataTemplate x:Key="item1"><Grid><Grid.RowDefinitions><RowDefinition Height="*" /><RowDefinition Height="30" /></Grid.RowDefinitions><Image Margin="5" Source="{Binding FullName}" /><TextBlockGrid.Row="1"Margin="5,0,5,5"HorizontalAlignment="Center"VerticalAlignment="Center"Text="{Binding Name}"TextTrimming="WordEllipsis" /></Grid>
</DataTemplate>
一次加載所有大圖片,界面停止響應,文本框無法輸入文字。
4.2 優化后的效果
<DataTemplate x:Key="item2"><controls:DeferredContentHost><controls:DeferredContentHost.Skeleton><controls:Skeleton><controls:SkeletonGroup Orientation="Vertical"><controls:SkeletonItemHeight="*"Margin="5"RadiusX="5"RadiusY="5" /><controls:SkeletonItemWidth="120"Height="30"Margin="5,0,5,5"HorizontalAlignment="Center"RadiusX="5"RadiusY="5" /></controls:SkeletonGroup></controls:Skeleton></controls:DeferredContentHost.Skeleton><Grid><Grid.RowDefinitions><RowDefinition Height="*" /><RowDefinition Height="30" /></Grid.RowDefinitions><Image Margin="5" Source="{Binding FullName}" /><TextBlockGrid.Row="1"Margin="5,0,5,5"HorizontalAlignment="Center"VerticalAlignment="Center"Text="{Binding Name}"TextTrimming="WordEllipsis" /></Grid></controls:DeferredContentHost>
</DataTemplate>
使用DeferredContentHost控件的延遲加載效果,加載過程文本框可以輸入文字,界面可以正常響應鼠標操作。
技術交流
QQ群:661224882