【问题标题】:Cocoa class member variable allocated inside function call nil unless forced to init/loadCocoa 类成员变量在函数调用 nil 内分配,除非强制初始化/加载
【发布时间】:2019-06-25 05:27:47
【问题描述】:

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

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

设置:

  • Xcode 10.1 (10B61)
  • macOS High Sierra 10.13.6
  • 从头开始 Cocoa 项目
  • 使用情节提要
  • 添加文件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 为零(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

【问题讨论】:

  • textViewNSTextContainer 的弱属性,textViewaddNewPage: 末尾超出范围时会被释放。
  • @Willeke 你知道为什么如果你强制它会起作用吗?
  • 请注意,m_mainStorage 不是实例变量。至于它为什么“起作用”,很可能是巧合,也可能是因为该调用最终触发了保留/自动释放。
  • textView of NSTextContainer 调用 objc_loadWeak

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


【解决方案1】:

我对可可不太熟悉,但我认为问题在于 ARC(自动引用计数)。

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

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

所以从函数返回后,它会被释放

但是,如果您将 textView 设为 TestMainView 的实例变量,它会按预期工作。 不太确定为什么如果你强制它也可以工作。 ~~(也许是编译器优化?)~~

这似乎是强迫,即调用

if (textContainer.textView) {

正在触发保留/自动释放调用,因此在下一次自动释放排水调用之前,textview 仍然存在。(我猜它在 awakeFromNib 函数返回之前不会被耗尽)。它起作用的原因是您在自动释放池释放它之前将 textView 添加到视图层次结构(强引用)。

【讨论】:

  • “也许是编译器优化” 可能正好相反,实际上,我怀疑它不会在“发布”版本中工作。
  • 现在这很有意义!但是 textContainer 变量不应该增加引用计数器,因为该值已分配给 initWithFrame() 内的 textContainer.textView 吗?如果文本容器仍在使用它,它是如何被释放的?
  • 文本容器对文本视图的引用很弱。所以引用计数不会改变。
  • 基本上你需要一个强大的参考。您可以将 textView 作为 TestMainView 的实例变量,也可以将 textView 添加到 addPage 函数中的视图层次结构中。
  • 哦,在从strongweak 的切换上很明显。这是一个很大的变化。
【解决方案2】:

cekisakurek's answer 是正确的。如果没有对它们的拥有(/“强”)引用,则对象将被释放。文本容器和文本视图都没有相互拥有的引用。容器有一个对视图的weak 引用,这意味着它会在视图终止时自动设置为nil。 (视图对容器有一个非空引用,这意味着如果容器在视图还活着的情况下被释放,你将在textView.textContainer 中有一个悬空指针。)

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

“强制加载”与延迟初始化无关;正如 bbum 评论的那样,它“有效”很可能是偶然的。我强烈怀疑它不会在优化的版本中。

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

【讨论】:

  • 感谢您的详细信息。我现在感觉有点被documentation 欺骗了,因为它明确指出“您明确地创建所有四个文本对象并将它们连接在一起,只维护对 NSTextStorage 对象的引用。”
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-07-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-11-02
  • 2016-11-09
相关资源
最近更新 更多