更改transitionWithView内的rootViewController时泄漏视图


97

在调查内存泄漏时,我发现了与setRootViewController:在过渡动画块内调用技术有关的问题:

[UIView transitionWithView:self.window
                  duration:0.5
                   options:UIViewAnimationOptionTransitionFlipFromLeft
                animations:^{ self.window.rootViewController = newController; }
                completion:nil];

如果旧的视图控制器(正在被替换的视图控制器)当前正在显示另一个视图控制器,则上面的代码不会从视图层次结构中删除显示的视图。

也就是说,这一系列操作...

  1. X成为Root View Controller
  2. X代表Y,因此Y的视图在屏幕上
  3. 使用transitionWithView:使Z中的新根视图控制器

...看起来对用户来说还可以,但是Debug View Hierarchy工具将显示Y的视图仍在Z的视图之后,在A的内部UITransitionView。也就是说,经过上述三个步骤,视图层次结构为:

  • UIWindow
    • UITransitionView
      • UIView(Y的视图)
    • UIView(Z的视图)

我怀疑这是一个问题,因为在过渡时,X的视图实际上并不是视图层次结构的一部分。

如果我dismissViewControllerAnimated:NO在紧接之前发送给X transitionWithView:,则结果视图层次为:

  • UIWindow
    • UIView(X的视图)
    • UIView(Z的视图)

如果我向dismissViewControllerAnimated:X 发送(是或否),然后在completion:块中执行转换,则视图层次结构正确。不幸的是,这会干扰动画。如果为解雇设置动画,则会浪费时间。如果不设置动画,它看起来就坏了。

我正在尝试其他方法(例如,使新的容器视图控制器类充当我的根视图控制器),但没有找到任何可行的方法。我将随时更新此问题。

最终目标是直接从呈现的视图过渡到新的根视图控制器,而不会留下杂散的视图层次结构。


我目前也遇到同样的问题
Alex

我刚刚遇到了同样的问题
Jamal Zafar 2014年

有幸找到解决这个问题的办法吗?同样的确切问题在这里。
大卫·贝兹

@DavidBaez我结束编写代码以在更改根目录之前主动关闭所有视图控制器。不过,它非常特定于我的应用程序。自发布此文章以来,我一直想知道是否应该交换UIWindow,但还没有时间进行大量试验。
benzado

Answers:


119

我最近有一个类似的问题。我必须UITransitionView从窗口中手动删除它以解决问题,然后在以前的根视图控制器上调用dismiss以确保已释放它。

解决方法不是很好,但是除非您发布问题后找到了更好的方法,否则这是我发现唯一可行的方法!viewController只是newController您最初提出的问题。

UIViewController *previousRootViewController = self.window.rootViewController;

self.window.rootViewController = viewController;

// Nasty hack to fix http://stackoverflow.com/questions/26763020/leaking-views-when-changing-rootviewcontroller-inside-transitionwithview
// The presenting view controllers view doesn't get removed from the window as its currently transistioning and presenting a view controller
for (UIView *subview in self.window.subviews) {
    if ([subview isKindOfClass:NSClassFromString(@"UITransitionView")]) {
        [subview removeFromSuperview];
    }
}
// Allow the view controller to be deallocated
[previousRootViewController dismissViewControllerAnimated:NO completion:^{
    // Remove the root view in case its still showing
    [previousRootViewController.view removeFromSuperview];
}];

我希望这也可以帮助您解决问题,这绝对是痛苦!

斯威夫特3.0

(请参阅其他Swift版本的编辑历史记录)

对于更好的实现,它是UIWindow允许传入可选过渡的扩展。

extension UIWindow {

    /// Fix for http://stackoverflow.com/a/27153956/849645
    func set(rootViewController newRootViewController: UIViewController, withTransition transition: CATransition? = nil) {

        let previousViewController = rootViewController

        if let transition = transition {
            // Add the transition
            layer.add(transition, forKey: kCATransition)
        }

        rootViewController = newRootViewController

        // Update status bar appearance using the new view controllers appearance - animate if needed
        if UIView.areAnimationsEnabled {
            UIView.animate(withDuration: CATransaction.animationDuration()) {
                newRootViewController.setNeedsStatusBarAppearanceUpdate()
            }
        } else {
            newRootViewController.setNeedsStatusBarAppearanceUpdate()
        }

        if #available(iOS 13.0, *) {
            // In iOS 13 we don't want to remove the transition view as it'll create a blank screen
        } else {
            // The presenting view controllers view doesn't get removed from the window as its currently transistioning and presenting a view controller
            if let transitionViewClass = NSClassFromString("UITransitionView") {
                for subview in subviews where subview.isKind(of: transitionViewClass) {
                    subview.removeFromSuperview()
                }
            }
        }
        if let previousViewController = previousViewController {
            // Allow the view controller to be deallocated
            previousViewController.dismiss(animated: false) {
                // Remove the root view in case its still showing
                previousViewController.view.removeFromSuperview()
            }
        }
    }
}

用法:

window.set(rootViewController: viewController)

要么

let transition = CATransition()
transition.type = kCATransitionFade
window.set(rootViewController: viewController, withTransition: transition)

6
谢谢。有效。如果您找到更好的方法,请分享
Jamal Zafar 2014年

8
看来,替换已经呈现视图的根视图控制器(或尝试取消分配仍然呈现视图控制器的UIWindow)将导致内存泄漏。在我看来,呈现一个视图控制器会在窗口中创建一个保留循环,而解除控制器是我发现打破它的唯一方法。我认为一些内部完成块对窗口有很强的参考作用。
卡尔·林德伯格

转换为swift 2.0后,NSClassFromString(“ UITransitionView”)出现了问题
Eugene Braginets

同样在iOS 9中仍在发生:(另外,我已经更新了Swift 2.0
丰富的

1
@ user023我已经在提交给App Store的2或3个应用程序中使用了此精确解决方案,没有任何问题!我猜因为您只需要对照字符串检查类的类型就可以了(可以是任何字符串)。可能导致拒绝的原因是UITransitionView在您的应用程序中有一个名为的类,然后将该类作为应用程序符号的一部分提取,我认为App Store会使用该类进行检查。
Rich

5

我遇到了这个问题,这使我整天烦恼。我已经尝试过@Rich的obj-c解决方案,事实证明,当我想在此之后呈现另一个viewController时,将被空白的UITransitionView阻止。

最后,我想出了这种方法,它对我有用。

- (void)setRootViewController:(UIViewController *)rootViewController {
    // dismiss presented view controllers before switch rootViewController to avoid messed up view hierarchy, or even crash
    UIViewController *presentedViewController = [self findPresentedViewControllerStartingFrom:self.window.rootViewController];
    [self dismissPresentedViewController:presentedViewController completionBlock:^{
        [self.window setRootViewController:rootViewController];
    }];
}

- (void)dismissPresentedViewController:(UIViewController *)vc completionBlock:(void(^)())completionBlock {
    // if vc is presented by other view controller, dismiss it.
    if ([vc presentingViewController]) {
        __block UIViewController* nextVC = vc.presentingViewController;
        [vc dismissViewControllerAnimated:NO completion:^ {
            // if the view controller which is presenting vc is also presented by other view controller, dismiss it
            if ([nextVC presentingViewController]) {
                [self dismissPresentedViewController:nextVC completionBlock:completionBlock];
            } else {
                if (completionBlock != nil) {
                    completionBlock();
                }
            }
        }];
    } else {
        if (completionBlock != nil) {
            completionBlock();
        }
    }
}

+ (UIViewController *)findPresentedViewControllerStartingFrom:(UIViewController *)start {
    if ([start isKindOfClass:[UINavigationController class]]) {
        return [self findPresentedViewControllerStartingFrom:[(UINavigationController *)start topViewController]];
    }

    if ([start isKindOfClass:[UITabBarController class]]) {
        return [self findPresentedViewControllerStartingFrom:[(UITabBarController *)start selectedViewController]];
    }

    if (start.presentedViewController == nil || start.presentedViewController.isBeingDismissed) {
        return start;
    }

    return [self findPresentedViewControllerStartingFrom:start.presentedViewController];
}

好了,现在您要做的就是[self setRootViewController:newViewController];在要切换根视图控制器时调用。


效果很好,但是在切换根视图控制器之前,正在呈现的视图控制器令人讨厌地闪烁。对dismissViewControllerAnimated:外观进行动画处理可能比不进行动画处理要好一些。确实避免UITransitionView在视图层次结构中使用ghost 。
pkamb

5

我尝试在iOs 9.3上为我工作的简单方法:只需在dismissViewControllerAnimated完成过程中从其层次结构中删除旧的viewController视图。

按照benzado的说明,让我们研究X,Y和Z视图:

也就是说,这一系列操作...

  1. X成为Root View Controller
  2. X代表Y,因此Y的视图在屏幕上
  3. 使用transitionWithView:使Z成为新的根视图控制器

哪给:

////
//Start point :

let X = UIViewController ()
let Y = UIViewController ()
let Z = UIViewController ()

window.rootViewController = X
X.presentViewController (Y, animated:true, completion: nil)

////
//Transition :

UIView.transitionWithView(window,
                          duration: 0.25,
                          options: UIViewAnimationOptions.TransitionFlipFromRight,
                          animations: { () -> Void in
                                X.dismissViewControllerAnimated(false, completion: {
                                        X.view.removeFromSuperview()
                                    })
                                window.rootViewController = Z
                           },
                           completion: nil)

在我的情况下,X和Y很好地解除了分配,并且它们的视图不再处于层次结构中!


0

发生了类似的问题。在我的情况下,我具有viewController层次结构,并且其中一个子视图控制器具有一个呈现的视图控制器。当我更改Windows根视图控制器时,由于某种原因,所提供的视图控制器仍在内存中。因此,解决方案是在更改Windows根视图控制器之前关闭所有视图控制器。


-2

我在使用以下代码时遇到了这个问题:

if var tc = self.transitionCoordinator() {

    var animation = tc.animateAlongsideTransitionInView((self.navigationController as VDLNavigationController).filtersVCContainerView, animation: { (context:UIViewControllerTransitionCoordinatorContext!) -> Void in
        var toVC = tc.viewControllerForKey(UITransitionContextToViewControllerKey) as BaseViewController
        (self.navigationController as VDLNavigationController).setFilterBarHiddenWithInteractivity(!toVC.filterable(), animated: true, interactive: true)
    }, completion: { (context:UIViewControllerTransitionCoordinatorContext!) -> Void in

    })
}

禁用此代码可解决问题。我设法通过仅在启用动画的过滤器栏初始化时启用此过渡动画来使其工作。

这并不是您真正要寻找的答案,但是它可以带您找到正确的解决方案。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.