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

У меня есть класс, который извлекает 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 для моего свойства делегата. Подробнее об этом позже...

Я пытаюсь написать класс 'wrapper', который реализует 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, чтобы избежать потенциальных циклов удержания. Глядя на инструменты, я нахожу, что ассигнования не продолжают расти с продолжающимися вызовами. Однако я считаю, что "лучшая практика" состоит в том, чтобы иметь делегатов weak.

Вопросы

Q1) всегда ли "хорошо" иметь делегатов strong

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

Ответ 1

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. (Или вы можете добавить его в существующий класс, используя категорию.)

НТН.

Ответ 2

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

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

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

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

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

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

Ответ 3

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

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

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

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

Ответ 4

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

Повторите попытку после сбоя

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

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

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

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

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

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

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


Этот ответ был полностью написан благодаря моей беседе с MattS