为什么我们需要为 Objective-C 对象字段做保留?

Why we need to do retain for Objective-C object field?

这是 Delphi 媒体播放器的代码:

type
  TAVMedia = class(TMedia)
  private
    FPlayer: AVPlayer;
    FPlayerItem: AVPlayerItem;
  public
    constructor Create(const AFileName: string); override;
    destructor Destroy; override;
  end;

constructor TAVMedia.Create(const AFileName: string);
var aURL: NSUrl;
begin
  inherited Create(AFileName);
  FPlayerItem := TAVPlayerItem.Wrap(TAVPlayerItem.OCClass.playerItemWithURL(URL));
  FPlayerItem.retain;
  FPlayer := TAVPlayer.Wrap(TAVPlayer.OCClass.playerWithPlayerItem(FPlayerItem));
  FPlayer.retain;
end;

destructor TAVMedia.Destroy;
begin
  FPlayer.release;
  FPlayer := nil;
  FPlayerItem.release;
  FPlayerItem := nil;
  inherited Destroy;
end;

我不是很明白他们为什么要做FPlayerItem.retainFPlayer.retainFPlayerItemFPlayer 是对象字段而不是局部变量,所以总是有对它们的强引用。那么这里的retain有什么用呢?

似乎执行 FPlayer.release; 也会释放 FPlayerItem,因此稍后调用 FPlayerItem.release; 时有时会触发访问冲突(奇怪的是并不总是)。

注意:我仍然不明白为什么我有一个 eaccessviolation 所以我决定把我所做的完整代码放在这里:

type
  TMyMedia = class(TObject)
  private
    FPlayer: AVPlayer;
    FPlayerItem: AVPlayerItem;
  public
    constructor Create;
    destructor Destroy; override;
  end;

constructor TMyMedia.Create;
begin
  inherited Create;

  P := TNSUrl.OCClass.URLWithString(StrToNSStr(aDataSource)); // Creates and returns an NSURL object initialized with a provided URL string
  if P = nil then raise EFileNotFoundException.Create(SFileNotFound); // If the URL string was malformed or nil, returns nil.
  aURL := TNSUrl.Wrap(P);
  try

    FPlayerItem := TAVPlayerItem.Wrap(TAVPlayerItem.OCClass.playerItemWithURL(URL));
    FPlayerItem.retain;

  finally
    aURL.release; // << if i don't do this then i will not have any exception at the end ??? 
    aURL := nil;  // <<
  end;


  FPlayer := TAVPlayer.Wrap(TAVPlayer.OCClass.playerWithPlayerItem(FPlayerItem));
  FPlayer.retain;
end;

destructor TAVMedia.Destroy;
begin

  ALLog('FPlayer.retainCount', inttostr(FPlayer.retainCount)); // => show 1
  ALLog('FPlayerItem.retainCount', inttostr(FPlayerItem.retainCount)); // => show 6

  FPlayer.release;
  FPlayer := nil;

  ALLog('FPlayerItem.retainCount', inttostr(FPlayerItem.retainCount)); // => show 1
  FPlayerItem.release; => here i receive Access violation at address 2156565 accessing address 68684458

  FPlayerItem := nil;
  inherited Destroy;
end;

FPlayerFPlayerItem 是 Delphi 围绕 Objective-C 原始对象指针的对象包装器。

虽然 Delphi for iOS 和基础 iOS 框架都使用引用计数来管理对象实例的生命周期,但所有相似之处都到此为止。这是两个独立的引用计数机制。

虽然保持对 FPlayerFPlayerItem 的强引用可确保 Delphi 包装器实例的生命周期,但调用 retain 会增加对包装的 Objective-C 对象的引用计数实例并在包装器本身的生命周期内使该对象实例保持活动状态。

不调用 retain 包装的对象可能会被 OS 释放,而 Delphi 包装器仍在使用它。

当然,要减少包装对象的引用计数,有必要在包装器被销毁时使用匹配的 release 调用。


至于为什么在FPlayerItem.release;期间会出现异常,很难说。这可能是线程问题、FMX 部分中的错误,甚至是底层 OS 框架。

就包装的 Objective-C 实例而言,它们在需要的地方保留自己的强引用,因此释放顺序对它们而言并不重要,而且 OS 也不太可能是罪魁祸首(我不能肯定地说)。

但是如果我必须编写上面的析构函数代码,我会使用以下模式来避免 Delphi 方面的问题。

destructor TAVMedia.Destroy;
var
  tmpPlayer: AVPlayer;
  tmpPlayerItem: AVPlayerItem;
begin
  tmpPlayer := FPlayer;
  tmpPlayerItem := FPlayerItem;
  FPlayer := nil;
  FPlayerItem := nil;
  tmpPlayer.release;
  tmpPlayerItem.release;
  inherited Destroy;
end;

TNSUrl.OCClass.URLWithStringTAVPlayerItem.OCClass.playerItemWithURL 将项目添加到自动释放池。因此,您的保留计数为 1。当自动释放池释放它包含的项目时,它们将被释放,这通常发生在当前事件完成执行之后。

因此需要FPlayerItem.retain,因为函数退出后FPlayerItem不应该被释放。它已分配给 FPlayerItem,因此您希望它保持活动状态。

一般来说,如果你用CreatealloccopymutableCopynew...创建这样一个class然后保留被要求为你。然后你需要调用 releaseautorelease.

如果您使用 fileUrlWithPath 等其他函数创建这样的 class,那么它会被添加到自动释放池中。您的保留计数仍然为 1,但它会为您释放。如果你也释放它,那么你会崩溃。

如果您在 class 上调用 retain,则该调用必须与 release 平衡。