Хорошо ли когда-нибудь иметь "сильную" ссылку на делегата?

У меня есть класс, который получает JSON из URL-адреса и возвращает данные через шаблон протокола / делегата.

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

Я пришел в объективный мир сравнительно недавно (жил только во времена ARC и все еще смирялся с блоками), и, по общему признанию, мое понимание управления памятью находится на более тонкой стороне вещей.

Этот код работает нормально, но только если мой делегат strong, Я понимаю, что мой делегат должен быть weak чтобы избежать потенциальных удерживающих циклов. Глядя на инструменты, я обнаружил, что распределение не продолжает расти с продолжающимися звонками. Тем не менее, я считаю, что "лучшая практика" weak делегаты.

Вопросы

Q1) это когда-либо 'хорошо' иметь strong делегаты

Q2) как я могу реализовать основанную на блоке оболочку, оставляя делегат базового класса как weak делегат (т. е. предотвратить освобождение * DelegateBlock до того, как он получит методы протокола)?

3 ответа

Решение

Q1 - Да. Как вы указали, наличие слабых свойств делегатов - это рекомендация, позволяющая избежать сохранения циклов. Таким образом, нет ничего плохого в том, чтобы иметь сильного делегата, но если клиенты вашего класса ожидают, что он будет слабым, вы можете удивить их. Лучший подход состоит в том, чтобы сохранить слабый делегат, а на стороне сервера (класс со свойством делегата) внутренне сохранять сильную ссылку для тех периодов, в которых он нужен. Как @Scott указывает на документы Apple, делающие это для NSURLConnection, Конечно, такой подход не решает вашу проблему - где вы хотите, чтобы сервер сохранил делегат для вас...

Q2 - С точки зрения клиента проблема заключается в том, как сохранить делегата живым, пока этого требует сервер со слабой ссылкой на него. Существует стандартное решение этой проблемы, которое называется связанные объекты. Вкратце, среда выполнения 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 сам. (Или вы можете добавить его в существующий класс, используя категорию.)

НТН.

Это полностью зависит от архитектуры ваших объектов.

Когда люди используют слабые делегаты, это потому, что делегат, как правило, является своего рода "родительским" объектом, который сохраняет то, что имеет делегат (давайте назовем "делегат"). Почему это должен быть родительский объект? Это не должно быть; тем не менее, в большинстве случаев это самый удобный способ. Поскольку делегат является родительским объектом, который сохраняет делегат, делегат не может сохранить делегат, или у него будет цикл сохранения, поэтому он содержит слабую ссылку на делегат.

Однако это не единственная ситуация использования. Взять, к примеру, UIAlertView а также UIActionSheet в iOS Обычный способ их использования: внутри функции создайте представление оповещения с сообщением и добавьте к нему кнопки, установите его делегат, выполните любую другую настройку, вызовите -show на нем, а затем забыть его (он нигде не хранится). Это своего рода механизм "огонь и забыть". Однажды ты show это, вам не нужно сохранять его или что-то еще, и он все равно будет отображаться на экране. В некоторых случаях возможно, что вы захотите сохранить представление предупреждений, чтобы вы могли программно отключить его, но это редко; в подавляющем большинстве случаев вы просто показываете и забываете об этом и просто обрабатываете любые вызовы делегатов.

Таким образом, в этом случае правильным стилем будет сильный делегат, потому что 1) родительский объект не сохраняет представление оповещения, поэтому нет проблемы с циклом сохранения, и 2) делегат должен быть сохранен, так что когда какая-то кнопка нажата в окне оповещения, кто-то будет рядом, чтобы ответить на него. Теперь, часто, #2 не является проблемой, потому что делегат (родительский объект) является своего рода контроллером представления или чем-то, что иначе сохраняется чем-то другим. Но это не всегда так. Например, я могу просто иметь метод, который не является частью какого-либо контроллера представления, который любой может вызвать, чтобы показать представление с предупреждением, и, если пользователь нажимает Да, загружает что-то на сервер. Поскольку он не является частью какого-либо контроллера, он, вероятно, ничем не сохраняется. Но он должен оставаться достаточно долго до тех пор, пока не появится предупреждение. Поэтому в идеале представление о предупреждении должно иметь четкую ссылку на него.

Но, как я уже упоминал ранее, это не всегда то, что вы хотите для просмотра предупреждений; иногда вы хотите сохранить его и отклонить программно. В этом случае вам нужен слабый делегат, иначе это вызовет цикл сохранения. Так должен ли в представлении оповещения быть сильный или слабый делегат? Ну, звонящий должен решить! В некоторых ситуациях звонящий хочет сильного; в других звонящий хочет слабого. Но как это возможно? Делегат представления оповещения объявляется классом представления оповещения и должен быть объявлен как сильный или слабый.

К счастью, есть решение, позволяющее вызывающему абоненту решить - обратный вызов на основе блоков. В основанном на блоках API блок по существу становится делегатом; но блок не является родительским объектом. Обычно блок создается в вызывающем классе и захватывает self так что он может выполнять действия над "родительским объектом". Делегатор (в данном случае представление предупреждений) всегда имеет строгую ссылку на блок. Однако блок может иметь сильную или слабую ссылку на родительский объект, в зависимости от того, как блок записан в вызывающем коде (для захвата слабой ссылки на родительский объект не используйте self прямо в блоке, а вместо этого создайте слабую версию self вне блока, и пусть блок использует это вместо). Таким образом, вызывающий код полностью контролирует, имеет ли делегат сильную или слабую ссылку на него.

Вы правы в том, что на делегатов обычно ссылаются слабо. Тем не менее, есть случаи использования, когда предпочтение отдается сильной ссылке или даже необходимо. Apple использует это в NSURLConnection:

Во время загрузки соединение поддерживает сильную ссылку на делегата. Это освобождает эту сильную ссылку, когда соединение завершает загрузку, не удается или отменено.

NSURLConnection Экземпляр можно использовать только один раз. После того, как это заканчивается (или с ошибкой или с успехом), это освобождает делегата, и так как делегат readonly, он не может быть (безопасно) использован повторно.

Вы можете сделать что-то подобное. В вашем dataRetrieved а также dataFailed методы, установите ваш делегат в nil, Вам, вероятно, не нужно делать свой делегат readonly если вы хотите повторно использовать свой объект, но вам придется снова назначить свой делегат.

Как говорили другие, это об архитектуре. Но я покажу вам несколько примеров:

Повторить попытку при неудаче

Предположим, вы создали сеанс URLSession и ждете сетевого вызова, который вы сделали через viewController, иногда не имеет значения, потерпел ли он сбой, но в других случаях это так. например, ваше приложение отправляет сообщение другому пользователю, затем вы закрываете этот контроллер просмотра, и каким-то образом этот сетевой запрос не выполняется. Вы хотите повторить попытку? Если это так, то viewController должен оставаться в памяти, чтобы он мог повторно отправить запрос.

Запись на диск

Другой случай, когда запрос завершается успешно, вы можете захотеть записать что-то на диск, поэтому даже после обновления пользовательского интерфейса контроллера просмотра вы все равно можете захотеть синхронизировать свою локальную базу данных с сервером.

Большие фоновые задачи

Первоначальным вариантом использования NSURLSession было выполнение фоновых сетевых задач, загрузка больших файлов и тому подобное. Вам нужно что-то в памяти для обработки завершения этих задач, чтобы указать, что выполнение завершено, и ОС может приостановить приложение.

Связывание жизненного цикла загрузки больших файлов с определенным представлением - плохая идея... его нужно привязать к более стабильному / постоянному, например, самому сеансу...

Обычно, если я собираюсь использовать систему на основе делегатов, а не более новый блочный API URLSession, у меня есть вспомогательный объект, который инкапсулирует всю логику, необходимую для обработки случаев сбоя и успеха, которые могут мне потребоваться таким образом, у меня нет полагаться на тяжелые венчурные капиталисты, чтобы делать грязную работу


Этот ответ был полностью написан благодаря разговору с Matt S..