委托使用“强引用”是否合适?

19

我有一个类,从URL获取JSON数据,并通过协议/委托模式返回数据。

MRDelegateClass.h

#import <Foundation/Foundation.h>

@protocol MRDelegateClassProtocol
@optional
- (void)dataRetrieved:(NSDictionary *)json;
- (void)dataFailed:(NSError *)error;
@end

@interface MRDelegateClass : NSObject
@property (strong) id <MRDelegateClassProtocol> delegate;

- (void)getJSONData;
@end

请注意,我正在使用strong来定义我的代理属性。稍后会详细讲解...

我正试图编写一个“包装器”类,以块格式实现getJSONData。

MRBlockWrapperClassForDelegate.h

#import <Foundation/Foundation.h>

typedef void(^SuccessBlock)(NSDictionary *json);
typedef void(^ErrorBlock)(NSError *error);

@interface MRBlockWrapperClassForDelegate : NSObject
+ (void)getJSONWithSuccess:(SuccessBlock)success orError:(ErrorBlock)error;
@end

MRBlockWrapperClassForDelegate.m

#import "MRBlockWrapperClassForDelegate.h"
#import "MRDelegateClass.h"

@interface DelegateBlock:NSObject <MRDelegateClassProtocol>
@property (nonatomic, copy) SuccessBlock successBlock;
@property (nonatomic, copy) ErrorBlock errorBlock;
@end

@implementation DelegateBlock
- (id)initWithSuccessBlock:(SuccessBlock)aSuccessBlock andErrorBlock:(ErrorBlock)aErrorBlock {
    self = [super init];
    if (self) {
        _successBlock = aSuccessBlock;
        _errorBlock = aErrorBlock;
    }
    return self;
}

#pragma mark - <MRDelegateClass> protocols
- (void)dataRetrieved:(NSDictionary *)json {
    self.successBlock(json);
}
- (void)dataFailed:(NSError *)error {
    self.errorBlock(error);
}
@end

// main class
@interface MRBlockWrapperClassForDelegate()
@end

@implementation MRBlockWrapperClassForDelegate

+ (void)getJSONWithSuccess:(SuccessBlock)success orError:(ErrorBlock)error {
    MRDelegateClass *delegateClassInstance = [MRDelegateClass new];
    DelegateBlock *delegateBlock = [[DelegateBlock alloc] initWithSuccessBlock:success andErrorBlock:error];
    delegateClassInstance.delegate = delegateBlock; // set the delegate as the new delegate block
    [delegateClassInstance getJSONData];
}

@end

我相对较晚接触Objective-C(只生活在ARC时代,并且仍在努力理解块),承认我的内存管理理解方面有些薄弱。

这段代码似乎工作正常,但只有当我的代理是strong时才有效。我了解到我的代理应该是weak以避免潜在的保留循环。查看仪器后,发现进一步的调用不会导致分配增长。然而,我相信'最佳实践'是要有弱引用的代理。

问题

Q1)是否可以使用 strong 代理?

Q2)如何实现基于块的包装器,同时将基础类的代理作为 weak 代理(即,在*delegateBlock接收协议方法之前防止其被释放)?


7
不要以“get”为前缀命名方法。 - bbum
@bbum 我知道对于属性来说,使用 'set' 是不好的,但我不知道对于方法来说,使用 'get' 也是被视为不良习惯。我需要更深入地了解命名规范。还在不断学习 :) - So Over It
“get” 是为那些通过引用返回值的方法所保留的,而且很少使用。 - bbum
4个回答

15

问题1 - 是的,正如你自己指出的,将委托属性设为弱引用是为了帮助避免保留循环而给出的建议。因此,拥有强委托本身并没有什么问题,但如果你的类的客户端期望它是弱引用,那么你可能会给他们带来惊喜。更好的方法是保持委托的弱引用,对于需要强引用的时期,服务端(具有委托属性的类)在内部保持一个强引用。正如@Scott所指出的,苹果文档中就使用这种方法来处理NSURLConnection。当然,这种方法并不能解决你想要让服务器为你保留委托的问题...

问题2 - 从客户端的角度来看,问题在于如何在服务器只拥有弱引用的情况下保持委托的存活。这个问题有一个标准的解决方案,叫做“关联对象”。简单来说,Objective-C运行时实际上允许将一组键-值对对象与另一个对象相关联,同时还可以指定关联策略,即该关联应该持续多长时间。要使用这个机制,你只需要选择自己的唯一键,类型为void * - 即一个地址。以下代码概述展示了如何使用这个机制,以NSOpenPanel为例:

#import <objc/runtime.h> // import associated object functions

static char myUniqueKey; // the address of this variable is going to be unique

NSOpenPanel *panel = [NSOpenPanel openPanel];

MyOpenPanelDelegate *myDelegate = [MyOpenPanelDelegate new];
// associate the delegate with the panel so it lives just as long as the panel itself
objc_setAssociatedObject(panel, &myUniqueKey, myDelegate, OBJC_ASSOCIATION_RETAIN);
// assign as the panel delegate
[panel setDelegate:myDelegate];

使用关联策略 OBJC_ASSOCIATION_RETAIN 可以保留传入的对象 (myDelegate),只要与它关联的对象 (panel) 还在存在并且会在其被释放时释放该对象。

采用这种方案避免了使委托属性本身强引用的问题,并让客户端能够控制委托对象是否被保留。如果您也正在实现服务器,可以提供一个方法来完成这个任务,比如 associatedDelegate:? ,以避免客户端需要定义键并自己调用 objc_setAssociatedObject。(或者您可以使用类别将其添加到现有类中。)

希望这能够帮助您。


谢谢。关联对象范例似乎是我一直在寻找的。从你所推断和代码片段中我现在看到,似乎关联会自动释放?还是说我需要在不再需要关联时明确地“释放”它? - So Over It
2
@SoOverIt - 你可以选择在以后的阶段删除关联,但如果不这样做,它将一直保留,直到与之关联的对象被回收。如果策略是OBJC_ASSOCIATION_RETAIN,那么意味着在关联时进行保留,在对象被回收时进行释放。有关更多详细信息和支持的其他内容,请参阅文档。 - CRD

13

这完全取决于您对象的架构。

当人们使用弱引用委托时,通常是因为委托是某种“父”对象,它保留了具有委托的东西(让我们称之为“委托者”)。为什么必须是父对象呢?其实不必如此;但是,在大多数情况下,这似乎是最方便的模式。由于委托是保留委托者的父对象,因此委托者不能保留委托,否则将会形成循环引用,因此它对委托持有一个弱引用。

然而,这不是唯一的使用情况。以iOS中的UIAlertViewUIActionSheet为例。它们通常的用法是:在一个函数内部,创建一个带有消息和添加按键的警报视图,设置它的委托,进行任何其他自定义,调用-show方法显示出来,然后忘记它(它不会存储在任何地方)。这是一种“发射并忘记”的机制。一旦你show它,你就不需要保留它或做其他处理,它仍然会显示在屏幕上。在某些情况下,您可能希望将警报视图存储起来,以便可以通过编程方式解除它,但这是罕见的;在绝大多数情况下,您只需显示并忘记它,然后处理任何委托调用即可。

因此,在这种情况下,适当的样式应该是一个强委托,因为1)父对象不保留警报视图,所以不存在保留循环问题,2)委托需要被保留,这样当警报视图上的某个按钮被按下时,有人会回应。现在,很多时候,#2不是问题,因为委托(父对象)是某种视图控制器或其他被其他东西保留的东西。但并非总是如此。例如,我可以简单地拥有一个不属于任何视图控制器的方法,任何人都可以调用它来显示一个警报视图,并且如果用户按下"Yes",上传一些东西到服务器。由于它不是任何控制器的一部分,它很可能不被任何东西保留。但它需要保持足够长的时间,直到警报视图完成。因此,理想情况下,警报视图应具有对它的强引用。

但正如我之前提到过的那样,这并不总是您要求警报视图的方式;有时您希望在编程上保留它并将其关闭。在这种情况下,您需要一个弱委托,否则它将导致保留循环。那么,警报视图应该有强委托还是弱委托呢?呵呵,主叫者应该决定!在某些情况下,调用者需要强委托;在其他情况下,调用者需要弱委托。但这如何可能?警报视图委托由警报视图类声明,并且必须声明为强或弱。

幸运的是,有一种解决方案可以让调用者决定——基于块的回调。在基于块的API中,块本质上成为了委托;但块不是父对象。通常情况下,块在调用类中创建并捕获self,以便它可以对“父对象”执行动作。委托者(在此情况下是警报视图)始终强引用块。但是,块可能具有对父对象的强引用或弱引用,这取决于在调用代码中如何编写块(要捕获对父对象的弱引用,请不要直接在块中使用self,而是在块外部创建一个弱版本的self,并让块使用该版本)。通过这种方式,调用代码完全控制委托者是否具有对其的强引用或弱引用。


10

您是正确的,委托通常是弱引用。但在某些情况下,需要使用强引用,甚至是必需的。苹果在NSURLConnection中使用了这种方式:

下载期间,连接会保持对委托的强引用。当连接完成加载、失败或被取消时,它会释放该强引用。

NSURLConnection实例只能使用一次。在完成(无论成功还是失败)后,它会释放委托,而且由于委托是readonly的,因此不能(安全地)重用。

您可以做类似的事情。在dataRetrieveddataFailed方法中,将您的委托设置为nil。如果想要重复使用对象,则可能不需要将委托设为readonly,但您仍然需要重新分配委托。


感谢 Scott 提供的详细信息。当使用强引用委托时,明确将委托设置回 nil 是有意义的。 - So Over It

0

就像其他人所说的那样,这与架构有关。但我将通过几个例子带您了解:

失败重试

假设您已经创建了一个URLSession,并正在等待您通过viewController进行的网络调用。有时候,如果失败了并不重要,但在其他情况下却很重要。例如,您的应用程序正在向另一个用户发送消息,然后您关闭了该视图控制器,但是网络请求失败了。您是否希望它重试?如果是,则必须保留该viewController在内存中,以便它可以再次提交请求。

写入磁盘

另一种情况是,当请求成功时,您可能希望将某些内容写入磁盘,因此即使在viewcontroller更新其UI后,您仍可能希望将本地数据库与服务器同步。

大型后台任务

NSURLSession的原始用途是支持后台网络任务执行,大文件下载等等。您需要在内存中使用某些东西来处理这些任务的完成,以指示执行已完成,操作系统可以将应用程序置于睡眠状态。

将下载大型文件的生命周期与特定视图相关联是一个坏主意...它需要与某些更稳定/持久的东西绑定,例如会话本身...

通常情况下,如果我要使用委托系统而不是URLSession的新型块API,我会有一个辅助对象来封装处理失败和成功情况所需的所有逻辑,这样我就不必依赖于一个繁重的VC来完成这些工作。

这篇答案完全是在我与MattS的交谈中撰写的。


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