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

6

我在基础的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;
}

那么我该如何处理呢?我知道这是基本的内存处理问题,但我不知道怎么做。


为什么需要在LoginViewController上保留mainPlatformRootControler?如果从LoginViewController呈现RootViewController,则可以通过使用self.presentingViewControllerRootViewController获取loginViewController,然后调用一个方法而不使用通知。 - trungduc
我可以看到RootViewController实例正在堆叠,因为我有多个观察者。这是错误的,notificationcenter不会保留观察者。 - Marcio Romero Patrnogic
请提供您在导航栈中添加和“移除”RootViewController的代码。 - Marcio Romero Patrnogic
7个回答

1
首先,在viewDidLoad中观察"shouldLogOut",应该像下面这样:
    [[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;
}

希望这能帮到你!

0

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

让我们来理解一下: 您有一个带窗口的主应用程序委托。

在didFinishLaunch中的代码:

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

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

appDelegate.window = mainController

在MainController中的代码: MainController将拥有AppDelegate的实例,在注销后,您必须更改

appDelegate.window = loginController

希望这可以帮助您!


0
你在LoginViewControllerviewDidLoad中是否添加了以下类似的通知观察器?
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(shouldLogOut:) name:@"shouldLogOut" object:nil];

我猜你可能错过了这个,那么当RootViewController被解散后,你的登录类将无法接收到通知。


0

正如您所说,多个观察者会创建问题,因此当您不需要它时,必须删除观察者。

在您的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是否在堆栈中或者是从头开始加载等问题。因为实际的问题在于您的观察者。


0

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

基本上,我在注销/登录时交换主 UIWindowrootViewController。此外,我编程提供 rootViewController,而不是让 @UIApplicationMain 加载初始视图控制器。这样做的好处是,在应用程序启动期间,如果用户已经登录,则不需要加载 Login.storyboard

show 函数可以根据您的风格进行配置,但我喜欢交叉溶解转换,因为它们非常简单。

enter image description here

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

这段代码是一个完整的示例,您可以创建一个新项目,清空“主界面”字段,然后将此代码放入应用程序委托中。

最终的过渡效果:

enter image description here


0

由于您在注销后解雇了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);
        };

0
问题可能是因为在注销时你从未解除对RootViewController的引用。通过将属性mainPlatformRootControler设置为nil,你只是从LoginViewController的角度放弃了对该对象的所有权。但这并不意味着其他任何持有对mainPlatformRootControler后面的对象的引用的对象也会解除其引用。

要解决这个问题,在RootViewController中添加一个logout通知的观察者,并在接收到通知时通过dismiss(animated:completion)关闭自身。

另外,如果你仅仅保存mainPlatformRootControler来将其置为空,则根本不需要该属性。通过正确地解除它(按照我上面写的方法),它将自动被清理,因此也不需要担心将其置为nil。(当然,如果你有其他保留mainPlatformRootControler的原因,那么就不要删除它。)


Andy,抱歉我完全忘记说我正在其中解雇RootViewController。我已经更新了我的答案。 - Karlo A. López

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接