在Windows 10 Mobile上,WinRT应用中的XAML页面未被垃圾回收,但在WP8.1上按预期工作。

3
我有一个为Windows Phone 8.1构建的WinRT应用程序。该应用程序具有一个主页面,导航到一个包含项目列表的页面,当用户点击项目时,它会导航到该项目的详细信息页面。事实证明,当用户单击项目然后按返回键时,Windows 10 Mobile上的第一个列表页面实例不会被垃圾回收。在Windows Phone 8.1上,一切都按预期工作。分析工具显示内存快照中的以下根路径。

enter image description here

RacePage是列表页面,因为在那个特定的快照中我来回切换了9次,所以有9个实例。Navigation Helper是应用程序模板Visual Studio创建的标准类。再次强调,我认为问题不在我的代码中,因为在WP8.1上不会发生泄漏。我不知道为什么已经连接事件的项没有被垃圾回收(它说RefCount handle可能是问题吗?)。有趣的是,详细信息页面似乎已经被正确地垃圾回收了。视图模型在每次导航时重新创建(即它们不是静态的)。
我会感激任何关于导致此问题的原因以及如何解决它的帮助。
这是页面的完整代码。
<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 Store中 - 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的非活动#ifdefs和过多的注释。 - Stilgar
这种问题很难调试。也许首先尝试完全删除 NavigationHelper(因为它只是加载和保存状态),并在页面卸载时强制进行 GC.Collect,以检查这两个事物是否对问题产生任何影响。 - Evk
我正在尝试强制进行GC。性能工具有一个按钮可以强制进行GC,但是我没有找到任何强制GC有所不同的点。我将继续测试不同的东西,目前问题已经足够容易控制而不会对用户造成影响,当我有时间时会努力解决它(毕竟这只是一个副业)。也许第一件事是构建一个单独的应用程序,在最小的设置下重现问题。 - Stilgar
移除NavigationHelper会有什么影响吗? - Evk
还没有尝试过,但是返回按钮处理程序已经存在。我需要将它们移动到每个页面上。 - Stilgar
1
由于无法在手机设备上使用WinDBG进行调试,您仍然可以使用VS保存转储文件,然后在WinDBG中打开它,加载SOS扩展并使用“!GCRoot”命令找出保留这些对象的本机内容。相关链接:https://blogs.msdn.microsoft.com/kristoffer/2007/01/09/debugging-memory-usage-in-managed-code-using-windbg/ - Sunius
1个回答

1
假设您的视图模型是静态的或存在于某种容器中(因此在创建后不会被垃圾回收),我认为这与一个长期存在的已知问题有关(我认为它尚未得到修复),该问题涉及到ICommand.CanExecuteChanged事件,在页面卸载时不会自动分离! 我建议在页面完全卸载并进行垃圾回收后,尝试为每个命令触发ICommand.CanExecuteChanged

模型不是静态的,我认为它没有存在于任何容器中。我也倾向于跳过命令,在视图模型上公开方法,然后在代码后台挂钩事件并手动调用VM方法。您可以在页面代码后台中看到视图模型的实例化。除了页面的DataContext之外,我不保留对它的任何引用。 - Stilgar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接