Does UISplitViewController have a retain cycle bug in iOS 9?
Asked Answered
W

2

7

In the following example, I am presenting a UIViewController that has a UIStackViewController as its child:

UIViewController *splitViewParentVC = UIViewController.new;

UIViewController *masterVC = UIViewController.new;
UIViewController *detailVC = UIViewController.new;

UISplitViewController *splitViewController = [[UISplitViewController alloc] init];
splitViewController.viewControllers = @[masterVC, detailVC];

[splitViewParentVC addChildViewController:splitViewController];
[splitViewParentVC.view addSubview:splitViewController.view];
[splitViewController didMoveToParentViewController:splitViewParentVC];
splitViewController.view.frame = splitViewParentVC.view.bounds;
splitViewController.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;

__weak UISplitViewController *wSplitViewController = splitViewController;

[self presentViewController:splitViewParentVC animated:YES completion:nil];

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    [self dismissViewControllerAnimated:YES completion:^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            if (wSplitViewController) {
                NSLog(@"the split view controller has leaked");
            } else {
                NSLog(@"the split view controller didn't leak");
            }
        });
    }];
});

In iOS 9 and 9.1, the above code will print the split view controller has leaked, indicating that the UIStackViewController has leaked (more importantly, it leaks its master and detail view controllers as well).

Wittie answered 21/10, 2015 at 23:44 Comment(1)
Does removing the child before dismissing the splitviewcontroller fix the leak?Flotage
W
4

Yes, a retain cycle bug has been confirmed to exist in iOS 9 by Apple Staff.

I've tested that the retain cycle does not exist in iOS 8.4, but does exist in iOS 9.0 and 9.1. The leak seems to be fixed as of iOS 9.2 (tested in Xcode 7.2 beta 2 on the iOS 9.2 Simulator) I've put together a sample project to easily confirm whether or not UISplitViewController causes itself to leak (just run it and check the console output).

This also tests an attempt to allow the master and detail view controllers to be deallocated. As one can see, the master view controller still seems to be retained by the UISplitViewController even after it is removed from the UISplitViewController.viewControllers array property.

Here is the code from the sample project:

- (void)viewDidLoad {
    [super viewDidLoad];

    [self testSplitViewControllerRetainCycleWithCompletion:^{
        [self testManuallyFreeingUpMasterAndDetailViewControllers];
    }];
}

- (void)testSplitViewControllerRetainCycleWithCompletion:(void (^)())completion {

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

        UIViewController *splitViewParentVC = UIViewController.new;

        UIViewController *masterVC = UIViewController.new;
        UIViewController *detailVC = UIViewController.new;

        UISplitViewController *splitViewController = [[UISplitViewController alloc] init];
        splitViewController.viewControllers = @[masterVC, detailVC];
        splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModeAllVisible;
        splitViewController.preferredPrimaryColumnWidthFraction = 0.3125; // 320 / 1024
        splitViewController.minimumPrimaryColumnWidth = 100;

        [splitViewParentVC addChildViewController:splitViewController];
        [splitViewParentVC.view addSubview:splitViewController.view];
        [splitViewController didMoveToParentViewController:splitViewParentVC];
        splitViewController.view.frame = splitViewParentVC.view.bounds;
        splitViewController.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;

        __weak UISplitViewController *wSplitViewController = splitViewController;
        __weak UIViewController *wMaster = masterVC;
        __weak UIViewController *wDetail = detailVC;

        [self presentViewController:splitViewParentVC animated:YES completion:nil];

        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [self dismissViewControllerAnimated:YES completion:^{
                dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                    if (wSplitViewController) {
                        NSLog(@"the split view controller has leaked");
                    } else {
                        NSLog(@"the split view controller didn't leak");
                    }
                    if (wMaster) {
                        NSLog(@"the master view controller has leaked");
                    } else {
                        NSLog(@"the master view controller didn't leak");
                    }
                    if (wDetail) {
                        NSLog(@"the detail view controller has leaked");
                    } else {
                        NSLog(@"the detail view controller didn't leak");
                    }

                    completion();
                });
            }];
        });
    });
}

- (void)testManuallyFreeingUpMasterAndDetailViewControllers {

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

        UIViewController *splitViewParentVC = UIViewController.new;

        UIViewController *masterVC = UIViewController.new;
        UIViewController *detailVC = UIViewController.new;

        UISplitViewController *splitViewController = [[UISplitViewController alloc] init];
        splitViewController.viewControllers = @[masterVC, detailVC];
        splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModeAllVisible;
        splitViewController.preferredPrimaryColumnWidthFraction = 0.3125; // 320 / 1024
        splitViewController.minimumPrimaryColumnWidth = 100;

        [splitViewParentVC addChildViewController:splitViewController];
        [splitViewParentVC.view addSubview:splitViewController.view];
        [splitViewController didMoveToParentViewController:splitViewParentVC];
        splitViewController.view.frame = splitViewParentVC.view.bounds;
        splitViewController.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;

        __weak UIViewController *wMaster = masterVC;
        __weak UIViewController *wDetail = detailVC;

        [self presentViewController:splitViewParentVC animated:YES completion:nil];

        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [self dismissViewControllerAnimated:YES completion:nil];

            splitViewController.viewControllers = @[UIViewController.new, UIViewController.new];

            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                if (wMaster) {
                    NSLog(@"the master view controller has STILL leaked even after an attempt to free it");
                } else {
                    NSLog(@"the master view controller didn't leak");
                }
                if (wDetail) {
                    NSLog(@"the detail view controller has STILL leaked even after an attempt to free it");
                } else {
                    NSLog(@"the detail view controller didn't leak");
                }
            });
        });
    });
}

UPDATE: The leak seems to be fixed as of iOS 9.2 (tested in Xcode 7.2 beta 2 on the iOS 9.2 Simulator)

Wittie answered 21/10, 2015 at 23:44 Comment(1)
Interesting how you tested it on iOS 9.4 when 9.1 came out today... Typo? :PNuncupative
N
0

As I know -[UIViewController addChildViewController:] has memory leak problem in iOS 9.0~9.1. So I think it not UISplitViewController's fault only. Snipets as follows,

- (void)viewDidLoad {
    [super viewDidLoad];
    MyFirstViewController *vc = [self.storyboard instantiateViewControllerWithIdentifier:@"MyFirstViewController"];
    [self addChildViewController:vc];
    [self.view addSubview:vc.view];
    [vc didMoveToParentViewController:self];
}

You will find that MyFirstViewController's dealloc has NOT be called if you retreat from the current view controller.

A possible workaround is use storyboard's Container View instead of addChildViewController in code. I'm comfirmed that Container View's child view controller will be released properly.

Another workaround is removeChildViewController: in -(void)viewDidDisappear:(BOOL)animated. However, as apple's staff mentioned, this workaround is not recommended.

- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    NSArray<__kindof UIViewController *> * children = self.childViewControllers;
    for (UIViewController *vc in children) { // not recommended
        [vc willMoveToParentViewController:nil];
        [vc.view removeFromSuperview];
        [vc removeFromParentViewController];
    }
}
Nador answered 13/11, 2015 at 7:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.