Cocoa class 在函数调用 nil 内分配的成员变量,除非强制 init/load
Cocoa class member variable allocated inside function call nil unless forced to init/load
我来自 C/C++ 背景,目前正在学习 Cocoa 和 Objective-C。
我有一个涉及延迟初始化的奇怪行为(除非我弄错了)并且感觉我缺少一些非常基本的东西。
设置:
- Xcode10.1 (10B61)
- macOS High Sierra 10.13.6
- 从头开始 Cocoa 项目
- 使用故事板
- 添加文件TestMainView.m/.h
- 在 main.storyboard 中的视图控制器下,将
NSView
自定义 class 设置为 TestMainView
- 在调试和发布版本下测试
基本上,我在视图控制器中创建了一个 NSTextView
以便能够编写一些文本。
在 TestMainView.m 中,我按照 here
中所述以编程方式创建对象链
有两条路径:
- 第一个是通过将
USE_FUNCTION_CALL
设置为 0 来启用的,它使整个代码 运行 都在 awakeFromNib()
. 中
- 通过将
USE_FUNCTION_CALL
设置为 1 启用第二个路径。它使文本容器和文本视图从函数调用 addNewPage() 分配,returns 文本容器供进一步使用.
第一个代码路径按预期工作:我可以写一些文本。
但是第二条代码路径不起作用,因为在 return 上,textContainer.textView
为零(textContainer
值本身完全没问题)。
但更麻烦的是(这就是我怀疑 lazy init 是罪魁祸首的地方)是如果我在函数调用中 "force" textContainer.textView
值,那么一切正常.您可以通过将 FORCE_VALUE_LOAD
设置为 1 来尝试此操作。
它不一定是 if()
,它也适用于 NSLog()
。如果您在 return 行设置断点并使用调试器打印值 ("p textContainer.textView")
,它甚至可以工作
所以我的问题是:
- 这与延迟初始化有关吗?
- 这是一个错误吗?有解决方法吗?
- 我是不是在考虑 Cocoa/ObjC 以错误的方式编程?
我真的希望我在这里遗漏了一些东西,因为我不能期望我随机检查 Cocoa class 中的变量,希望它们不会变成 nil
。它甚至会默默地失败(没有错误消息,什么都没有)。
TestMainView.m
#import "TestMainView.h"
#define USE_FUNCTION_CALL 1
#define FORCE_VALUE_LOAD 0
@implementation TestMainView
NSTextStorage* m_mainStorage;
- (void)awakeFromNib
{
[super awakeFromNib];
m_mainStorage = [NSTextStorage new];
NSLayoutManager* layoutManager = [[NSLayoutManager alloc] init];
#if USE_FUNCTION_CALL == 1
NSTextContainer* textContainer = [self addNewPage:self.bounds];
#else
NSTextContainer* textContainer = [[NSTextContainer alloc] initWithSize:NSMakeSize(FLT_MAX, FLT_MAX)];
NSTextView* textView = [[NSTextView alloc] initWithFrame:self.bounds textContainer:textContainer];
#endif
[layoutManager addTextContainer:textContainer];
[m_mainStorage addLayoutManager:layoutManager];
// textContainer.textView is nil unless forced inside function call
[self addSubview:textContainer.textView];
}
#if USE_FUNCTION_CALL == 1
- (NSTextContainer*)addNewPage:(NSRect)containerFrame
{
NSTextContainer* textContainer = [[NSTextContainer alloc] initWithSize:NSMakeSize(FLT_MAX, FLT_MAX)];
NSTextView* textView = [[NSTextView alloc] initWithFrame:containerFrame textContainer:textContainer];
[textView setMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)];
#if FORCE_VALUE_LOAD == 1
// Lazy init ? textContainer.textView is nil unless we force it
if (textContainer.textView)
{
}
#endif
return textContainer;
}
#endif
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
// Drawing code here.
}
@end
TestMainView.h
#import <Cocoa/Cocoa.h>
NS_ASSUME_NONNULL_BEGIN
@interface TestMainView : NSView
@end
NS_ASSUME_NONNULL_END
我对 cocoa 不太熟悉,但我认为问题出在 ARC(自动引用计数)上。
NSTextView* textView = [[NSTextView alloc] initWithFrame:containerFrame textContainer:textContainer];
在NSTextContainer的.h文件中可以看到NSTextView是弱引用类型
所以从函数返回后它被释放
但是,如果您将 textView 设为 TestMainView 的实例变量,它将按预期工作。
不太确定为什么如果你强制它也会起作用。 ~~(也许是编译器优化?)~~
这似乎是强迫,即调用
if (textContainer.textView) {
正在触发 retain/autorelease 调用,所以直到下一次自动释放耗尽调用,textview 仍然存在。(我猜它不会耗尽直到 awakeFromNib 函数 returns)。它起作用的原因是您在自动释放池释放它之前将 textView 添加到视图层次结构(强引用)。
是正确的。如果没有对它们的拥有 (/"strong") 引用,则对象将被释放。文本容器和文本视图都没有相互引用。容器对视图有一个 weak 引用,这意味着当视图消失时它会自动设置为 nil
。 (视图对容器有一个非零引用,这意味着如果在视图仍然存在时容器被释放,您将在 textView.textContainer
中有一个悬空指针。)
文本容器保持活动状态,因为它从方法返回并分配给一个变量,只要该变量在范围内,它就会创建一个拥有引用。该视图唯一拥有的引用位于 addNewPage:
方法内,因此它不会超过该范围。
"force load"与惰性初始化无关;正如 bbum 评论的那样,它 "works" 很可能是偶然的。我强烈怀疑它不会在优化的构建中。
让我向您保证,您 不需要 在 Cocoa 编程中随意地检查属性。但是您确实需要考虑对象之间的所有权关系。在这种情况下,其他东西需要同时拥有容器和视图。这可以是你的 class,通过 ivar/property,或者另一个适合 NSText{Whatever} API 的对象(我不熟悉)。
我来自 C/C++ 背景,目前正在学习 Cocoa 和 Objective-C。
我有一个涉及延迟初始化的奇怪行为(除非我弄错了)并且感觉我缺少一些非常基本的东西。
设置:
- Xcode10.1 (10B61)
- macOS High Sierra 10.13.6
- 从头开始 Cocoa 项目
- 使用故事板
- 添加文件TestMainView.m/.h
- 在 main.storyboard 中的视图控制器下,将
NSView
自定义 class 设置为TestMainView
- 在调试和发布版本下测试
基本上,我在视图控制器中创建了一个 NSTextView
以便能够编写一些文本。
在 TestMainView.m 中,我按照 here
有两条路径:
- 第一个是通过将
USE_FUNCTION_CALL
设置为 0 来启用的,它使整个代码 运行 都在awakeFromNib()
. 中
- 通过将
USE_FUNCTION_CALL
设置为 1 启用第二个路径。它使文本容器和文本视图从函数调用 addNewPage() 分配,returns 文本容器供进一步使用.
第一个代码路径按预期工作:我可以写一些文本。
但是第二条代码路径不起作用,因为在 return 上,textContainer.textView
为零(textContainer
值本身完全没问题)。
但更麻烦的是(这就是我怀疑 lazy init 是罪魁祸首的地方)是如果我在函数调用中 "force" textContainer.textView
值,那么一切正常.您可以通过将 FORCE_VALUE_LOAD
设置为 1 来尝试此操作。
它不一定是 if()
,它也适用于 NSLog()
。如果您在 return 行设置断点并使用调试器打印值 ("p textContainer.textView")
所以我的问题是:
- 这与延迟初始化有关吗?
- 这是一个错误吗?有解决方法吗?
- 我是不是在考虑 Cocoa/ObjC 以错误的方式编程?
我真的希望我在这里遗漏了一些东西,因为我不能期望我随机检查 Cocoa class 中的变量,希望它们不会变成 nil
。它甚至会默默地失败(没有错误消息,什么都没有)。
TestMainView.m
#import "TestMainView.h"
#define USE_FUNCTION_CALL 1
#define FORCE_VALUE_LOAD 0
@implementation TestMainView
NSTextStorage* m_mainStorage;
- (void)awakeFromNib
{
[super awakeFromNib];
m_mainStorage = [NSTextStorage new];
NSLayoutManager* layoutManager = [[NSLayoutManager alloc] init];
#if USE_FUNCTION_CALL == 1
NSTextContainer* textContainer = [self addNewPage:self.bounds];
#else
NSTextContainer* textContainer = [[NSTextContainer alloc] initWithSize:NSMakeSize(FLT_MAX, FLT_MAX)];
NSTextView* textView = [[NSTextView alloc] initWithFrame:self.bounds textContainer:textContainer];
#endif
[layoutManager addTextContainer:textContainer];
[m_mainStorage addLayoutManager:layoutManager];
// textContainer.textView is nil unless forced inside function call
[self addSubview:textContainer.textView];
}
#if USE_FUNCTION_CALL == 1
- (NSTextContainer*)addNewPage:(NSRect)containerFrame
{
NSTextContainer* textContainer = [[NSTextContainer alloc] initWithSize:NSMakeSize(FLT_MAX, FLT_MAX)];
NSTextView* textView = [[NSTextView alloc] initWithFrame:containerFrame textContainer:textContainer];
[textView setMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)];
#if FORCE_VALUE_LOAD == 1
// Lazy init ? textContainer.textView is nil unless we force it
if (textContainer.textView)
{
}
#endif
return textContainer;
}
#endif
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
// Drawing code here.
}
@end
TestMainView.h
#import <Cocoa/Cocoa.h>
NS_ASSUME_NONNULL_BEGIN
@interface TestMainView : NSView
@end
NS_ASSUME_NONNULL_END
我对 cocoa 不太熟悉,但我认为问题出在 ARC(自动引用计数)上。
NSTextView* textView = [[NSTextView alloc] initWithFrame:containerFrame textContainer:textContainer];
在NSTextContainer的.h文件中可以看到NSTextView是弱引用类型
所以从函数返回后它被释放
但是,如果您将 textView 设为 TestMainView 的实例变量,它将按预期工作。 不太确定为什么如果你强制它也会起作用。 ~~(也许是编译器优化?)~~
这似乎是强迫,即调用
if (textContainer.textView) {
正在触发 retain/autorelease 调用,所以直到下一次自动释放耗尽调用,textview 仍然存在。(我猜它不会耗尽直到 awakeFromNib 函数 returns)。它起作用的原因是您在自动释放池释放它之前将 textView 添加到视图层次结构(强引用)。
nil
。 (视图对容器有一个非零引用,这意味着如果在视图仍然存在时容器被释放,您将在 textView.textContainer
中有一个悬空指针。)
文本容器保持活动状态,因为它从方法返回并分配给一个变量,只要该变量在范围内,它就会创建一个拥有引用。该视图唯一拥有的引用位于 addNewPage:
方法内,因此它不会超过该范围。
"force load"与惰性初始化无关;正如 bbum 评论的那样,它 "works" 很可能是偶然的。我强烈怀疑它不会在优化的构建中。
让我向您保证,您 不需要 在 Cocoa 编程中随意地检查属性。但是您确实需要考虑对象之间的所有权关系。在这种情况下,其他东西需要同时拥有容器和视图。这可以是你的 class,通过 ivar/property,或者另一个适合 NSText{Whatever} API 的对象(我不熟悉)。