如何正确管理内存栈和视图控制器?
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
希望对您有所帮助!!
您是否在 LoginViewController
的 viewDidLoad
中添加了通知观察器,如下所示
[[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 处换出了主 UIWindow
的 rootViewController
。此外,我以编程方式提供 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);
};
我真的在为这些基本的 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
希望对您有所帮助!!
您是否在 LoginViewController
的 viewDidLoad
中添加了通知观察器,如下所示
[[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 处换出了主 UIWindow
的 rootViewController
。此外,我以编程方式提供 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);
};