我有一个非常常见的iOS应用场景:
该应用程序的 MainVC 是 UITabBarController 。我将此VC设置为AppDelegate.swift文件中的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 内部,单击“登录”按钮,然后将 LoginVC 推入堆栈的顶部。
navigationController?.pushViewController(LoginVC(), animated: true)
当用户成功登录后,我从LoginVC内部关闭导航控制器。
self.navigationController?.dismiss(animated: true, completion: nil)
基本上,我正在尝试实现以下流程:
一切正常,但是问题是 LoginVC 从未从内存中取消分配。因此,如果用户登录并注销4次(没有理由这样做,但仍然有机会),我将在内存中看到 LoginVC 4次,而LandingVC则看到0次。
我不明白为什么 LoginVC 没有被释放,而 LandingVC 被取消分配。
在我的脑海中(并纠正我在哪里错了),因为当我使用导航控制器时,它包含2个VC( LandingVC 和 LoginVC ) LoginVC 内部的dismiss(),应关闭导航控制器,因此两者都包含VC。
来自Apple文档:
呈现视图控制器负责解散其呈现的视图控制器。如果您在呈现的视图控制器本身上调用此方法,则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)
}
}
}
答案 0 :(得分:1)
经过大量搜索,我想找到了解决方法:
启发我的是所有人都在评论这个问题以及这篇文章:
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?()
}
}
因此,如文章所述:
LoginVC 对 LoginView 有很强的引用,对 loginAction 和 forgotPasswordAction 闭包的引用也很强刚刚创建了一个强烈的自我参考。
您可以很清楚地看到我们有一个循环。这意味着,如果退出此视图控制器,则无法将其从内存中删除,因为它仍被闭包引用。
这可能是为什么我的LoginVC从未从内存中释放的原因。 [SPOILER ALERT:那是原因!]
如问题所示,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() { ... }
}
现在,我知道是什么原因导致了保留周期,我需要找到一种方法并打破它。如文章所述:
要打破一个循环,您只需要打破一个参考,您就想打破最简单的一个。在处理闭包时,您总是希望断开最后一个链接,即闭包所引用的内容。
为此,您需要在捕获不需要强链接的变量时指定。您有两个选择:弱或没有所有权,您可以在关闭的开始就声明它。
所以我在 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天的时间弄清楚了),一切都像以前一样运行,但是内存感谢了我。
耦合要说的话:
当我第一次注意到此内存问题时,我责怪自己没有正确关闭/弹出视图控制器。您可以在以下我的上一个StackOverflow问题中找到更多信息:ViewControllers, memory consumption and code efficiency
在此过程中,我学到了很多有关显示/推动视图控制器和导航控制器的知识;因此,即使我朝错误的方向看,我也确实学到了很多东西。
没有什么是免费的,内存泄漏教会了我!
希望我可以和我一样解决其他问题!