如何正确管理内存栈和视图控制器?

How to correctly manage memory stack and view controllers?

我真的在为这些基本的 iOS 编程问题苦苦挣扎,但我就是不知道发生了什么以及如何解决它。

我有我的主登录控制器,它检测用户何时登录并在身份验证成功时显示下一个控制器:

@interface LoginViewController (){

    //Main root instance
    RootViewController *mainPlatformRootControler;
}

-(void)loggedInActionWithToken:(NSString *)token anonymous:(BOOL)isAnon{
    NSLog(@"User loged in.");

    mainPlatformRootControler = [self.storyboard instantiateViewControllerWithIdentifier:@"rootViewCOntrollerStoryIdentifier"];

    [self presentViewController:mainPlatformRootControler animated:YES completion:^{

    }];

}

效果很好,没问题。

我的麻烦是处理注销。如何完全删除 RootViewController 实例并显示一个新实例?

我可以看到 RootViewController 实例正在堆叠,因为我有多个观察者,在注销然后登录后,它们被调用多次(我退出并重新进入的次数)。

我尝试了以下但没有成功:

首先在 RootViewController 中检测到注销并关闭:

[self dismissViewControllerAnimated:YES completion:^{
                [[NSNotificationCenter defaultCenter] postNotificationName:@"shouldLogOut" object:nil];

            }];

然后在LoginViewController中:

-(void)shouldLogOut:(NSNotification *) not{
    NSLog(@"No user signed in");
    mainPlatformRootControler = NULL;
    mainPlatformRootControler = nil;
}

那我该如何处理呢?我知道它是一个基本的内存句柄,但我不知道怎么做?

问题 很可能是您在注销发生时从未关闭 RootViewController。通过将 属性 mainPlatformRootControler 设置为 nil,从 LoginViewController 的角度来看,您只是放弃了对象的所有权。这并没有说明任何其他也拥有对 mainPlatformRootControler.

背后对象的引用的内容

要解决此问题RootViewController 内为 logout 通知添加一个通知观察器,当收到通知时,自行关闭通过 dismiss(animated:completion)

奖金 你也不需要 属性 mainPlatformRootControler 如果你所做的只是保存它以消除它。通过适当地关闭它(以我上面写的方式),它将自动被清理,因此也不需要担心 nil 将其清除。 (现在,如果您有其他原因保留 mainPlatformRootControler,那么显然不要删除它)。

因为登录和注销是一次性的过程,所以在登录后,不显示新的控制器,而是用主控制器替换登录控制器。

让我们来了解一下: 您有 window.

的主应用程序委托

didFinishLaunch 中的代码:

if (loggedIn) {
     self.window = yourMainController
} else {
     self.window = loginController
}

登录控制器中的代码: LoginController 将有 AppDelegate 实例,登录后,您必须更改

appDelegate.window = mainController

主控制器中的代码: MainController 将具有 AppDelegate 实例,注销后,您必须更改

appDelegate.window = loginController

希望对您有所帮助!!

您是否在 LoginViewControllerviewDidLoad 中添加了通知观察器,如下所示

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(shouldLogOut:) name:@"shouldLogOut" object:nil];

我猜你错过了这个,然后你的登录 class 在 RootViewController 关闭后无法收到通知。

首先,你必须观察"shouldLogOut" in viewDidLoad 应该如下所示:

    [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(shouldLogout:) name:@"shouldLogout" object:nil];

然后在 dismissViewControllerAnimated 中应该如下所示:

[self dismissViewControllerAnimated:true completion:^{
        [[NSNotificationCenter defaultCenter] postNotificationName:@"shouldLogOut" object:nil];
    }];

您需要在登录视图控制器中定义 shouldLogOut: 选择器

-(void)shouldLogOut:(NSNotification *) not{
    mainPlatformRootControler = nil;
}

希望对您有所帮助!

正如您所说,有多个观察者会产生问题,那么您必须在不需要时移除观察者。

在你的 RootViewController

-(void)viewWillAppear:(BOOL)animated  
{  
  [super viewWillAppear:animated];  

  // Add observer
  [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(shouldLogout:) name:@"shouldLogout" object:nil];
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];

    // Remove observer by name
    [[NSNotificationCenter defaultCenter] removeObserver:self name:@"shouldLogout" object:nil];
}

因此,通过这种方式,您不必考虑您的 RootViewController 是在堆栈中还是从新加载等。因为实际问题出在您的观察者身上。

管理视图层次结构的正确方法有很多,但我将分享一种我发现简单有效的方法。

基本上,我在日志 out/in 处换出了主 UIWindowrootViewController。此外,我以编程方式提供 rootViewController 而不是让 @UIApplicationMain 加载初始视图控制器。这样做的好处是,在应用程序启动期间,如果用户已登录,则永远不必加载 Login.storyboard

show 功能可以根据您的风格进行配置,但我喜欢交叉溶解过渡,因为它们非常简单。

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    lazy var window: UIWindow? = {

        let window = UIWindow()
        window.makeKeyAndVisible()

        return window
    }()

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

        // Your own logic here
        let isLoggedIn = false

        if isLoggedIn {
            show(MainViewController(), animated: false)
        } else {
            show(LoginViewController(), animated: false)
        }

        return true
    }
}

class LoginViewController: UIViewController {

    override func loadView() {
        let view = UIView()
        view.backgroundColor = .red
        let logoutButton = UIButton()
        logoutButton.setTitle("Log In", for: .normal)
        logoutButton.addTarget(self, action: #selector(login), for: .touchUpInside)
        view.addSubview(logoutButton)
        logoutButton.translatesAutoresizingMaskIntoConstraints = false

        NSLayoutConstraint.activate(
            [logoutButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
             logoutButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)]
        )

        self.view = view
    }

    @objc
    func login() {
        AppDelegate.shared.show(MainViewController())
    }
}

class MainViewController: UIViewController {

    override func loadView() {
        let view = UIView()
        view.backgroundColor = .blue
        let logoutButton = UIButton()
        logoutButton.setTitle("Log Out", for: .normal)
        logoutButton.addTarget(self, action: #selector(logout), for: .touchUpInside)
        view.addSubview(logoutButton)
        logoutButton.translatesAutoresizingMaskIntoConstraints = false

        NSLayoutConstraint.activate(
            [logoutButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
             logoutButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            ]
        )

        self.view = view
    }

    @objc
    func logout() {
        AppDelegate.shared.show(LoginViewController())
    }
}

extension AppDelegate {

    static var shared: AppDelegate {
        // swiftlint:disable force_cast
        return UIApplication.shared.delegate as! AppDelegate
        // swiftlint:enable force_cast
    }
}

private let kTransitionSemaphore = DispatchSemaphore(value: 1)

extension AppDelegate {

    /// Animates changing the `rootViewController` of the main application.
    func show(_ viewController: UIViewController,
              animated: Bool = true,
              options: UIViewAnimationOptions = [.transitionCrossDissolve, .curveEaseInOut],
              completion: (() -> Void)? = nil) {

        guard let window = window else { return }

        if animated == false {
            window.rootViewController = viewController
            return
        }

        DispatchQueue.global(qos: .userInitiated).async {
            kTransitionSemaphore.wait()

            DispatchQueue.main.async {

                let duration = 0.35

                let previousAreAnimationsEnabled = UIView.areAnimationsEnabled
                UIView.setAnimationsEnabled(false)

                UIView.transition(with: window, duration: duration, options: options, animations: {
                    self.window?.rootViewController = viewController
                }, completion: { _ in
                    UIView.setAnimationsEnabled(previousAreAnimationsEnabled)

                    kTransitionSemaphore.signal()
                    completion?()
                })
            }
        }
    }
}

这段代码是一个完整的例子,你可以新建一个项目,清除掉"Main Interface"字段,然后把这段代码放到app delegate中。

结果转换:

由于您正在关闭 RootViewController 并且在注销后将引用设为 nil 但实例未释放,唯一的其他可能性是其他东西保留了对 RootViewController 的引用。你可能有一个保留周期。 如果两个对象彼此有强引用,就会发生循环引用。并且由于在释放所有强引用之前无法释放对象,因此存在内存泄漏。

保留循环的例子包括:

    RootViewController *root = [[RootViewController alloc] init];
    AnOtherViewController *another = [[AnOtherViewController alloc] init];
    //The two instances reference each other
    root.anotherInstance = another;
    another.rootInstance = root; 

或者

    self.block = ^{
                //self is captured strongly by the block
                //and the block is captured strongly by the self instance
                NSLog(@"%@", self);
            };

解决方案是对其中一个引用使用弱指针。由于弱指针是不保留其目标的指针。 例如

@property(weak) RootViewController *anotherInstance;

_typeof(self) __weak weakSelf = self
self.block = ^{
             _typeof(self) strongSelf = weakSelf
            //self is captured strongly by the block
            //and the block is captured strongly by the self instance
            NSLog(@"%@", strongSelf);
        };