Objective-C阻止“保留周期”警告,不明白为什么

时间:2013-01-08 19:47:34

标签: objective-c objective-c-blocks

我见过同样形式的其他几个问题,但我要么a)无法理解所提供的答案,要么b)看不出那些情况与我的相似。

我正在UIView上编写一个类别,以递归方式评估UIView的所有子视图,并返回通过测试的子视图数组。我已经注意到我的编译器警告发生在哪里:

-(NSArray*)subviewsPassingTest:(BOOL(^)(UIView *view, BOOL *stop))test {

   __block BOOL *stop = NO;

    NSArray*(^__block evaluateAndRecurse)(UIView*);
    evaluateAndRecurse = ^NSArray*(UIView *view) {
        NSMutableArray *myPassedChildren = [[NSMutableArray alloc] init];
        for (UIView *subview in [view subviews]) {
            BOOL passes = test(subview, stop);
            if (passes) [myPassedChildren addObject:subview];
            if (stop) return myPassedChildren;


            [myPassedChildren addObjectsFromArray:evaluateAndRecurse(subview)];
            // ^^^^ Compiler warning here ^^^^^
            // "Capturing 'evaluateAndRecurse' strongly in this block 
            // is likely to lead to a retrain cycle"
        }
        return myPassedChildren;
    };

    return evaluateAndRecurse(self);
}

此外,如果我在块的声明__block中未包含(^__block evaluateAndRecurse)修饰符,则会导致bad_access失败。如果有人能够解释为什么会这样,那也是非常有帮助的。谢谢!

2 个答案:

答案 0 :(得分:8)

这里的问题是你的块evaluteAndRecurse()捕获了自己,这意味着,如果它被复制(我不相信它会在你的情况下,但在稍微不那么简单的情况下它可能)然后它将保留自己,因此永远存在,因为没有什么可以打破保留周期。

编辑:Ramy Al Zuhouri提出了一个很好的观点,使用__unsafe_unretained对该块的唯一引用是危险的。只要块保留在堆栈上,这将起作用,但是如果需要复制块(例如,它需要转义到父作用域),则__unsafe_unretained将导致它被释放。以下段落已使用推荐的方法进行了更新:

您可能想要做的是使用标有__unsafe_unretained的单独变量,该变量也包含块,并捕获该单独的变量。这样可以防止它自行保留。你可以使用__weak,但是因为你知道在调用它时块必须是活的,所以没有必要打扰弱引用的(非常轻微的)开销。这将使您的代码看起来像

NSArray*(^__block __unsafe_unretained capturedEvaluteAndRecurse)(UIView*);
NSArray*(^evaluateAndRecurse)(UIView*) = ^NSArray*(UIView *view) {
    ...
        [myPassedChildren addObjectsFromArray:capturedEvaluateAndRecurse(subview)];
};
capturedEvaluateAndRecurse = evaluteAndRecurse;

或者,您可以捕获指向块的指针,该指针具有相同的效果,但允许您在块实例化之前而不是之后获取指针。这是个人偏好。它还允许您省略__block

NSArray*(^evaluateAndRecurse)(UIView*);
NSArray*(^*evaluteAndRecursePtr)(UIView*) = &evaluateAndRecurse;
evaluateAndRecurse = ^NSArray*(UIView*) {
    ...
        [myPassedChildren addObjectsFromArray:(*evaluateAndRecursePtr)(subview)];
};

至于需要__block,这是一个单独的问题。如果您没有__block,则块实例将实际捕获变量的先前值。请记住,创建块时,任何未标记为__block的捕获变量实际上都会在块实例化的位置存储为其状态的const副本。并且由于在之前创建了块,它被分配给变量,这意味着它在赋值之前捕获capturedEvaluteAndRecurse变量的状态,这将是nil(在ARC;否则,它将是垃圾内存。)

从本质上讲,您可以将给定的块实例视为实际上是每个捕获变量都具有ivar的隐藏类的实例。因此,使用您的代码,编译器基本上将其视为:

// Note: this isn't an accurate portrayal of what actually happens
PrivateBlockSubclass *block = ^NSArray*(UIView *view){ ... };
block->stop = stop;
block->evaluteAndRecurse = evaluateAndRecurse;
evaluteAndRecurse = block;

希望这可以清楚地说明为什么它会捕获evaluateAndRecurse的先前值而不是当前值。

答案 1 :(得分:0)

我做过类似的事情,但以不同的方式减少分配新阵列的时间,并且没有任何问题。你可以尝试调整你的方法看起来像这样:

- (void)addSubviewsOfKindOfClass:(id)classObject toArray:(NSMutableArray *)array {

    if ([self isKindOfClass:classObject]) {

        [array addObject:self];
    }

    NSArray *subviews = [self subviews];
    for (NSView *view in subviews) {

        [view addSubviewsOfKindOfClass:classObject toArray:array];
    }
}