且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

UWP开发入门(十六)——常见的内存泄漏的原因

更新时间:2022-03-25 13:08:08

原文:UWP开发入门(十六)——常见的内存泄漏的原因

  本篇借鉴了同事翔哥的劳动成果,在巨人的肩膀上把稿子又念了一遍。

  内存泄漏的概念我这里就不说了,之前《UWP开发入门(十三)——Diagnostic Tool检查内存泄漏》中提到过,即使有垃圾回收机制,写C#还是有可能发生内存泄漏。

  一般来说,以下两种情况会导致内存泄漏:

  1. 对象用完了但是没有释放资源
  1. 对象本身是做了清理内存的操作,但是对象内部的子对象没有成功释放资源

  下面就UWP开发中具体的实例来说明需要避免的写法

  • static/global的对象上注册了事件
FakeService.Instance.ShowMeTheMoneyEvent += Instance_ShowMeTheMoneyEvent;

  比如我们有一个底层的FakeService,提供整个APP生命周期的数据和网络的访问。假设某个页面+=了这个FackServiceEvent,在离开页面时没有-=掉。那么该页面就无法被垃圾回收。

合理的做法是在OnNavigatedFrom方法里,把事件反注册掉。

        protected override void OnNavigatedFrom(NavigationEventArgs e)
        {
            base.OnNavigatedFrom(e);
            FakeService.Instance.ShowMeTheMoneyEvent -= Instance_ShowMeTheMoneyEvent;
        }
  • DispatcherTimer事件未关闭

  这种情况就属于对象内部的属性未能被释放,假设页面内部存在Timer对象:

    public sealed partial class TimerPage : Page
    {
        private DispatcherTimer Timer { get; set; } = new DispatcherTimer();

        public ArrayList arrayList { get; set; }

        public TimerPage()
        {
            this.InitializeComponent();
            arrayList = new ArrayList(10000000);
            Timer.Tick += Timer_Tick;
            Timer.Interval = TimeSpan.FromSeconds(1);
            Timer.Start();
        }

        private void Timer_Tick(object sender, object e)
        {
            int count = 0;
            int.TryParse(TextBoxTimer.Text, out count);
            count += 1;
            TextBoxTimer.Text = count.ToString();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            this.Frame.GoBack();
        }

        protected override void OnNavigatedFrom(NavigationEventArgs e)
        {
            base.OnNavigatedFrom(e);
            Timer.Stop();
        }
    }

  如果在离开页面之前,未调用Timer对象的Stop方法,也未-=Tick事件(这里Stop方法会自动-=Tick事件)。该页面就不能正常的回收。

  这里并不是说所有的Event都需要在OnNavigatedFrom方法中-=,例如Control本身的LoadedIsEnabledChanged等事件等并不会造成内存泄漏,反注册这些事件是为了避免事件的重复触发。而DispatcherTimer比较特殊,我理解它会把自己加到一个专门维护计时器的队列中,然后不停的触发Tick事件,如果没有Stop-=,就等于Timer一直引用了外部的对象,从而导致页面本身也无法回收。

  • Data Binding Memory Leak

  这一条在很多的文档上有所提及,很遗憾我没法通过Diagnostic Tools监测出来具体的泄漏,我猜测可能是很小规模的内存泄漏。但是避免的方式非常容易,只要平时写XAML注意一下就可以了。

  会出现问题的写法是以下两种:

  1. 未实现INotifyPropertyChanged的对象,而你又想监测Property变化
  2. 未实现INotifyCollectionChanged 接口的集合,而你又想监测Collection变化

  其实很好处理。如果想监测变化,就老老实实继承对应的接口。如果使用了普通的Property和集合,并且不想监测变化,一定记得Mode = OneTime

  当然如果属性本身是dependency property,就不存在内存泄漏的情况了。

        <!--内存泄漏,因为Children集合没有实现INotifyPropertyChanged来通知Count属性变化-->
        <TextBlock Text="{Binding ElementName=layoutRoot, Path=Children.Count}" />
        <!--不会内存泄漏,因为ActualWidth是依赖属性-->
        <TextBlock Text="{Binding ElementName=layoutRoot, Path=ActualWidth}" />
        <!--不会内存泄漏,因为Mode = OneTime-->
        <TextBlock Text="{Binding ElementName=layoutRoot, Path=Children.Count, Mode = OneTime}" />
  • 非托管资源的释放

  这个都非常熟悉,不多说了。主要是通过using语句,或者在try { … } finally { … }中调用Dispose或者Close方法来释放非托管资源。