Почему этот блок Objective-C протекает? - PullRequest
3 голосов
/ 29 февраля 2012

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

MyVideoPlayer *videoPlayer = [[[MyVideoPlayer alloc] init] autorelease];
[videoPlayer canPlayAsset:(MyVideoAsset *)asset completionBlock:^(BOOL isAssetPlayable) {
    if (isAssetPlayable) {
        [videoPlayer playAsset:asset];
        [self presentModalViewController:videoPlayer animated:YES];
    }
}];

Этот метод может либо мгновенно возвращаться, либо может потребоваться некоторый пользовательвход и некоторые сетевые вызовы, следовательно, блок, который фактически представляет игрока.Я заметил какое-то странное поведение и обнаружил, что видео плеер проигрывал.Вот что я думал:

  1. videoPlayer автоматически освобожден.
  2. videoPlayer сохраняется блоком.
  3. Блок выполняется, илибо представляет videoPlayer, либо нет.
  4. Блок освобождается и освобождает videoPlayer.
  5. . videoPlayer освобождается (немедленно, если не было)t или когда модальное представление отклонено).

Вместо этого происходит следующее:

  1. videoPlayer автоматически выпускается.
  2. videoPlayer сохраняется блоком.
  3. Блок выполняется и либо представляет videoPlayer, либо нет.
  4. ????

Теперь я заметил, что смог получить ожидаемое поведение, если бы изменил блок следующим образом:

MyVideoPlayer *videoPlayer = [[[MyVideoPlayer alloc] init] autorelease];
[videoPlayer canPlayAsset:(MyVideoAsset *)asset completionBlock:^(BOOL isAssetPlayable) {
    if (isAssetPlayable) {
        [videoPlayer playAsset:asset];
        [self presentModalViewController:videoPlayer animated:YES];
    }
    [videoPlayer autorelease];
}];

Но я действительно не хочу добавлять это, не зная, что я делаю.

Насколько я понимаю, это не цикл сохранения, поскольку videoPlayer не сохраняет и не копирует блок.Мое понимание того, что блок будет освобожден, когда он больше не будет иметь неправильный объем?Может ли кто-нибудь помочь мне понять правильный способ сделать это?

ОБНОВЛЕНИЕ

Просто еще немного информации, реализация MyVideoPlayer canPlayAsset:completionBlock: (детали удалены)чтобы защитить невинных) выглядит примерно так:

- (void)canPlayAsset:(MyVideoAsset *)asset completionBlock:(void(^)(BOOL isAssetPlayable))completion {
    if (!asset) {
        completion(NO);
        return;
    }
    // user is a singleton object
    if (user.isGuest) {
        if ([user allowedRating:asset.rating]) {
            completion(YES);
        } else {
            // show alert
            completion(NO);
        }
    } else {
        if ([user allowedRating:asset.rating]) {
            completion(YES);
        } else {
            // prompt for pin
        }
    }
}

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

ОБНОВЛЕНИЕ 2

ОК, я отследил ее.Проблема не была с этим блоком вообще.В видеоплеере просочилась сетевая просьба, в которой был блок со ссылкой на себя.

Это часть мультфильма, в которой мы говорим: «Сегодня я получил ценный урок ...»

1 Ответ

5 голосов
/ 29 февраля 2012

Просто измените первую строку на:

__block MyVideoPlayer *videoPlayer = [[[MyVideoPlayer alloc] init] autorelease];

Причина, по которой это работает, заключается в следующем:

В среде с подсчетом ссылок по умолчанию, когда вы ссылаетесь на Objective-Объект C внутри блока, он сохраняется.Это верно, даже если вы просто ссылаетесь на переменную экземпляра объекта.Переменные объекта помечены блок __ модификатор типа хранилища, однако не сохраняются .

См. Документацию Apple по Переменные объектов и блоков

Но почему он не был выпущен для начала?

Простое объяснение состоит в том, что после выполнения completionBlock он не освобождается.Он будет выпущен позже в dealloc, поэтому все переменные, которые он сохраняет, все еще сохраняютсяЭто имеет смысл, поскольку можно выполнить блок столько раз, сколько вы пожелаете, пока он не будет освобожден.

Я видел такое поведение раньше, когда на объект, который владеет блоком, ссылаются в блоке и освобождает блок в dealloc, что предотвращает освобождение объекта. Решение состоит в том, чтобы слабо ссылаться на объект, которому принадлежит блок. Таким образом, тип владения, такой как MyVideoPlayer, достигает dealloc, что впоследствии освобождает блок (completionBlock в этом примере).

Альтернативой использованию ключевого слова __block во избежание сохранения типа является завершение объекта в NSValue с использованием valueWithNonretainedObject: и nonretainedObjectValue методы.Например:

MyVideoPlayer *videoPlayer = [[[MyVideoPlayer alloc] init] autorelease];
NSValue *videoPlayerRef = [NSValue valueWithNonretainedObject:videoPlayer];
[videoPlayer canPlayAsset:(MyVideoAsset *)asset 
          completionBlock:^(BOOL isAssetPlayable) {
              if (isAssetPlayable) {
                  MyVideoPlayer *tempVideoPlayer = 
                      (MyVideoPlayer*)[videoPlayerRef nonretainedObjectValue];
                  [tempVideoPlayer playAsset:asset];
                  [self presentModalViewController:tempVideoPlayer animated:YES];
              }
}];

Редактировать / Разговор

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

Заголовочный файл ...

typedef void(^MyVideoPlayerCompletionBlock)(BOOL isAssetPlayable);

@interface MyVideoPlayer : NSObject
@property(nonatomic, copy) MyVideoPlayerCompletionBlock completion;
... // other property definitions (or ivars)    

-(void)canPlayAsset:(MyVideoAsset*)asset 
    completionBlock:(MyVideoPlayerCompletionBlock)completion;
... // other methods defined
@end

Реализация ...

@implementation MyVideoPlayer

@synthesize completion = _completion;

-(void)dealloc {
    // Note: Block is released in dealloc like any other property or variable
    [_completion release], _completion = nil;
    ...  // other variables and properties not shown here are released
    [super dealloc];
}

-(void)canPlayAsset:(MyVideoAsset*)asset 
    completionBlock:(MyVideoPlayerCompletionBlock)completion {
    ... // Does something with asset.  Plays it, stores it, whatever

    // Saves completion block to call at a later time.
    // Note that this code could alternatively look like
    // _completion = [completion copy];
    // Blocks are usually copied and not retained
    self.completion = completion;
}

... В НЕКОТОРЫХ ТОЧКАХ ЗАВЕРШЕНИЯ БЛОКА ЗАВЕРШЕНИЯ ... (возможно, в обработчике событий или в каком-то другом виде)

-(void)SomeEventHandlerOrFuncThatCallsCompletionInMyVideoPlayer {
    // Time to call completion!
    if (self.completion) {
        self.completion(YES);  // OR no, doesn't matter
        //
        // WHOA! :: self.completion is not released
        //
        //     i.e. self.completion is not nil, and all
        //          variables inside it are still retained
        //          because calling a block doesn't also
        //          release the block.  To do that you would
        //          need to do:     self.completion = nil;
        //          AFTER calling:  self.completion(...);
        //
        //    SO...
        //
        //          Because the block was not released, it is
        //          still retaining variables (such as the current
        //          instance of MyViewPlayer).  Note that the
        //          Block will never be released until dealloc
        //          is called.  :(  So..., if you want the block
        //          to retain the current MyViewPlayer until
        //          execution of the completion block is over, you
        //          will want to call:  self.completion = nil;
        //          to release the block and all associated variables
        //          after calling the completion block.
    }
}

@end
...