除非被迫初始化/加载,否则在函数调用nil中分配的可可类成员变量

时间:2019-01-31 17:13:52

标签: objective-c macos cocoa memory-management automatic-ref-counting

我来自C / C ++背景,目前正在学习一些有关Cocoa和Objective-C的知识。

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

设置:

  • Xcode 10.1(10B61)
  • macOS High Sierra 10.13.6
  • 从零开始的可可项目开始
  • 使用情节提要
  • 添加文件TestMainView.m / .h
  • 在main.storyboard的View Controller下,将NSView自定义类设置为TestMainView
  • 在调试和发布版本下进行了测试

基本上,我在视图控制器中创建了一个NSTextView以便能够写一些文本。 在TestMainView.m中,我以编程方式创建了描述为here

的对象链

有两条路径:

  • 通过将USE_FUNCTION_CALL设置为0启用第一个,它使整个代码在awakeFromNib()内部运行。
  • 通过将USE_FUNCTION_CALL设置为1来启用第二条路径。它使文本容器和文本视图可以通过函数调用addNewPage()分配,并返回该文本容器以供进一步使用。

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

但是第二条代码路径不起作用,因为返回时textContainer.textView为nil(textContainer值本身就很好)。

更令人不安的是(如果我怀疑这是偷懒初始化的罪魁祸首)是,如果我在函数调用中“强制” textContainer.textView值,那么一切都很好。您可以将FORCE_VALUE_LOAD设置为1。

它不必是if(),它也可以与NSLog()一起使用。如果您在返回行上设置断点并使用调试器打印值(“ p textContainer.textView”),它甚至可以工作

所以我的问题是:

  • 这与延迟初始化有关吗?
  • 那是个错误吗?有解决方法吗?
  • 我在想用错误的方式设计Cocoa / ObjC吗?

我真的希望我在这里遗漏了一些东西,因为不能期望我在Cocoa类中随机检查变量,希望它们不会变成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

2 个答案:

答案 0 :(得分:2)

我对可可粉不太熟悉,但我认为问题是ARC(自动参考计数)。

NSTextView* textView = [[NSTextView alloc] initWithFrame:containerFrame textContainer:textContainer];

在NSTextContainer的.h文件中,您可以看到NSTextView是弱引用类型。

enter image description here

因此从函数返回后,它将被释放

但是,如果将textView设为TestMainView的实例变量,则它将按预期工作。 不太确定为什么强制使用它也能起作用。 ~~(也许编译器最优化了?)~~

似乎是在打电话给

if (textContainer.textView) {

会触发保留/自动释放调用,因此直到下一个自动释放消耗调用之前,textview仍处于活动状态。(我猜测直到awakeFromNib函数返回之前,它不会被消耗掉)。之所以起作用,是因为您要在自动释放池释放textView之前将其添加到视图层次结构(一个强大的参考)中。

答案 1 :(得分:2)

cekisakurek's answer是正确的。如果没有对它们的拥有(/“强”)引用,则将对象释放。文本容器和文本视图都没有相互拥有的引用。该容器具有对该视图的引用,这意味着该视图消失时将自动设置为nil。 (该视图对容器具有非固定的引用,这意味着如果在视图仍处于活动状态时释放了容器,则textView.textContainer中将有一个悬空指针。)

文本容器保持活动状态,因为它是从方法返回并分配给变量的,只要该变量在范围内,该容器就会创建一个拥有引用。该视图唯一的引用位于addNewPage:方法内部,因此它不会超出该范围。

“强制负载”与延迟初始化无关;正如bbum所说,它“起作用”很可能是偶然的。我强烈怀疑它不会在优化的版本中实现。

让我向您保证,您不需要需要在Cocoa编程中随意修改属性。但是您确实需要考虑对象之间的所有权关系。在这种情况下,其他东西需要同时拥有容器和视图。可以通过ivar / property在这里使用您的类,也可以是给定NSText {Whatever} API合适的另一个对象(我不熟悉)。