Cocoa class 在函数调用 nil 内分配的成员变量,除非强制 init/load

Cocoa class member variable allocated inside function call nil unless forced to init/load

我来自 C/C++ 背景,目前正在学习 Cocoa 和 Objective-C。

我有一个涉及延迟初始化的奇怪行为(除非我弄错了)并且感觉我缺少一些非常基本的东西。

设置:

基本上,我在视图控制器中创建了一个 NSTextView 以便能够编写一些文本。 在 TestMainView.m 中,我按照 here

中所述以编程方式创建对象链

有两条路径:

第一个代码路径按预期工作:我可以写一些文本。

但是第二条代码路径不起作用,因为在 return 上,textContainer.textView 为零(textContainer 值本身完全没问题)。

但更麻烦的是(这就是我怀疑 lazy init 是罪魁祸首的地方)是如果我在函数调用中 "force" textContainer.textView 值,那么一切正常.您可以通过将 FORCE_VALUE_LOAD 设置为 1 来尝试此操作。

它不一定是 if(),它也适用于 NSLog()。如果您在 return 行设置断点并使用调试器打印值 ("p textContainer.textView")

,它甚至可以工作

所以我的问题是:

我真的希望我在这里遗漏了一些东西,因为我不能期望我随机检查 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 的对象(我不熟悉)。