【问题标题】:Leaking views when changing rootViewController inside transitionWithView在transitionWithView中更改rootViewController时泄漏视图
【发布时间】:2015-01-01 23:43:30
【问题描述】:

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

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

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

也就是这一系列的操作……

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

...对用户来说看起来不错,但是调试视图层次结构工具将显示 Y 的视图仍然存在于 Z 的视图​​后面,位于 UITransitionView 内。也就是经过上面三个步骤,视图层次结构是:

  • 用户界面窗口
    • UITransitionView
      • UIView(Y 的视图)
    • UIView(Z 的视图​​)

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

如果我在transitionWithView: 之前将dismissViewControllerAnimated:NO 发送到X,则生成的视图层次结构是:

  • 用户界面窗口
    • UIView(X 的视图)
    • UIView(Z 的视图​​)

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

我正在尝试一些其他方法(例如,创建一个新的容器视图控制器类作为我的根视图控制器),但没有找到任何可行的方法。我会随时更新这个问题。

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

【问题讨论】:

  • 我目前也有同样的问题
  • 我刚遇到同样的问题
  • 运气好能找到合适的解决方案吗?同样的问题在这里。
  • @DavidBaez 我最终编写了代码以在更改根之前主动关闭所有视图控制器。不过,它非常特定于我的应用程序。自发布此消息以来,我一直想知道是否可以交换 UIWindow,但没有时间进行太多实验。

标签: ios cocoa-touch uiviewcontroller core-animation


【解决方案1】:

我最近遇到了类似的问题。我必须手动从窗口中删除 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];
}];

我希望这也可以帮助您解决问题,这绝对是个麻烦!

Swift 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)

【讨论】:

  • 谢谢。有效。如果您找到更好的方法,请分享
  • 看来替换已呈现视图的根视图控制器(或尝试释放仍呈现视图控制器的 UIWindow)将导致内存泄漏。在我看来,呈现视图控制器会在窗口中创建一个保留循环,而解除控制器是我发现打破它的唯一方法。我认为一些内部完成块对窗口有很强的引用。
  • 在转换为 swift 2.0 后遇到 NSClassFromString("UITransitionView") 问题
  • iOS 9 中仍然存在 :( 我也更新了 Swift 2.0
  • @user023 我已经在提交到 App Store 的 2 或 3 个应用程序中使用了这个确切的解决方案,没有出现任何问题!我猜你只是根据字符串检查类的类型,这很好(它可以是任何字符串)。可能导致拒绝的原因是在您的应用程序中有一个名为 UITransitionView 的类,因为它会作为应用程序符号的一部分被拾取,我认为 App Store 会使用它来检查。
【解决方案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

    })
}

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

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

【讨论】:

    【解决方案3】:

    我遇到了这个问题,它困扰了我一整天。 我已经尝试过@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: 看起来可能比没有动画好一点。确实避免了视图层次结构中的幽灵UITransitionViews。
    【解决方案4】:

    我尝试了一个在 iOs 9.3 上对我有用的简单方法:只需在 dismissViewControllerAnimated 完成期间从其层次结构中删除旧 viewController 的视图。

    让我们按照benzado 的说明处理 X、Y 和 Z 视图:

    也就是这一系列的操作……

    1. X 成为根视图控制器
    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 很好地被释放,并且他们的视图不再处于层次结构中!

    【讨论】:

      【解决方案5】:

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

      【讨论】:

        猜你喜欢
        • 2013-10-16
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-06-14
        • 2018-05-24
        • 2016-04-21
        • 1970-01-01
        相关资源
        最近更新 更多