2

我有一个非常常见的 iOS 应用场景:

该应用程序的MainVC是一个UITabBarController。我在 AppDelegate.swift 文件中将此 VC 设置为 rootViewController:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    window = UIWindow()
    window?.rootViewController = MainVC()
    window?.makeKeyAndVisible()
}

当用户注销时,我展示了一个带有LandingVC的导航控制器作为导航堆栈的根视图控制器。

let navController = UINavigationController(rootViewController: LandingVC)
self.present(navController, animated: true, completion: nil)

LandingVC 中,您单击 Login 按钮,LoginVC被推到堆栈的顶部。

navigationController?.pushViewController(LoginVC(), animated: true)

当用户成功登录时,我从 LoginVC 内部解除()导航控制器。

self.navigationController?.dismiss(animated: true, completion: nil)

基本上,我正在尝试实现以下流程:

在此处输入图像描述

一切正常,但问题是LoginVC永远不会从内存中释放。因此,如果用户登录和注销 4 次(没有理由这样做但仍有机会),我将在内存中看到 4 次 LoginVC 和 0 次 LandingVC

我不明白为什么LoginVC没有被释放,但LandingVC是。

在我看来(并纠正我的错误),因为导航控制器出现并且它包含 2 个 VC(LandingVCLoginVC),当我在LoginVC中使用 dismiss() 时,它应该关闭导航控制器,因此两者都包含 VC .

  • MainVC : 展示 VC
  • 导航控制器:介绍 VC

来自苹果文档:

呈现视图控制器负责关闭它呈现的视图控制器。如果您在呈现的视图控制器本身上调用此方法,UIKit 会要求呈现的视图控制器处理解除。

我相信当我在LoginVC中关闭导航控制器时出了点问题。有没有办法在用户登录后立即在MainVC(呈现 VC)中触发dismiss()?

PS:使用下面的代码不会成功,因为它会弹出到导航堆栈的根视图控制器,即 LandingVC;而不是 MainVC。

self.navigationController?.popToRootViewController(animated: true)

任何帮助将非常感激!

=====================================

我的 LoginVC 代码:

import UIKit
import Firebase
import NotificationBannerSwift

class LoginVC: UIViewController {

    // reference LoginView
    var loginView: LoginView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // dismiss keyboard when clicking outside textfields
        self.hideKeyboard()

        // setup view elements
        setupView()
        setupNavigationBar()
    }

    fileprivate func setupView() {
        let mainView = LoginView(frame: self.view.frame)
        self.loginView = mainView
        self.view.addSubview(loginView)

        // link button actions from LoginView to functionality inside LoginViewController
        self.loginView.loginAction = loginButtonClicked
        self.loginView.forgotPasswordAction = forgotPasswordButtonClicked
        self.loginView.textInputChangedAction = textInputChanged

        // pin view
        loginView.translatesAutoresizingMaskIntoConstraints = false
        loginView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        loginView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
        loginView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        loginView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
}

    fileprivate func setupNavigationBar() {
        // make navigation controller transparent
        self.navigationController?.navigationBar.isTranslucent = true
        self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
        self.navigationController?.navigationBar.shadowImage = UIImage()

        // change color of text
        self.navigationController?.navigationBar.tintColor = UIColor.white

        // add title
        navigationItem.title = "Login"

        // change title font attributes
        let textAttributes = [
            NSAttributedStringKey.foregroundColor: UIColor.white,
            NSAttributedStringKey.font: UIFont.FontBook.AvertaRegular.of(size: 22)]
        self.navigationController?.navigationBar.titleTextAttributes = textAttributes
    }


    fileprivate func loginButtonClicked() {
        // some local authentication checks

        // ready to login user if credentials match the one in database
        Auth.auth().signIn(withEmail: emailValue, password: passwordValue) { (data, error) in
            // check for errors
            if let error = error {
                // display appropriate error and stop rest code execution
                self.handleFirebaseError(error, language: .English)
                return
            }


            // if no errors during sign in show MainTabBarController
            guard let mainTabBarController = UIApplication.shared.keyWindow?.rootViewController as? MainTabBarController else { return }

            mainTabBarController.setupViewControllers()

            // this is where i dismiss navigation controller and the MainVC is displayed
            self.navigationController?.dismiss(animated: true, completion: nil)
        }
    }

    fileprivate func forgotPasswordButtonClicked() {
        let forgotPasswordViewController = ForgotPasswordViewController()

        // present as modal
        self.present(forgotPasswordViewController, animated: true, completion: nil)
    }

    // tracks whether form is completed or not
    // disable registration button if textfields not filled
    fileprivate func textInputChanged() {
        // check if any of the form fields is empty
        let isFormEmpty = loginView.emailTextField.text?.count ?? 0 == 0 ||
        loginView.passwordTextField.text?.count ?? 0 == 0

        if isFormEmpty {
            loginView.loginButton.isEnabled = false
            loginView.loginButton.backgroundColor = UIColor(red: 0.80, green: 0.80, blue: 0.80, alpha: 0.6)
        } else {
            loginView.loginButton.isEnabled = true
            loginView.loginButton.backgroundColor = UIColor(red: 32/255, green: 215/255, blue: 136/255, alpha: 1.0)
        }
    }
}
4

1 回答 1

2

经过大量搜索,我想我找到了解决方案:

启发我的是所有人都在评论这个问题以及这篇文章:

https://medium.com/@stremsdoerfer/understanding-memory-leaks-in-closures-48207214cba

我将从我的编码哲学开始:我喜欢保持我的代码分离和干净。所以,我总是尝试用我想要的所有元素创建一个 UIView,然后将它“链接”到适当的视图控制器。但是当 UIView 有按钮并且按钮需要完成动作时会发生什么?众所周知,视图内部没有“逻辑”的空间:

class LoginView: UIView {

    // connect to view controller
    var loginAction: (() -> Void)?
    var forgotPasswordAction: (() -> Void)?

    // some code that initializes the view, creates the UI elements and constrains them as well

    // let's see the button that will login the user if credentials are correct
    let loginButton: UIButton = {
        let button = UIButton(title: "Login", font: UIFont.FontBook.AvertaSemibold.of(size: 20), textColor: .white, cornerRadius: 5)
        button.addTarget(self, action: #selector(handleLogin), for: .touchUpInside)
        button.backgroundColor = UIColor(red: 0.80, green: 0.80, blue: 0.80, alpha: 0.6)
        return button
    }()

    // button actions
    @objc func handleLogin() {
        loginAction?()
    }

    @objc func handleForgotPassword() {
        forgotPasswordAction?()
    }
}

正如文章所说:

LoginVCLoginView有一个强引用,它对 loginActionforgotPasswordAction闭包有一个强引用,它们刚刚创建了一个对 self 的强引用。

如您所见,我们有一个循环。这意味着,如果你退出这个视图控制器,它就不能从内存中删除,因为它仍然被闭包引用。

这可能是我的 LoginVC 从未从内存中释放的原因。[剧透警告:这就是原因!]

如问题所示,LoginVC 负责执行所有按钮操作。我之前做的是:

class LoginVC: UIViewController {

    // reference LoginView
    var loginView: LoginView!

    override func viewDidLoad() {
        super.viewDidLoad()
        setupView()
    }

    fileprivate func setupView() {
        let mainView = LoginView(frame: self.view.frame)
        self.loginView = mainView
        self.view.addSubview(loginView)

        // link button actions from LoginView to functionality inside LoginVC

        // THIS IS WHAT IS CAUSING THE RETAIN CYCLE <--------------------
        self.loginView.loginAction = loginButtonClicked
        self.loginView.forgotPasswordAction = forgotPasswordButtonClicked

        // pin view
        .....
    }

    // our methods for executing the actions
    fileprivate func loginButtonClicked() { ... }
    fileprivate func forgotPasswordButtonClicked() { ... }

}

现在我知道是什么导致了保留周期,我需要找到一种方法并打破它。正如文章所说:

要打破一个循环,你只需要打破一个引用,你会想要打破最简单的一个。在处理闭包时,您总是希望断开最后一个链接,这是闭包引用的内容。

为此,您需要在捕获不需要强链接的变量时指定。您拥有的两个选项是:weak 或 unowned,并且您在关闭的一开始就声明它。

所以我在LoginVC中为实现这一目标所做的更改是:

fileprivate func setupView() {

    ...
    ...
    ...

    self.loginView.loginAction = { [unowned self] in
        self.loginButtonClicked()
    }

    self.loginView.forgotPasswordAction = { [unowned self] in
        self.forgotPasswordButtonClicked()
    }

    self.loginView.textInputChangedAction = { [unowned self] in
        self.textInputChanged()
    }
}

在这个简单的代码更改之后(是的,我花了 10 天时间才弄明白),一切都像以前一样运行,但内存在感谢我。

有几件事要说:

  1. 当我第一次注意到这个内存问题时,我责怪自己没有正确关闭/弹出视图控制器。您可以在我之前的 StackOverflow 问题中找到更多信息:ViewControllers、内存消耗和代码效率

  2. 在这个过程中,我学到了很多关于展示/推送视图控制器和导航控制器的知识;所以即使我看错了方向,我也确实学到了很多东西。

  3. 没有什么是免费的,内存泄漏教会了我这一点!

希望我能帮助和我有同样问题的人!

于 2018-06-28T09:01:00.287 回答