Rò rỉ chế độ xem khi thay đổi rootViewController bên trong chuyển tiếpWithView


97

Trong khi điều tra rò rỉ bộ nhớ, tôi đã phát hiện ra một vấn đề liên quan đến kỹ thuật gọi setRootViewController:bên trong khối hoạt ảnh chuyển tiếp:

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

Nếu bộ điều khiển chế độ xem cũ (bộ đang được thay thế) hiện đang trình bày bộ điều khiển chế độ xem khác, thì đoạn mã trên không loại bỏ chế độ xem đã trình bày khỏi hệ thống phân cấp chế độ xem.

Đó là, chuỗi hoạt động này ...

  1. X trở thành Bộ điều khiển Chế độ xem gốc
  2. X trình bày Y, để chế độ xem của Y hiển thị trên màn hình
  3. Sử dụng transitionWithView:để biến Z trở thành Bộ điều khiển Chế độ xem gốc mới

... có vẻ ổn đối với người dùng, nhưng công cụ Thứ bậc của Chế độ xem Gỡ lỗi sẽ tiết lộ rằng chế độ xem của Y vẫn ở đó phía sau chế độ xem của Z, bên trong a UITransitionView. Tức là, sau ba bước trên, phân cấp chế độ xem là:

  • UIWindow
    • UITransitionView
      • UIView (góc nhìn của Y)
    • UIView (góc nhìn của Z)

Tôi nghi ngờ đây là một vấn đề bởi vì, tại thời điểm chuyển đổi, chế độ xem của X thực sự không phải là một phần của hệ thống phân cấp chế độ xem.

Nếu tôi gửi dismissViewControllerAnimated:NOtới X ngay trước đó transitionWithView:, hệ thống phân cấp chế độ xem kết quả là:

  • UIWindow
    • UIView (góc nhìn của X)
    • UIView (góc nhìn của Z)

Nếu tôi gửi dismissViewControllerAnimated:(CÓ hoặc KHÔNG) tới X, sau đó thực hiện chuyển đổi trong completion:khối, thì phân cấp chế độ xem là chính xác. Thật không may, điều đó cản trở hoạt ảnh. Nếu tạo hiệu ứng cho việc sa thải, nó sẽ lãng phí thời gian; nếu không hoạt ảnh, nó trông bị hỏng.

Tôi đang thử một số cách tiếp cận khác (ví dụ: tạo một lớp bộ điều khiển chế độ xem vùng chứa mới để phục vụ như bộ điều khiển chế độ xem gốc của tôi) nhưng không tìm thấy bất kỳ thứ gì hoạt động. Tôi sẽ cập nhật câu hỏi này khi tôi tiếp tục.

Mục tiêu cuối cùng là chuyển đổi trực tiếp từ chế độ xem đã trình bày sang bộ điều khiển chế độ xem gốc mới và không để lại các cấu trúc phân cấp chế độ xem lạc chỗ.


Hiện tại tôi cũng gặp phải vấn đề này
Alex,

Tôi vừa phải đối mặt với vấn đề tương tự
Jamal Zafar

Bất kỳ may mắn tìm thấy một giải pháp tốt cho điều này? Cùng một vấn đề CHÍNH XÁC ở đây.
David Baez

@DavidBaez Tôi đã viết mã để loại bỏ mạnh mẽ tất cả các bộ điều khiển chế độ xem trước khi thay đổi gốc. Tuy nhiên, nó rất cụ thể cho ứng dụng của tôi. Kể từ khi đăng bài này, tôi đã tự hỏi liệu hoán đổi cái UIWindowcó phải là điều nên làm hay không, nhưng tôi không có thời gian để thử nghiệm nhiều.
benzado

Câu trả lời:


119

Tôi đã có một vấn đề tương tự gần đây. Tôi đã phải xóa thủ công đó UITransitionViewkhỏi cửa sổ để khắc phục sự cố, sau đó gọi loại bỏ trên bộ điều khiển chế độ xem gốc trước đó để đảm bảo nó được phân bổ.

Cách khắc phục không thực sự tốt lắm nhưng trừ khi bạn đã tìm ra cách tốt hơn kể từ khi đăng câu hỏi, đó là cách duy nhất tôi thấy có hiệu quả! viewControllerchỉ là newControllercâu hỏi ban đầu của bạn.

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];
}];

Tôi hy vọng điều này cũng giúp bạn giải quyết vấn đề của mình, đó là một nỗi đau tuyệt đối ở mông!

Swift 3.0

(Xem lịch sử chỉnh sửa cho các phiên bản Swift khác)

Để triển khai đẹp hơn như một phần mở rộng UIWindowcho phép chuyển đổi tùy chọn vào.

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()
            }
        }
    }
}

Sử dụng:

window.set(rootViewController: viewController)

Hoặc là

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

6
Cảm ơn. Nó đã làm việc. Xin đừng chia sẻ nếu bạn tìm thấy một cách tiếp cận tốt hơn
Jamal Zafar

8
Có vẻ như việc thay thế bộ điều khiển chế độ xem gốc đã trình bày các chế độ xem (hoặc cố gắng xử lý UIWindow vẫn có bộ điều khiển chế độ xem đã trình bày) sẽ dẫn đến rò rỉ bộ nhớ. Đối với tôi, dường như việc trình bày một bộ điều khiển dạng xem tạo ra một vòng lặp giữ lại với cửa sổ và loại bỏ các bộ điều khiển là cách duy nhất tôi tìm thấy để phá vỡ nó. Tôi nghĩ rằng một số khối hoàn thành nội bộ có tham chiếu mạnh đến cửa sổ.
Carl Lindberg

Đã gặp sự cố với NSClassFromString ("UITransitionView") sau khi chuyển đổi sang nhanh chóng 2.0
Eugene Braginets

Vẫn còn xảy ra trong iOS 9 nữa :( Tôi cũng đã cập nhật cho Swift 2.0
Rich

1
@ user023 Tôi đã sử dụng giải pháp chính xác này trong 2 hoặc 3 ứng dụng được gửi đến App Store mà không gặp sự cố! Tôi đoán vì bạn chỉ kiểm tra loại của lớp đối với một chuỗi, nó tốt (nó có thể là bất kỳ chuỗi nào). Điều có thể gây ra từ chối là có một lớp có tên UITransitionViewtrong ứng dụng của bạn vì sau đó lớp đó được chọn như một phần của các ký hiệu của ứng dụng mà tôi nghĩ rằng App Store sử dụng để kiểm tra.
Phong phú

5

Tôi đã đối mặt với vấn đề này và nó làm tôi khó chịu trong cả ngày. Tôi đã thử giải pháp obj-c của @ Rich và hóa ra khi tôi muốn trình bày một viewController khác sau đó, tôi sẽ bị chặn với một UITransitionView trống.

Cuối cùng, tôi đã tìm ra cách này và nó đã hiệu quả với tôi.

- (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];
}

Được rồi, bây giờ tất cả những gì bạn phải làm là gọi [self setRootViewController:newViewController];khi bạn muốn chuyển đổi bộ điều khiển chế độ xem gốc.


Hoạt động tốt, nhưng có một đèn flash khó chịu của bộ điều khiển chế độ xem trình bày ngay trước khi bộ điều khiển chế độ xem gốc được chuyển vào. Hoạt ảnh giao dismissViewControllerAnimated:diện có lẽ tốt hơn một chút so với không có hoạt ảnh. Không tránh được bóng ma UITransitionViewtrong hệ thống phân cấp chế độ xem.
pkamb

5

Tôi thử một điều đơn giản phù hợp với tôi trên iOs 9.3: chỉ cần xóa chế độ xem cũ của viewController khỏi hệ thống phân cấp của nó trong khi dismissViewControllerAnimatedhoàn thành.

Hãy làm việc trên chế độ xem X, Y và Z như được giải thích bởi benzado :

Đó là, chuỗi hoạt động này ...

  1. X trở thành Bộ điều khiển Chế độ xem gốc
  2. X trình bày Y, để chế độ xem của Y hiển thị trên màn hình
  3. Sử dụng chuyển đổiWithView: để biến Z trở thành Bộ điều khiển Chế độ xem gốc mới

Đưa ra :

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

Trong trường hợp của tôi, X và Y là đối phó tốt và quan điểm của họ không còn phân cấp nữa!


0

Gặp sự cố tương tự. Trong trường hợp của tôi, tôi có cấu trúc phân cấp viewController và một trong những bộ điều khiển chế độ xem con có bộ điều khiển chế độ xem được trình bày. Khi tôi thay đổi bộ điều khiển chế độ xem gốc của cửa sổ, vì một số lý do, bộ điều khiển chế độ xem được trình bày vẫn còn trong bộ nhớ. Vì vậy, giải pháp là loại bỏ tất cả bộ điều khiển chế độ xem trước khi tôi thay đổi bộ điều khiển chế độ xem gốc của cửa sổ.


-2

Tôi đã gặp sự cố này khi sử dụng mã này:

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

    })
}

Vô hiệu hóa mã này, đã khắc phục sự cố. Tôi đã quản lý để làm cho điều này hoạt động bằng cách chỉ bật hoạt ảnh chuyển tiếp này khi thanh bộ lọc được hoạt ảnh được khởi chạy.

Đó không thực sự là câu trả lời mà bạn đang tìm kiếm, nhưng nó có thể đưa bạn đến đúng chỗ để tìm ra giải pháp của mình.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.