NSViewController在Mac OS X 10.4上的应用

4
我正在处理一个代码库,而不是一个应用程序,在10.4上开始开发,并需要在10.4上运行,但可以一直运行到10.8。它手动从nib文件加载视图,我最近才意识到存在严重的内存泄漏问题,因为这些nib利用绑定并针对文件的拥有者进行绑定,创建引用循环并阻止文件的拥有者类释放。我认为这更糟糕的原因是“文件的拥有者”加载了自己的nib。
我使用以下代码加载nib(此代码位于基类中,子类覆盖+ nibName):
NSString *nibName = [[self class] nibName];
NSNib *nib = [[NSNib alloc] initWithNibNamed:nibName bundle:myBundle];
[nib instantiateNibWithOwner:self topLevelObjects:&topLevelObjects];

由于我必须针对10.4版本进行开发,因此无法使用NSViewController。 我认为我需要实现自己的视图控制器类,但是如何防止引用循环 发生,并且保证像NSViewController类一样工作? 如果视图控制器是nib的“文件所有者”,那么我不会把引用循环从当前类推到视图控制器吗? NSViewController如何防止这种情况发生?

2个回答

2
NSViewController在内存管理方面并没有什么特别之处,甚至对于顶级对象也是如此。它只是提供了一个安全的地方来加载nib文件,并在整个nib生命周期内将其内容保留在内存中,这意味着该类本身只是一个外部文件的所有者。为了好玩,我重新实现了这个类,并注释掉了一些有趣的部分。有些东西我直接删除了,因为它们太过hacky而不值得实现,或者因为它们很少使用而不值得重新制作。完整的类,包括文档和注释,可以在这里找到;
@interface CFIViewController : NSResponder <NSCoding> {
@private
    NSString *_nibName;
    NSBundle *_nibBundle;
    id _representedObject;
    NSString *_title;
    IBOutlet NSView *view;
    NSArray *_topLevelObjects;
    id _autounbinder; 
    //NSString *_designNibBundleIdentifier;
}

- (id)initWithNibName:(NSString*)nibName bundle:(NSBundle *)nibBundleOrNil;

- (void)setRepresentedObject:(id)representedObject;
- (id)representedObject;

- (void)setTitle:(NSString *)title;
- (NSString *)title;

- (NSView *)view;
- (void)loadView;

- (NSString *)nibName;
- (NSBundle *)nibBundle;

- (void)setView:(NSView *)view;

@end

@implementation CFIViewController

- (void)loadView {
    NSArray *topLevelObjects = nil;

    NSNib *loadedNib = [[[NSNib alloc]initWithNibNamed:self.nibName bundle:self.nibBundle]autorelease];
    if (loadedNib == nil) {
        [NSException raise:NSInternalInconsistencyException format:@"-[%@ %@]", NSStringFromClass(self.class), NSStringFromSelector(_cmd)];
        return;
    }

    BOOL loaded = NO;


#if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_8
    loaded = [loadedNib instantiateWithOwner:self topLevelObjects:&topLevelObjects];
#else 
    loaded = [loadedNib instantiateNibWithOwner:self topLevelObjects:&topLevelObjects];
#endif

    if (loaded) {
        [self _setTopLevelObjects:topLevelObjects];
        [topLevelObjects makeObjectsPerformSelector:@selector(release)];
    } else {
        [NSException raise:NSInternalInconsistencyException format:@"CFIViewController could not instantiate the %@ nib.", self.nibName];
    }

    if (self.view != nil) {
        [self viewDidLoad];
        return;
    }

    [NSException raise:NSInternalInconsistencyException format:@"-[%@ %@]", NSStringFromClass(self.class), NSStringFromSelector(_cmd)];
}

@end

这是一个相当简单的机制。在Cocoa中,NSViewController添加到任何类型的控制器隐喻中的真正功能,是能够与NSDocument及其底层的Core Data混乱有序地工作。
如果视图控制器是nib的“文件所有者”,那么我不会将引用循环从当前类推到我的视图控制器吗?NSViewController如何防止这种情况发生?
NSViewController以最有趣的方式处理顶级对象的保留。当它获取对包含它们的数组的引用时,它会浅复制该数组,然后释放旧数组中的所有对象。实际上,NSViewController夺走了所有对NIB解冻对象的引用,使其远离NSCoder,从而确保在数组在dealloc中消失时安全释放。
但是,在绑定方面,NSViewController具有内部getter用于名为NSAutounbinder的NSProxy子类,KVO在绑定和解除绑定对象时会查找它。通过调整释放并提供内部autounbinder指针的getter,控制器类可以轻松释放自己及其绑定,而无需争斗。绝不建议您在未验证KVO仍然查找autounbinder getter的情况下,在未来的OS X版本中使用CFIViewController中的实现,但对于大多数其他版本,它似乎都可以。CFIViewController提供了使用内部NSAutoUnbinder类的选项,因此解决了任何绑定保留周期问题。

这比我预期的要花费更多的精力!感谢您,我会仔细研究它。当有赏金可用时,我会为您的努力奖励您一些额外的积分。 - dreamlax
@dreamlax 谢谢你!我真的很喜欢深入了解AppKit并使其更易管理。请随意在任何未来项目中使用它。 - CodaFi
如果我现在接受了你的答案,悬赏选项就会消失,所以我暂时取消了接受(但不用担心,我会回来的!) - dreamlax
厉害的代码!也许是因为缺咖啡,但我看不出你代码中的 _setTopLevelObjects: 如何对对象进行浅拷贝。它确实对数组进行了拷贝,但没有对其中包含的对象进行拷贝。我有什么遗漏吗? - Monolo
@Monolo 正是如此。它对数组进行了浅拷贝,然后从 NSCoder 底层释放了旧值。它有效地在其端保留了该数组(在 Cocoa 中,这意味着现在它和 NSCoder 拥有它),然后断开了 NSCoder 对对象的链接。 - CodaFi
显示剩余12条评论

1
我建议您只使用NSWindowController或自定义子类。您的子类可以有一个view输出口和一个符合KVO的representedObject属性。这应该足以替代10.4兼容性。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接