我有一个为Windows Phone 8.1构建的WinRT应用程序。该应用程序有一个主页面,该主页面指向包含项目列表的页面,当点击项目时,它会指向该项目的详细信息页面。事实证明,当用户点击某个项目然后再按下该列表页面的第一个实例时,不会在Windows 10 Mobile上收集垃圾。在Windows Phone 8.1上,一切都按预期工作。分析工具在内存快照中显示以下根目录路径。
RacePage是列表页面,有九个实例,因为在那个特定的快照中我来回了9次。 Navigation Helper是Visual Studio创建的应用程序模板的标准类。我再也不认为问题出现在我的代码中,因为WP8.1上没有发生泄漏我不知道为什么挂起事件的项目没有GCed(它说RefCount句柄可能是问题) ?)。有趣的是,带有详细信息的页面似乎正确地进行了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;
}
}
答案 0 :(得分:1)
假设您的视图模型是静态的或存在于某种容器中(因此在创建后不会收集垃圾),这看起来与长期已知的问题有关(我认为它不是已修复但与ICommand.CanExecuteChanged
事件相关的事件在卸载页面时不会自动分离!
我建议在页面完全卸载并执行gc后,尝试为每个命令提升ICommand.CanExecuteChanged
。