Ios 在transitionWithView中更改rootViewController时泄漏视图

Ios 在transitionWithView中更改rootViewController时泄漏视图,ios,cocoa-touch,uiviewcontroller,core-animation,Ios,Cocoa Touch,Uiviewcontroller,Core Animation,在调查内存泄漏时,我发现了一个与调用过渡动画块内的setRootViewController:技术相关的问题: [UIView transitionWithView:self.window duration:0.5 options:UIViewAnimationOptionTransitionFlipFromLeft animations:^{ self.window.rootViewCon

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

[UIView transitionWithView:self.window
                  duration:0.5
                   options:UIViewAnimationOptionTransitionFlipFromLeft
                animations:^{ self.window.rootViewController = newController; }
                completion:nil];
如果旧视图控制器(被替换的视图控制器)当前正在显示另一个视图控制器,则上述代码不会从视图层次结构中删除显示的视图

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

  • X成为根视图控制器
  • X表示Y,因此Y的视图显示在屏幕上
  • 使用
    transitionWithView:
    使Z成为新的根视图控制器
  • …在用户看来是正常的,但“调试视图层次结构”工具将显示Y的视图仍在Z的视图后面,位于
    UITransitionView
    中。也就是说,经过上述三个步骤后,视图层次结构为:

    • UIWindow
      • 超转换视图
        • UIView(Y视图)
      • UIView(Z视图)
    我怀疑这是一个问题,因为在转换时,X的视图实际上不是视图层次结构的一部分

    如果在
    transitionWithView:
    之前立即向X发送
    dismissViewControllerAnimated:NO
    ,则生成的视图层次结构为:

    • UIWindow
      • UIView(X的视图)
      • UIView(Z视图)
    如果我将
    dismissViewControllerAnimated:
    (是或否)发送到X,然后在
    完成:
    块中执行转换,则视图层次结构是正确的。不幸的是,这会干扰动画。如果将解雇动画化,则会浪费时间;如果不设置动画,则它看起来已损坏

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


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

    我最近遇到了类似的问题。我必须手动从窗口中删除该
    UITransitionView
    ,以解决问题,然后在上一个根视图控制器上调用dismise,以确保其解除分配

    修复不是很好,但除非你在发布问题后找到更好的方法,否则这是我发现唯一有效的方法
    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)
    


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

    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
    
        })
    }
    
    禁用此代码,修复了问题。我只在初始化设置动画的过滤器栏时才启用此转换动画,从而成功地实现了这一点


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

    我面对这个问题,它让我烦恼了一整天。 我已经尝试过@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]当您想要切换根视图控制器时。

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

    让我们使用X、Y和Z视图,如下所述:

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

  • X成为根视图控制器
  • X表示Y,因此Y的视图显示在屏幕上
  • 使用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是很好的dealloc,他们的视图不再在层次结构中

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

    我目前也遇到了同样的问题。我刚刚遇到了同样的问题。幸运的是找到了一个合适的解决方案吗?这里也有同样的问题。@DavidBaez我最后写了一段代码,在更改根目录之前,我会主动关闭所有视图控制器。不过,这对我的应用程序来说非常特殊。自从发布这篇文章以来,我一直想知道交换
    UIWindow
    是否是应该做的事情,但还没有时间做很多实验。谢谢。成功了。如果您发现更好的方法,请分享。替换已显示视图的根视图控制器(或尝试解除锁定仍显示视图控制器的UIWindow)将导致内存泄漏。在我看来,显示视图控制器会创建一个带有窗口的保留循环,而取消控制器是我发现的唯一打破它的方法。我认为一些内部完成块对窗口有很强的引用。在转换为swift 2.0后,NSClassFromString(“UITransitionView”)出现问题在iOS 9中仍然存在:(我还更新了Swift 2。0@user023我已经在提交到应用商店的2到3个应用中使用了这个精确的解决方案,没有问题!我想,因为您只是根据字符串检查类的类型,所以这很好(可以是任何字符串)。可能导致拒绝的原因是在你的应用程序中有一个名为
    UITransitionView
    的类,因为它是作为应用程序符号的一部分提取的,我认为应用程序
    ////
    //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)