Сбой в objc_retain в методе, выполняемом с помощью performSelector

У меня есть эта странная ошибка, связанная с автоматической установкой ARC objc_retains в моем коде.

У меня есть следующие два класса:

@interface MenuItem : NSObject
@property (weak, nonatomic) id target;
@property (unsafe_unretained, nonatomic) SEL action;
@property (strong, nonatomic) id object;
- (instancetype)initWIthTarget:(id)target action:(SEL)action withObject:(id)object;
- (void)performAction;
@end

@implementation MenuItem 
- (void)performAction
{
    if (self.target && self.action)
    {
      if (self.object)
      {
        [self.target performSelector:self.action withObject:self.object];
      }
      else
      {
        [self.target performSelector:self.action];
      }
    }
}
@end

@interface Widget : NSObject
- (void)someMethod:(id)sender;
@end

В какой-то момент я создаю экземпляр MenuItem как таковой:

MenuItem *item = [MenuItem alloc] initWithTarget:widget action:@selector(someMethod:) object:nil];

Затем в другом месте я вызываю performAction в пункте меню:

 [item performAction];

В реализации someMethod я получаю сбой:

@implementation Widget
- (void)someMethod:(id)sender
{
  // EXEC_BAD_ACCESS crash in objc_retain
}
@end

Почему это происходит?

Ответ 1

Причиной аварии было то, что я использовал неправильный performSelector.

NSObject определяет несколько версий performSelector. Тот, который я вызывал, был:

- (id)performSelector:(SEL)aSelector;

Однако метод, который я вызывал, принял параметр id. Например:

- (void)someMethod:(id)sender;

Теперь ARC - хорошая безопасная система управления памятью, которая пытается обеспечить правильное сохранение параметров во время выполнения метода. Так что хотя мой someMethod: был пуст, ARC создавал код, который выглядел так:

- (void)someMethod:(id)sender 
{
    objc_retain(sender);
    objc_release(sender);
}

Проблема с этим, однако, заключалась в том, что я вызывал performSelector: и не поставлял значение для параметра sender. Таким образом, sender указывал случайный мусор на стек. Поэтому при вызове objc_retain() приложение разбилось.

Если я изменил:

MenuItem *item = [[MenuItem alloc] initWithTarget:widget 
                                          action:@selector(someMethod:) 
                                          object:nil];

к

MenuItem *item = [[MenuItem alloc] initWithTarget:widget 
                                          action:@selector(someMethod) 
                                          object:nil];

и

- (void)someMethod:(id)sender;

к

- (void)someMethod;

Затем крушение уходит.

Аналогично, я также могу изменить

[self.target performSelector:self.action];

to

[self.target performSelector:self.action withObject:nil];

если я хочу следовать "стандартной" форме методов целевого действия, которые принимают один параметр. Преимущество второй формы performSelector заключается в том, что если я вызываю метод, который не принимает параметр, он все равно будет работать нормально.