【问题标题】:When is NS_RETURNS_RETAINED needed?什么时候需要 NS_RETURNS_RETAINED?
【发布时间】:2012-08-21 14:02:53
【问题描述】:

举个例子:

- (NSString *)pcen NS_RETURNS_RETAINED {
    return (__bridge_transfer NSString *)CFURLCreateStringByAddingPercentEscapes(NULL, (__bridge CFStringRef) self, NULL, (CFStringRef) @"!*'();:@&=+$,/?%#[]", kCFStringEncodingUTF8);
}

NS_RETURNS_RETAINED 放在那里是否正确?


另一个例子:

+ (UIImage *)resizeImage:(UIImage *)img toSize:(CGSize)size NS_RETURNS_RETAINED {
    UIGraphicsBeginImageContextWithOptions(size, NO, 0.0);
    [img drawInRect:...];
    UIImage *resizedImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return resizedImage;
}

这似乎更复杂,因为返回的 UIImage 是“Get”方法的结果。但是,它从中获取它的图形上下文是在该方法的范围内创建的,所以在这里也有NS_RETURNS_RETAINED 是否正确?


第三个例子:

@property (readonly) NSArray *places;
---
@synthesize places=_places;
---
- (NSArray *)places {
    if (_places)
        return _places;
    return [[NSArray alloc] initWithObjects:@"Unknown", nil];
}

不知道在这里做什么,因为返回的对象可能是新创建的,也可能不是。


最后一个问题;如果返回的对象是自动释放方法的结果,则可能不需要NS_RETURNS_RETAINED。所以说最后一个例子中的返回被修改为

return [NSArray arrayWithObject:@"Unknown"];

那么最好的做法是什么?

【问题讨论】:

    标签: objective-c ios memory-management automatic-ref-counting reference-counting


    【解决方案1】:

    第一个例子

    将 NS_RETURNS_RETAINED 放在那里是否正确?

    这是不正确 -- 这里不需要属性。添加属性会违反命名约定,这点非常重要。

    更详细地说,不需要属性,因为在使用(__bridge_transfer NSString*) 的示例中,引用是转移。有人可能会认为 CFCreated-Reference 可能需要更多的东西,但 (__bridge_transfer NSString*) 是将该引用转移到 ARC 所需的全部内容;让它为你管理。

    如果您使用 (__bridge NSString*)CF_*_Create_*_ 进行类型转换,那么 CFCreate 函数返回的引用将不会传输到 ARC,并且会引入泄漏。

    作为替代方案,如果您选择显式释放返回的字符串(例如使用CFRelease),则可以避免泄漏。

    第二个例子

    但是,它从中获取它的图形上下文是在该方法的范围内创建的,所以在这里也有 NS_RETURNS_RETAINED 是否正确?

    使用属性是不正确或不必要的。 ARC 使用命名约定和属性来确定要添加的引用计数操作——它理解您的程序。

    与第一个示例不同,不应创建显式 __bridge_transfer

    添加属性会破坏命名约定。

    第三个例子

    - (NSArray *)places 
    ...
    

    不知道在这里做什么,因为返回的对象可能是新创建的,也可能不是。

    同样,不应使用任何属性。不应创建明确的__bridge_transfer。 ARC 理解 ObjC 约定,包括返回现有和新创建的对象。它将为两条路径插入正确的引用计数操作。

    最后一个问题;如果返回的对象是自动释放方法的结果,则可能不需要 NS_RETURNS_RETAINED 。所以说最后一个例子中的返回被修改为

    return [NSArray arrayWithObject:@"Unknown"];
    

    同样,不需要任何属性。不应进行显式转移。

    在所有系统库中只存在该属性的少数用途。


    我真的,真的,真的,真的建议不要使用这些属性,尤其是:

    • 涉及动态调度的地方(所有 objc 方法都符合)
    • 其中参数(使用)和结果(保留返回)是 ObjC 类型

    基本原理是引用传输应该是本地实现的,并且几乎没有真正需要偏离它;向后兼容性可能是我能想到的“最好”的理由。如果您可以控制您的代码,只需更新它以尽可能做正确的事情,而不是引入这些属性。这可以通过遵守命名约定并在适当的情况下将引用转移到 ARC 来实现。

    有关使用属性和偏离命名约定时可能遇到的一些错误示例,请参阅:Deep copy of dictionaries gives Analyze error in Xcode 4.2

    坚持命名约定的另一个很好的理由是您并不总是知道您的程序将被如何使用。如果有人想在 MRC 翻译中使用您的程序,那么他们将不得不编写不寻常的程序,如下所示:

    某处

    - (NSString *)name NS_RETURNS_RETAINED;
    

    其他地方

    NSString * name = obj.name;
    NSLog(@"%@", name);
    [name release]; // << ME: not a mistake. triple checked.
    

    【讨论】:

    • 非常感谢您清除所有这些。出于兴趣,在什么情况下会使用 NS_RETURNS_RETAINED?
    • @Alec 不客气。如果您遵循惯例并遵守规则,我们中的许多人将永远不需要使用此属性。我已经提到了向后兼容性(也就是说,如果你想维护一个不遵循 Apple 命名约定的设计)。 Apple 的框架中也有一些有趣的用途; self-swapping on unarchiving 和 NSMakeCollectable(一个垃圾收集添加,也有一个消耗属性)——这几乎是所有 iOS 框架中的所有内容。 (续)
    • (续)我在一些(非常)内部函数(它们都使用静态调度)中使用了消费属性,以便在初始化和所有权“获取”期间进行汇集。总体而言,这些属性很少使用,而且非常内部。
    • @Alec - 虽然最终的建议是有效的,但我认为这个答案中给出的解释是错误,对不起贾斯汀。这里没有足够的空间来解释为什么,我将把它作为单独的答案添加。
    • @CRD 去吧 - 在我学会了一些东西之前无法入睡 :)
    【解决方案2】:

    [这个答案部分是对贾斯汀给出的答案的长评论/更正。先前的答案让我相信对属性的语义以及 ARC 如何处理返回引用的描述不正确。]

    答案在于ARC分析的工作原理和NS_RETURNS_RETAINED的含义。

    ARC 分析您的源以确定何时保留、释放或自动释放可保留对象引用。

    如果您的应用程序的所有来源都可用,那么理论上,分析可能能够从“第一原则”确定此信息 - 从最小的表达式开始并向外工作。 p>

    然而所有来源都不可用 - 例如有些已经在框架等中编译 - 因此在分析方法调用时,ARC 不会查看方法的来源,而只会查看其签名 - 它的名称及其参数和返回值的类型。

    仅考虑可保留对象类型的返回值 ARC 需要知道所有权是否正在转移 - 在这种情况下,ARC 需要在某个时候释放它 - 或不(例如 autoreleased 参考)——在这种情况下,如果需要所有权,ARC 将需要保留它。

    ARC 根据方法的名称 和任何属性确定此信息。以initnew 开头或包含copy 的方法根据定义转移所有权;所有其他方法都没有。属性 NS_RETURNS_RETAINED 通知 ARC,无论其名称如何,方法都会转移其返回引用的所有权。

    这是故事的一半……另一半是 ARC 如何处理方法体中的return 语句。

    return 实际上是一种赋值,在进行可保留对象引用赋值时,ARC 根据其对当前所有权和引用的了解以及目的地的要求。

    对于return 语句,毫无疑问,目标的要求由方法名称和签名上指定的任何属性决定。如果签名表明所有权正在转移,那么 ARC 将返回一个 retained 引用,否则它将返回一个 autoreleased 引用。

    了解 ARC 在方法调用的两端都起作用很重要,它确保返回适当的引用并且确定如何处理返回的引用。

    有了所有的序言,我们可以看看你的第一个例子。看起来你正在NSString 上编写一个方法,所以我们将添加该细节,首先我们将省略该属性:

    @interface NSString (AddingPercentEscapes)
    
    - (NSString *) pcen;
    
    @end
    
    @implementation NSString (AddingPercentEscapes)
    
    - (NSString *) pcen
    {
       return (__bridge_transfer NSString *)CFURLCreateStringByAddingPercentEscapes(NULL, (__bridge CFStringRef) self, NULL, (CFStringRef) @"!*'();:@&=+$,/?%#[]", kCFStringEncodingUTF8);
    }
    
    @end
    

    还有一个简单的用法:

    - (void)applicationDidFinishLaunching:(NSNotification *)aNotification
    {
       NSString *test = @"This & than > other";
    
       NSLog(@"pcen: %@", [test pcen]);
    }
    

    编译pcen方法return语句ARC看签名的时候,名字(pcen)不表示所有权转移,没有属性,所以ARC在返回的引用上加了autorelease通过表达式(__bridge_transfer NSString *) ... kCFStringEncodingUTF8),因为该表达式返回由pcen 拥有的引用。

    重要提示: 什么表达式并不重要,重要的是 pcen 是否拥有它保留的引用 - 特别是 __bridge_transfer 并不能确定所有权方法返回的引用。

    applicationDidFinishLaunching 方法中编译对pcen 的调用时,ARC 再次查看签名,确定当前方法需要所有权并且返回的引用不属于所有并插入retain

    您可以通过在 Xcode 中调用“产品 > 生成输出 > 程序集文件”来验证这一点,在生成的程序集中,您将在 pcen 的代码中看到类似于以下内容的内容:

    callq   _CFURLCreateStringByAddingPercentEscapes
    movq    %rax, %rdi
    callq   _objc_autoreleaseReturnValue
    addq    $16, %rsp
    popq    %rbp
    ret
    

    它显示了由 ARC 插入的自动释放,并在 applicationDidFinishLaunching 的程序集中类似于以下内容:

    callq   _objc_msgSend
    movq    %rax, %rdi
    callq   _objc_retainAutoreleasedReturnValue
    

    这是对 pcen 的调用,后跟插入的 ARC 保留。

    所以您的示例在没有注释的情况下工作正常,ARC 做的是正确的事情。但是它也适用于注释,让我们将界面更改为:

    @interface NSString (AddingPercentEscapes)
    
    - (NSString *) pcen NS_RETURNS_RETAINED;
    
    @end
    

    运行(和分析)这个版本,它也可以工作。但是生成的代码发生了变化,ARC 确定它应该根据属性的存在转移所有权,因此return 语句的程序集变为:

    callq   _CFURLCreateStringByAddingPercentEscapes
    addq    $16, %rsp
    popq    %rbp
    ret
    

    ARC 确实插入自动释放。在调用站点,程序集变为:

    callq   _objc_msgSend
    movq    -40(%rbp), %rdi         ## 8-byte Reload
    movq    %rax, %rsi
    movq    %rax, -48(%rbp)         ## 8-byte Spill
    movb    $0, %al
    callq   _NSLog
    

    ARC 在这里插入保留。

    所以两个版本都是“正确的”,但哪个更好?

    似乎带有该属性的版本更好,因为 ARC 不需要插入自动释放/保留;但是运行时优化了这个序列(因此调用_objc_retainAutoreleasedReturnValue 而不是_objc_retain 之类的东西)所以成本并不像看起来那么大。

    然而正确的答案是都不是...

    推荐的解决方案是依赖 Cocoa/ARC 约定并更改方法的名称,例如:

    @interface NSString (AddingPercentEscapes)
    
    - (NSString *) newPercentEscapedString;
    
    @end
    

    以及相关的更改。

    执行此操作,您将获得与 pcen NS_RETURNS_RETAINED 相同的代码,因为 ARC 确定它应该根据 名称 new... 转移所有权。

    这个答案已经(太)长了,希望以上内容可以帮助您找出其他两个示例的答案!

    【讨论】:

    • 谢谢 CRD,非常非常有用的答案。关于您遵循new... 命名约定的建议,似乎stringByAppendingString: 之类的Cocoa 方法不这样做。怎么会?
    • 还有一个可能的更正:Methods starting with init or new or containing copy transfer, by definition, ownership; all other methods do not. 不是allocnew 并包含copy吗?
    • @Alec - new...string...(一般为 &lt;classname&gt;...class 方法。这些约定早于 ARC。前者是类方法的约定,alloc & init;后者适用于allocinitautorelease。在您的示例中,您有一个创建新对象的 instance 方法。要让 ARC 自动转移所有权,该方法需要位于 init、new 或 copy 系列之一中。所以我选择了newPercentEscapedString,也许copyWithPercentEscapes 会是一个更好的名字,任你选!
    • @Alec - 回复alloc。正确,alloc 确实返回了被调用者拥有的引用。然而,它通常不会在列表中提及。 init 方法使用(即取得所有权)其参数(来自alloc)并返回被调用者拥有的引用 - 所以它在列表中。 [注意:不能保证init 返回它传递的相同引用,因此 *consumes - 如果它返回不同的引用,则释放传递的引用。 NSNumber 之类的类可能会这样做,例如从传递相同值的不同调用返回相同的引用。]*
    • @Justin - 您首先在第一个示例中说添加属性是 不正确 - 它不是。然后您通过引用__bridge_transfer 来解释这一点,并说这意味着您没有属性,再次错了。 return 表达式中的内容实际上与 ARC 无关,仅返回引用的所有权。在此示例中,__bridge_transfer 导致结果引用归 pcen 所有。因此,逻辑上应该添加属性,以便将此所有权转移给被调用者,实际上,最好将方法重命名为遵循约定,这样就会发生这种情况。
    猜你喜欢
    • 1970-01-01
    • 2012-09-16
    • 1970-01-01
    • 2018-12-10
    • 2010-12-29
    • 2017-07-13
    • 2011-08-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多