线程/定时器的MonoTouch内存问题

时间:2013-11-05 12:53:26

标签: c# multithreading memory xamarin.ios xamarin

我遇到了MonoTouch的问题,即使UIViewControllers从导航堆栈中弹出,它们仍永远保留在内存中。

我有一个UINavigationController,它包含一个带按钮的UIViewController。单击该按钮将名为ThreadingViewController的自定义UIViewController推送到导航堆栈。

ThreadingViewController使用NSTimer.CreateRepeatingScheduledTimer和Thread来每隔一秒更新标签文本。

当用户点击“返回”以弹回根视图时,Mono Profiler会说我的ThreadingViewController仍然存在于内存中。 Profiler告诉我它与NSAction和/或ThreadStart有关,它引用了ThreadingViewController,使它保持活着状态。我可以通过检查探查器中的“反向引用”复选框来看到这一点。

这意味着如果用户在根ViewController和自定义ThreadingViewController之间向后和向前单击100次,则内存中将有100个此ViewController实例。它不是垃圾收集。

在ViewDidDisappear中,我尝试中止线程,将其设置为null,但无济于事。

我需要做些什么才能让MonoTouch正确清理/调整此ThreadingViewController?

以下是重现问题的完整(仅限C#)源代码:

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using MonoTouch.Foundation;
using MonoTouch.UIKit;
using System.Threading;

namespace MemoryTests
{
    [Register("AppDelegate")]
    public partial class AppDelegate : UIApplicationDelegate
    {
        private UIWindow window;
        private UINavigationController rootNavigationController;
        private RootScreen rootScreen;

        public override bool FinishedLaunching(UIApplication app, NSDictionary options)
        {
            window = new UIWindow(UIScreen.MainScreen.Bounds);

            rootScreen = new RootScreen();
            rootNavigationController = new UINavigationController(rootScreen);
            window.RootViewController = rootNavigationController;

            window.MakeKeyAndVisible();
            return true;
        }
    }

    public class RootScreen : UIViewController
    {
        private UIButton button;

        public override void ViewDidLoad()
        {
            base.ViewDidLoad();

            this.Title = "Root Screen";

            // Add a button
            button = new UIButton(UIButtonType.RoundedRect);
            button.SetTitle("Click me", UIControlState.Normal);
            button.Frame = new RectangleF(100, 100, 120, 44);
            this.View.Add(button);
        }

        public override void ViewWillAppear(bool animated)
        {
            base.ViewWillAppear(animated);

            button.TouchUpInside += PushThreadingViewController;
        }

        public override void ViewDidDisappear(bool animated)
        {
            base.ViewDidDisappear(animated);

            button.TouchUpInside -= PushThreadingViewController;
        }

        private void PushThreadingViewController(object sender, EventArgs e)
        {
            var threadingViewController = new ThreadingViewController();
            NavigationController.PushViewController(threadingViewController, true);
        }
    }

    public class ThreadingViewController : UIViewController
    {
        private UILabel label;
        private NSTimer timer;
        private int counter;

        public override void ViewDidLoad()
        {
            base.ViewDidLoad();

            this.Title = "Threading Screen";

            // Add a label
            label = new UILabel();
            label.Frame = new RectangleF(0f, 200f, 320f, 44f);
            label.Text = "Count: 0";
            this.View.Add(label);

            // Start a timer
            var timerThread = new Thread(StartTimer as ThreadStart);
            timerThread.Start();
        }

        public override void ViewDidDisappear (bool animated)
        {
            base.ViewDidDisappear(animated);

            timer.Dispose();
            timer = null;
            // Do I need to clean up more Threading things here?
        }

        [Export("StartTimer")]
        private void StartTimer()
        {
            using (var pool = new NSAutoreleasePool())
            {
                timer = NSTimer.CreateRepeatingScheduledTimer(1d, TimerTicked);
                NSRunLoop.Current.Run();
            }
        }

        private void TimerTicked()
        {
            InvokeOnMainThread(() => {
                label.Text = "Count: " + counter;
                counter++;
            });
        }
    }
}

以下是分析器的屏幕截图,告诉我在内存中有3个ThreadingViewController实例:

enter image description here

干杯。

3 个答案:

答案 0 :(得分:1)

我认为问题在于:

timer.Dispose();

尝试将其更改为:

timer.Invalidate();

NSTimer是唯一使用NSAction的内容。 另外,我认为这不是很有用:

var timerThread = new Thread(StartTimer as ThreadStart);
timerThread.Start();

原因:

  1. 这不是后台主题。不确定前台.NET线程在iOS上的表现。我的猜测是,它永远不会结束。
  2. 您不需要一直有一个单独的线程,从你开始NSTimer开始。 NSTimers轻巧,防弹,并提供您需要的所有选项。即使您在UI线程上启动它们,它们也能很好地工作。 (如果您有其他原因,请忽略上面的#2,这在您的代码中无法直接显示。但是,没有其他任何想法了。)
  3. PS:我还没有测试过上面的代码。但我完全相信你必须Invalidate() NSTimers以便当你不再需要它们时它们会停止运行。

答案 1 :(得分:0)

随时连接点击处理程序,例如:

button.TouchUpInside + = PushThreadingViewController;

您还必须断开连接。我建议将上面的代码行移到ViewDidAppear覆盖中,并将下面的代码行添加到ViewDidDisappear覆盖中:

button.TouchUpInside - = PushThreadingViewController;

我还建议您在将其推入导航堆栈之前检查threadingViewController是否为null并且仅创建新实例(如果是)。这样,如果它还没有被垃圾收集,它每次都会使用相同的一个。

答案 2 :(得分:0)

小心ViewDidDisappear。您可以在任何时候调用View-Controller控制另一个View-Controller(调用PresentViewController,调用共享UI,显示iAd等)。如果您只想在从导航堆栈中删除View-Controller时做出反应,我建议您:

public override void DidMoveToParentViewController(UIViewController parent)
{
  base.DidMoveToParentViewController(parent);

  if(parent == null)
    Cleanup();
}

尝试从清理()方法中调用 ReleaseOutlets ()(从Xamarin Studio XIB处理器生成),以及取消触摸事件处理程序改善情况。