6

我见过其他几个相同形式的问题,但我要么 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此外,当我在块的声明中不包含修饰符时,我会遇到 bad_access 失败(^__block evaluateAndRecurse)。如果有人可以解释为什么会这样,那也将非常有帮助。谢谢!

4

2 回答 2

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而不是当前值。

于 2013-01-08T20:00:50.773 回答
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];
    }
}
于 2013-01-08T20:03:58.267 回答