1

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

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

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

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

这意味着如果用户在根 ViewController 和自定义的 ThreadingViewController 之间来回点击 100 次,那么内存中将有 100 个该 ViewController 的实例。它没有被垃圾收集。

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

我需要做什么才能让这个 ThreadingViewController 被 MonoTouch 正确清理/GC?

这是重现该问题的完整(仅限 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 实例:

在此处输入图像描述

干杯。

4

3 回答 3

1

我认为问题出在这里:

timer.Dispose();

尝试将其更改为:

timer.Invalidate();

NSTimer是这里唯一使用NSAction. 另外,我认为这不是很有帮助:

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

原因:

  1. 这不是后台线程。不确定前台 .NET 线程在 iOS 上的行为方式。我的猜测是,它永远不会结束。
  2. 您不需要一直有一个单独的线程,您可以在其中启动 NSTimer。NSTimers 轻巧、防弹,并提供您需要的所有选项。即使您在 UI 线程上启动它们,它们也能很好地工作。(如果您有其他原因这样做,请忽略上面的#2,这在您的代码中是不直接可见的。但是,没有别的想法)。

PS:我没有测试你上面的代码。但我 100% 确信Invalidate() NSTimers,当您不再需要它们时,您必须让它们停止运行。

于 2013-11-05T18:15:08.207 回答
0

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

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

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

尝试从Cleanup () 方法中调用ReleaseOutlets ()(从 Xamarin Studio XIB 处理器生成),除了取消连接触摸事件处理程序可以改善这种情况。

于 2013-11-05T14:00:34.280 回答
0

每当您连接点击处理程序时,例如:

button.TouchUpInside += PushThreadingViewController;

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

button.TouchUpInside -= PushThreadingViewController;

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

于 2013-11-05T13:20:19.743 回答