4

问题

在我的 ARC 项目中,我有一个管理对象的类,称为LazyMutableArray. 有些对象实际上是 nil,但我收藏的用户永远不会知道这一点;因此我将它设为 的子类NSMutableArray,并尝试做“同样的事情”。特别是,对象在添加时会被保留

现在让我们看一下其他方法的内存行为。事实证明,AppleNSArray将销毁方法记录为该规则的一个例外,因为它们是release,而不是autoreleased对象。

addObject:关于++objectAtIndex:数组破坏的组合是否被 Apple 记录为永远不会自动释放,或者只是恰好出现在我测试的示例和 Apple 包含的示例中,存在一些争论。

如何在我的子类中创建具有完全相同内存语义的方法?


最后更新

经过一番思考,我决定NSMutableArray在这种情况下,与NSPointerArray. 我应该注意,新类与之前的实现具有相同的retain/autorelease对。

感谢 Rob Napier,我发现对我的objectAtIndex:方法的任何修改都不会改变这种行为,这回答了我最初关于这种方法的问题。

在实践层面,一些人说任何方法都可以无缘无故地解决额外的retain/对;autorelease以其他方式期望是不合理的,试图找出哪些方法可以做到这一点,哪些不可以做到这一点是不合理的。因此,在多个层面上,这对我来说都是一个很好的学习机会。

代码(基于NSMutableArray)可在 GitHub 上找到: implementationheadertest(that's -testLazyMutableMemorySemantics)。谢谢大家的参与。


为什么我尝试子类化NSMutableArray

我同意,子类化基础对象并不总是一个合适的解决方案。在这种情况下,我有对象(实际上是 OData 资源),其中大多数都有subobjects子对象数组最自然的类显然是NSArray. 使用不同的课程对我来说似乎没有意义。

但是对于 OData 集合,这个“子对象数组”虽然是一个 NSArray,但必须有不同的实现。具体来说,对于 1000 个元素的集合,鼓励服务器以(比如说)20 个为单位返回集合,而不是一次全部返回。如果在这种情况下有另一种合适的模式,我会全神贯注。


我如何找到这个的更多细节

我对这个集合进行了单元测试,可以将值放入数组中,从数组中读取,等等。到目前为止,一切都很好。但是,我意识到返回对象会增加其保留计数

我怎么看?假设我在惰性数组中插入了两个对象lazy,一个弱保留,一个强保留(*参见代码*)。然后按预期保留计数为weakSingleton1。但现在我阅读了元素:

XCTAssertEqual(weakSingleton, lazy[0], @"Correct element storage"); // line B

在调试器中,我看到保留计数上升到 2。当然,-retainCount可能会给我错误的信息,所以让我们尝试通过以下方式破坏数组中的引用

lazy[0] = nil; // yep, does the right thing
XCTAssertNil(weakSingleton, @"Dropped by lazy array"); // line C <-- FAIL

确实,我们看到weakSingleton没有发布。

现在你可能猜到它不仅仅是一个保留,它是一个自动释放的保留——在 B 行周围放置一个@autoreleaseweakSingleton释放. 这对的确切来源并不明显,但似乎来自NSPointerArray -addPointer:(不幸的是不是来自 ARC's [[object retain] autorelease])。但是,我不想返回一个自动释放的对象并使方法语义与其超类不同!

毕竟,我要覆盖的方法 NSMutableArray -objectAtIndex:` 并没有这样做;如果数组被释放,它返回的对象将立即释放,如 Apple 的示例中所述。这就是我想要的:修改 A 行周围的方法,使其返回的对象没有额外的保留/自动释放对。我不确定编译器是否应该让我这样做:)

注意 1我可以为单个文件关闭 ARC,但这将是我的第一个非 ARC Objective-C 代码。无论如何,这种行为可能不是来自 ARC。

注2有什么大惊小怪的?好吧,在这种情况下,我可以更改我的单元测试,但事实是,通过添加或删除 B 行,我正在更改 C 行的单元测试结果。

换句话说,我的方法所描述的行为[LazyMutableArray -objectAtIndex]本质上是通过读取索引 0 处的对象,我实际上是在更改该对象的保留计数,这意味着我可能会遇到意外的错误。

注意 3当然,如果对此不采取任何措施,我将记录此行为并继续;也许,这确实应该被视为一个实现细节,而不是包含在测试中。


实现的相关方法

    @implementation LazyMutableArray {
        NSPointerArray *_objects; 
        // Created lazily, only on -setCount:, insert/add object.
    }

    - (id)objectAtIndex:(NSUInteger)index {
        @synchronized(self) {
            if (index >= self.count) {
                return nil;
            }
            __weak id object = [_objects pointerAtIndex:index];
                if (object) {
                    return object;
                }
            }

        // otherwise do something else to compute a return value
        // but this branch is never called in this test
        [self.delegate array:self missingObjectAtIndex:index];

        @synchronized(self) {
            if (index >= self.count) {
                return nil;
            }
            __weak id object = [_objects pointerAtIndex:index];
            if (object) {
                return object;
            }
        }
        @throw([NSException exceptionWithName:NSObjectNotAvailableException
                               reason:@"Delegate was not able to provide a non-nil element to a lazy array"
                             userInfo:nil]);

    }

    - (void)createObjects {
        if (!_objects) {
            _objects = [NSPointerArray strongObjectsPointerArray];
        }
    }

    - (void)addObject:(id)anObject {
       [self createObjects];
       [_objects addPointer:(__bridge void*)anObject];
    }

完整的测试代码:

// Insert two objects into lazy array, one held weakly, one held strongly.

NSMutableArray * lazy = [LazyMutableArray new];
id singleton = [NSMutableArray new];
[lazy addObject:singleton];

__weak id weakSingleton = singleton;
singleton = [NSMutableDictionary new];
[lazy addObject:singleton];

XCTAssertNotNil(weakSingleton, @"Held by lazy array");
XCTAssertTrue(lazy.count == 2, @"Cleaning and adding objects");

//    @autoreleasepool {
XCTAssertEqual(weakSingleton, lazy[0], @"Correct element storage");
XCTAssertEqual(singleton, lazy[1], @"Correct element storage");
//    }

lazy = nil;

XCTAssertNotNil(singleton, @"Not dropped by lazy array");
XCTAssertNil(weakSingleton, @"Dropped by lazy array");

最后一行失败,但如果我将第一行更改为lazy = [NSMutableArray new] 取消注释,它会成功@autoreleasepool

4

2 回答 2

2

首先,我不会创建这个子类。这正是NSPointerArray它的用途。将其包装成一个NSArray模糊的重要细节,这种方法可能会破坏。例如,[NSArray arrayWithArray:lazyMutableArray]如果lazyMutableArray包含 NULL 的正确行为是什么?假设NSArray永远不会包含 NULL 的算法需要警惕这一事实。确实,您可能会遇到类似的问题,将 non-retainingCFArray视为NSArray; 我从经验中说,这正是这种子类非常危险的原因(也是我几年前停止这样做的原因)。不要创建一个不能在所有可以使用其超类的情况下使用的子类(LSP)。

如果您有一个具有新语义的集合,我会将它从 子类化NSObject,并使其符合<NSFastEnumeration>. 看看如何NSPointerArray不是NSArray. 这不是意外。面对同样的问题,请注意苹果选择的方向。

现在你可能猜到它不仅仅是一个保留,它是一个自动释放的保留——在 B 行周围放置一个 @autorelease 会释放弱单例。这似乎是因为 ARC 下的 A 行转换为 [[object retain] autorelease]。但是,我不想返回一个自动释放的对象并让调用者记住这一点!

调用者不应该假设其他任何事情。调用者永远不能随意假设一个方法没有添加平衡的自动释放。如果调用者希望自动释放池耗尽,那是他们的责任。

综上所述,如果不需要,避免额外的自动释放有一些好处,这是一个有趣的学习机会。

我将首先将此代码简化为最简单的形式,根本没有您的子类。只需探索如何NSPointerArray工作:

__weak id weakobject;
@autoreleasepool
{
  NSPointerArray *parray = [NSPointerArray strongObjectsPointerArray];
  {
    id object = [NSObject new];
    [parray addPointer:(__bridge void*)object];
    weakobject = object;
  }
  parray = nil;
}
NSAssert(!weakobject, @"weakobject still exists");

我在这里的结构(例如额外的嵌套块)旨在避免意外创建我无意创建的强引用。

在我的实验中,这在没有自动释放池的情况下失败并成功。这表明额外的保留/自动释放是围绕或通过调用添加的addPointer:,而不是通过 ARC 修改您的界面。

如果您不将此实现用于addObject:,我有兴趣深入挖掘。这是一个有趣的问题,即使我不相信你应该这样子类化。

于 2013-11-09T23:31:05.257 回答
1

我将详细说明为什么我说这“看起来很像家庭作业”。这可能会为我赢得很多反对票,但对于后来发现这个问题的其他人来说,它也将成为一个很好的学习案例。

子类NSMutableArray化不是程序的目标。这是实现其他目标的一种手段。如果我冒昧地猜测一下,我希望您正在尝试创建一个数组,该数组在访问对象时会延迟创建对象。有更好的方法可以做到这一点,而无需自己处理内存管理。

这是我将如何实现延迟加载数组的示例。

@interface LazyMutableArray : NSMutableArray
- (id)initWithCreator:(id(^)(int))creator;
@end

@interface LazyMutableArray ( ) 
@property (nonatomic, copy) id (^creator)(int);
@property (nonatomic, assign) NSUInteger highestSet;
@end

@implementation LazyMutableArray

- (id)initWithCreator:(id(^)(int))creator
{
    self = [super init];
    if (self) {
        self.highestSet = NSNotFound;
        self.creator = creator;
    }
    return self;
}

- (id)objectAtIndex:(NSUInteger)index
{
    id obj = nil;
    if ((index < self.highestSet) && (self.highestSet != NSNotFound)) {
        obj = [super objectAtIndex:index];
        if ([obj isKindOfClass:[NSNull class]]) {
            obj = self.creator(index);
            [super replaceObjectAtIndex:index withObject:obj];
        }
    } else {
        if (self.highestSet == NSNotFound) {
            self.highestSet = 0;
        }
        while (self.highestSet < index) {
            [super add:[NSNull null]];
            self.highestSet += 1;
        }
        obj = self.creator(index);
        [super add:obj];
        self.highestSet += 1;
    }
    return obj;
}

公平警告:我没有编译或检查任何这些代码的语法。它可能有一些错误,但通常应该可以工作。此外,此实现缺少add:countremoveObjectAtIndex:insertObject:atIndex:和可能的实现replaceObjectAtIndex:withObject:。我在这里展示的只是为了让你开始。

于 2013-11-09T23:01:51.560 回答