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

Posted

技术标签:

【中文标题】线程/计时器的 MonoTouch 内存问题【英文标题】:MonoTouch memory issue with Threading / Timers 【发布时间】:2013-11-05 12:53:25 【问题描述】:

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

我有一个 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 实例:

干杯。

【问题讨论】:

【参考方案1】:

我认为问题出在这里:

timer.Dispose();

尝试将其更改为:

timer.Invalidate();

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

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

原因:

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

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

【讨论】:

调用 timer.Invalidate() 没有帮助。此外,当我删除您建议的两行代码时,它们在线程中启动计时器,而是通过调用 StartTimer() 直接启动计时器,整个事情都崩溃了:UI 只是冻结了。其他建议? 是的,你是否也删除了 NSRunLoop.Current.Run(); ?它阻塞了线程。当然,TimerTicked 中的 InvokeOnMainThread 也需要移除。【参考方案2】:

任何时候连接点击处理程序,例如:

button.TouchUpInside += PushThreadingViewController;

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

button.TouchUpInside -= PushThreadingViewController;

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

【讨论】:

我已更新代码示例,以便在视图出现和消失时订阅和取消订阅按钮单击事件。它不能解决问题。我也觉得重用同一个 UIViewController 是不好的做法。实际上,我想每次都将数据传递给 UIViewController,所以我想要一个新的 UIViewController,而不是一个旧状态的旧的。 我不知道重用 UIViewController 是不好的做法,但我很想知道为什么。您可以每次将数据传递给 threadingViewController,因为您在 rootViewController 中有对它的引用。来自您的 rootViewController 的引用实际上可能是阻止 threadingViewController 被垃圾收集的原因。此外,您可以覆盖 dispose 方法,这是您应该处理对象的地方。 尝试运行我的示例代码并重现问题。注释掉注释“启动计时器”下面的两行代码。 ThreadingViewController 由 GC 清理。使用线程,它不是。因此,无论我们是即时创建该变量还是拥有一个实例变量似乎都无关紧要...... 不要仅仅使用:“var timerThread = new Thread(StartTimer as ThreadStart);”,而是在你的 threadingViewController 中为 timerThread 创建一个私有字段,这样你也可以处理它。 试过了。没有帮助。我还将“StartTimer as ThreadStart”提取到一个私有字段中,并将其设置为 null。也没有帮助...【参考方案3】:

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

public override void DidMoveToParentViewController(UIViewController parent)

  base.DidMoveToParentViewController(parent);

  if(parent == null)
    Cleanup();

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

【讨论】:

您是否尝试使用我的示例代码重现问题、实施您的建议并解决问题...? 我知道这不是 DoMyHomework.com。但是您可以从我的示例代码中看到没有涉及 XIB。所以我不知道 ReleaseOutlets() 可能有什么帮助。我也很确定 ViewDidDisappear() 没有问题。问题在于线程。注释掉启动 Thread 的两行代码可以解决问题。重新插入线程后,内存堆积问题再次出现......

以上是关于线程/计时器的 MonoTouch 内存问题的主要内容,如果未能解决你的问题,请参考以下文章

Monotouch内存限制崩溃

iOS 或 MonoTouch 中的固有内存泄漏?

我应该使用哪些 Instruments 工具来了解我的 Monotouch 应用程序的内存使用情况?

项目经理问我Java内存区域模型!急急急

项目经理问我Java内存区域模型!急急急

当 CPU 利用率和内存增加时,计时器间隔增加