XAML 页面不是在 Windows 10 Mobile 上的 WinRT 应用程序中收集的垃圾,但在 WP8.1 上按预期工作
Posted
技术标签:
【中文标题】XAML 页面不是在 Windows 10 Mobile 上的 WinRT 应用程序中收集的垃圾,但在 WP8.1 上按预期工作【英文标题】:XAML page is not garbage collected in WinRT app on Windows 10 Mobile but works as expected on WP8.1 【发布时间】:2017-07-17 02:47:59 【问题描述】:我有一个为 Windows Phone 8.1 构建的 WinRT 应用程序。该应用程序有一个主页,该主页指向一个包含项目列表的页面,当点击一个项目时,它会指向该项目的详细信息页面。事实证明,当用户单击某个项目然后按回时,列表页面的第一个实例在 Windows 10 移动版上不会被垃圾收集。在 Windows Phone 8.1 上,一切都按预期工作。分析工具会在内存快照中显示以下到 root 的路径。
RacePage 是列表页面,有九个实例,因为在那个特定的快照中我来回切换了 9 次。 Navigation Helper 是 Visual Studio 创建的应用模板中的标准类。同样,我不认为问题出在我的代码中,因为泄漏不会在 WP8.1 上发生.有趣的是,带有详细信息的页面似乎已正确 GC。每次导航都会重新创建视图模型(即它们不是静态的)
对于导致问题的原因以及如何解决问题的任何帮助,我将不胜感激。
这是页面的完整代码
<Page
x:Class="Medusa.WinRT.RacePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Medusa.WinRT"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Page.Resources>
<ResourceDictionary>
<Style x:Key="ImageLabelStyle" TargetType="TextBlock">
<Setter Property="Margin" Value="5,0,0,0"/>
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="FontFamily" Value="ThemeResource PhoneFontFamilySemiLight" />
<Setter Property="FontSize" Value="ThemeResource TextStyleMediumFontSize" />
<Setter Property="TextLineBounds" Value="Full" />
<Setter Property="TextWrapping" Value="NoWrap" />
<Setter Property="LineHeight" Value="20" />
<Setter Property="Foreground" Value="ThemeResource PhoneMidBrush" />
</Style>
</ResourceDictionary>
</Page.Resources>
<Grid>
<Hub x:Name="pMain" Header="Binding Title">
<Hub.Background>
<ImageBrush ImageSource="Binding BackgroundImagePath" Stretch="UniformToFill" Opacity="0.3"></ImageBrush>
</Hub.Background>
<HubSection Header="UNITS" HeaderTemplate="ThemeResource HubSectionHeaderTemplate">
<DataTemplate>
<ListView Margin="0,0,-12,0" ItemsSource="Binding Units" Background="Transparent">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel x:Name="spUnit" Tapped="spUnit_Tapped" Background="Transparent" Tag="Binding">
<StackPanel Orientation="Horizontal" Margin="0,0,0,17">
<Image Width="80" Height="72" Source="Binding MenuImagePath" ImageFailed="ImageFailed"></Image>
<Grid Width="270" Tag="Binding">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20" />
<ColumnDefinition Width="40" />
<ColumnDefinition Width="20" />
<ColumnDefinition Width="40" />
<ColumnDefinition Width="20" />
<ColumnDefinition Width="40" />
<ColumnDefinition Width="20" />
<ColumnDefinition Width="40" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.ColumnSpan="8" Text="Binding Name" Style="ThemeResource ListViewItemTextBlockStyle"/>
<Image Grid.Row="1" Grid.Column="0" Width="20" Height="20" Source="Assets/icon-mineral.png"></Image>
<TextBlock Grid.Row="1" Grid.Column="1" Text="Binding MineralCost" Style="ThemeResource ImageLabelStyle"/>
<Image Grid.Row="1" Grid.Column="2" Width="20" Height="20" Source="Binding Path=RaceGasIconPath"></Image>
<TextBlock Grid.Row="1" Grid.Column="3" Text="Binding GasCost" Style="ThemeResource ImageLabelStyle"/>
<Image Grid.Row="1" Grid.Column="4" Width="20" Height="20" Source="Binding Path=RaceBuildTimeIcon"></Image>
<TextBlock Grid.Row="1" Grid.Column="5" Text="Binding BuildTime" Style="ThemeResource ImageLabelStyle"/>
<Image Grid.Row="1" Grid.Column="6" Width="20" Height="20" Source="Binding Path=RaceSupplyIcon"></Image>
<TextBlock Grid.Row="1" Grid.Column="7" Text="Binding SupplyCost" Style="ThemeResource ImageLabelStyle"/>
</Grid>
</StackPanel>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</DataTemplate>
</HubSection>
<HubSection Header="BUILDINGS" HeaderTemplate="ThemeResource HubSectionHeaderTemplate">
<DataTemplate>
<ListView Margin="0,0,-12,0" ItemsSource="Binding Buildings" Background="Transparent">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel x:Name="spBuilding" Tapped="spBuilding_Tapped" Tag="Binding" Background="Transparent">
<StackPanel Orientation="Horizontal" Margin="0,0,0,17" >
<Image Width="80" Height="72" Source="Binding MenuImagePath" ImageFailed="ImageFailed"></Image>
<Grid Width="270">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20" />
<ColumnDefinition Width="40" />
<ColumnDefinition Width="20" />
<ColumnDefinition Width="40" />
<ColumnDefinition Width="20" />
<ColumnDefinition Width="40" />
<ColumnDefinition Width="20" />
<ColumnDefinition Width="40" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.ColumnSpan="8" Text="Binding Name" Style="ThemeResource ListViewItemTextBlockStyle"/>
<Image Grid.Row="1" Grid.Column="0" Width="20" Height="20" Source="Assets/icon-mineral.png"></Image>
<TextBlock Grid.Row="1" Grid.Column="1" Text="Binding MineralCost" Style="ThemeResource ImageLabelStyle"/>
<Image Grid.Row="1" Grid.Column="2" Width="20" Height="20" Source="Binding Path=RaceGasIconPath"></Image>
<TextBlock Grid.Row="1" Grid.Column="3" Text="Binding GasCost" Style="ThemeResource ImageLabelStyle"/>
<Image Grid.Row="1" Grid.Column="4" Width="20" Height="20" Source="Binding Path=RaceBuildTimeIcon"></Image>
<TextBlock Grid.Row="1" Grid.Column="5" Text="Binding BuildTime" Style="ThemeResource ImageLabelStyle"/>
<Image Grid.Row="1" Grid.Column="6" Width="20" Height="20" Source="Binding Path=RaceSupplyIcon"></Image>
<TextBlock Grid.Row="1" Grid.Column="7" Text="Binding SupplyValue" Style="ThemeResource ImageLabelStyle"/>
</Grid>
</StackPanel>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</DataTemplate>
</HubSection>
</Hub>
</Grid>
</Page>
后面的代码:
public sealed partial class RacePage : Page
private NavigationHelper navigationHelper;
public RacePage()
this.InitializeComponent();
navigationHelper = new NavigationHelper(this);
navigationHelper.LoadState += OnNavigationHelperLoadState;
this.Unloaded += RacePage_Unloaded;
private void RacePage_Unloaded(object sender, RoutedEventArgs e)
DataContext = null;
navigationHelper.LoadState -= OnNavigationHelperLoadState;
navigationHelper = null;
private void OnNavigationHelperLoadState(object sender, LoadStateEventArgs e)
Initialize((Races)e.NavigationParameter);
private void Initialize(Races race)
if (DataContext == null)
var viewModel = new RaceViewModel(App.Settings.CurrentGameInfo, race);
DataContext = viewModel;
private void ImageFailed(object sender, ExceptionRoutedEventArgs e)
((Image)sender).Source = new BitmapImage(new Uri("ms-appx:///Assets/noimage80x72.png", UriKind.Absolute));
protected override void OnNavigatedTo(NavigationEventArgs e)
navigationHelper.OnNavigatedTo(e);
protected override void OnNavigatedFrom(NavigationEventArgs e)
navigationHelper.OnNavigatedFrom(e);
private void spUnit_Tapped(object sender, TappedRoutedEventArgs e)
var unitViewModel = (UnitViewModel)((Panel)sender).Tag;
this.Frame.Navigate(typeof(UnitPage), unitViewModel);
private void spBuilding_Tapped(object sender, TappedRoutedEventArgs e)
var buildingViewModel = (BuildingViewModel)((Panel)sender).Tag;
this.Frame.Navigate(typeof(BuildingPage), buildingViewModel);
调试故事(我是如何做到这一点的)
我发布了一个使用 WinRT 为 WP8.1 和 Win10 Mobile 构建的应用程序。当它进入市场时,我进行了更多测试,发现在 W10M 上,列表页面上的图像在到详细信息页面来回大约 10 次后开始滞后(出现一秒钟后)。虽然我确实在 W10M 上测试了应用程序,但我并没有点击太多以使问题变得可见,而在开发过程中,我正在使用内存量很小的 WP8.1 模拟器进行测试,所以我没有遇到这个问题。该问题在 WP8.1 上不存在。该问题可以在模拟器中重现。
我假设存在某种泄漏并运行了分析工具。我首先注意到 PropertyChanged 代表的数量正在增加。我想也许我的 ViewModel 通过事件处理程序持有引用。由于我不需要双向数据绑定,因此我删除了 INotifyPropertyChanged 实现,但问题仍然存在,并且委托被称为 CustomPropertyImpl 的东西取代(这似乎是用于对 POCO 进行数据绑定的基础设施)。
然后我查看了我的视图模型以检查它们是否是静态的。他们不是。我将一个未加载的处理程序连接到列表页面并手动将 DataContext 设置为 null。这减少了大量泄漏的对象,并且问题没有在视觉上重现,但是当我查看分析工具时,列表页面仍然泄漏。问题似乎仍然会发生,但您需要加载数百个页面而不是 10 个。
查看到根的路径,W10M 似乎使一些具有事件挂钩的对象保持活动状态。该页面有一个集线器控件和两个项目列表。后面的代码有几个事件处理程序。
该应用在此处的 Windows 应用商店中发布 - https://www.microsoft.com/en-us/store/p/sc2-master/9n2cjmrsnd8l
编辑:根据请求 NavigationHelper 类(已删除非 Windows Phone 部分)
[Windows.Foundation.Metadata.WebHostHidden]
public class NavigationHelper : DependencyObject
private Page Page get; set;
private Frame Frame get return this.Page.Frame;
public NavigationHelper(Page page)
this.Page = page;
this.Page.Loaded += (sender, e) =>
#if WINDOWS_PHONE_APP
Windows.Phone.UI.Input.HardwareButtons.BackPressed += HardwareButtons_BackPressed;
#endif
;
// Undo the same changes when the page is no longer visible
this.Page.Unloaded += (sender, e) =>
#if WINDOWS_PHONE_APP
Windows.Phone.UI.Input.HardwareButtons.BackPressed -= HardwareButtons_BackPressed;
#endif
;
#region Navigation support
RelayCommand _goBackCommand;
RelayCommand _goForwardCommand;
public RelayCommand GoBackCommand
get
if (_goBackCommand == null)
_goBackCommand = new RelayCommand(
() => this.GoBack(),
() => this.CanGoBack());
return _goBackCommand;
set
_goBackCommand = value;
public RelayCommand GoForwardCommand
get
if (_goForwardCommand == null)
_goForwardCommand = new RelayCommand(
() => this.GoForward(),
() => this.CanGoForward());
return _goForwardCommand;
public virtual bool CanGoBack()
return this.Frame != null && this.Frame.CanGoBack;
public virtual bool CanGoForward()
return this.Frame != null && this.Frame.CanGoForward;
public virtual void GoBack()
if (this.Frame != null && this.Frame.CanGoBack) this.Frame.GoBack();
public virtual void GoForward()
if (this.Frame != null && this.Frame.CanGoForward) this.Frame.GoForward();
#if WINDOWS_PHONE_APP
private void HardwareButtons_BackPressed(object sender, Windows.Phone.UI.Input.BackPressedEventArgs e)
if (this.GoBackCommand.CanExecute(null))
e.Handled = true;
this.GoBackCommand.Execute(null);
#endif
#endregion
#region Process lifetime management
private String _pageKey;
public event LoadStateEventHandler LoadState;
public event SaveStateEventHandler SaveState;
public void OnNavigatedTo(NavigationEventArgs e)
var frameState = SuspensionManager.SessionStateForFrame(this.Frame);
this._pageKey = "Page-" + this.Frame.BackStackDepth;
if (e.NavigationMode == NavigationMode.New)
// Clear existing state for forward navigation when adding a new page to the
// navigation stack
var nextPageKey = this._pageKey;
int nextPageIndex = this.Frame.BackStackDepth;
while (frameState.Remove(nextPageKey))
nextPageIndex++;
nextPageKey = "Page-" + nextPageIndex;
// Pass the navigation parameter to the new page
if (this.LoadState != null)
this.LoadState(this, new LoadStateEventArgs(e.Parameter, null));
else
// Pass the navigation parameter and preserved page state to the page, using
// the same strategy for loading suspended state and recreating pages discarded
// from cache
if (this.LoadState != null)
this.LoadState(this, new LoadStateEventArgs(e.Parameter, (Dictionary<String, Object>)frameState[this._pageKey]));
public void OnNavigatedFrom(NavigationEventArgs e)
var frameState = SuspensionManager.SessionStateForFrame(this.Frame);
var pageState = new Dictionary<String, Object>();
if (this.SaveState != null)
this.SaveState(this, new SaveStateEventArgs(pageState));
frameState[_pageKey] = pageState;
#endregion
public delegate void LoadStateEventHandler(object sender, LoadStateEventArgs e);
public delegate void SaveStateEventHandler(object sender, SaveStateEventArgs e);
public class LoadStateEventArgs : EventArgs
public Object NavigationParameter get; private set;
public Dictionary<string, Object> PageState get; private set;
public LoadStateEventArgs(Object navigationParameter, Dictionary<string, Object> pageState)
: base()
this.NavigationParameter = navigationParameter;
this.PageState = pageState;
public class SaveStateEventArgs : EventArgs
public Dictionary<string, Object> PageState get; private set;
public SaveStateEventArgs(Dictionary<string, Object> pageState)
: base()
this.PageState = pageState;
【问题讨论】:
已添加。我已删除仅适用于 Windows 而不适用于 WP 和过多 cmets 的非活动 #ifdefs 这样的问题很难调试。也许首先尝试完全删除 NavigationHelper(因为它只是加载和保存状态)并在页面卸载时强制 GC.Collect,只是为了检查这两件事是否对问题有任何影响。 我在玩 Forced GC。性能工具有一个强制 GC 的按钮。找不到强制 GC 产生影响的任何点。我将继续测试不同的东西。现在问题已经足够包含,对用户来说不是问题,我会在有时间的时候尝试解决它(毕竟这是一个附带项目)。也许第一件事是构建一个单独的应用程序,以最小的设置重现问题。 那去掉 NavigationHelper 怎么样,没有什么区别? 由于您无法在手机设备上使用 WinDBG 进行调试,您仍然可以使用 VS 保存转储文件,然后在 WinDBG 中打开它,加载 SOS 扩展并使用“!GCRoot”命令找出什么保持这些对象是原生的。相关链接:blogs.msdn.microsoft.com/kristoffer/2007/01/09/… 【参考方案1】:假设您的视图模型是静态的或存在于某种容器上(因此在创建后不会收集垃圾),这在我看来与一个长期已知的问题有关(我相信它不是已修复)与卸载页面时不会自动分离的ICommand.CanExecuteChanged
事件相关!
我建议在页面完全卸载和 gc'ed 之后尝试为每个命令提高 ICommand.CanExecuteChanged
。
【讨论】:
模型不是静态的,我不认为它存在于任何容器中。我也倾向于跳过命令并在视图模型上公开方法,然后在后面的代码中挂钩事件并手动调用 VM 方法。您可以在后面的页面代码中看到视图模型的实例化。除了页面的 DataContext 之外,我没有保留对它的任何引用。以上是关于XAML 页面不是在 Windows 10 Mobile 上的 WinRT 应用程序中收集的垃圾,但在 WP8.1 上按预期工作的主要内容,如果未能解决你的问题,请参考以下文章
在 App.xaml 中添加按钮并以编程方式将其添加到其他 xaml 页面 Windows 运行时应用