Isso é um bug no MonoTouch GC?

Nota: Eu criei um projeto simples – você pode ver como os tipos de comutação entre UIButton e CustomButton no storyboard alteram o comportamento do GC.

Estou tentando enrolar minha cabeça no coletor de lixo MonoTouch.
O problema é semelhante ao fixado no MT 4.0 , porém com tipos herdados.

Para ilustrar isso, considere dois controladores de visualização, pai e filho.

A visão de criança contém um único UIButton que grava para console na torneira.
O método Dispose do controlador lança uma exceção, por isso é difícil errar.

Aqui vai controlador de visão infantil:

 public override void ViewDidLoad () { base.ViewDidLoad (); sayHiButton.TouchUpInside += (sender, e) => SayHi(); } } void SayHi() { Console.WriteLine("Hi"); } protected override void Dispose (bool disposing) { throw new Exception("Hey! I've just been collected."); base.Dispose (disposing); } 

O controlador de exibição pai apenas apresenta o controlador filho e configura um timer para descartá-lo e executar o GC:

 public override void ViewDidLoad () { base.ViewDidLoad (); var child = (ChildViewController)Storyboard.InstantiateViewController("ChildViewController"); NSTimer.CreateScheduledTimer(2, () => { DismissViewController(false, null); GC.Collect(); }); PresentViewController(child, false, null); } 

Se você executar este código, ele previsivelmente trava dentro de ChildViewController.Dispose() chamado de seu finalizador porque o controlador filho foi coletado como lixo. Legal.

Agora abra o storyboard e mude o tipo de botão para CustomButton . O MonoDevelop irá gerar uma subclass UIButton simples:

 [Register ("CustomButton")] public partial class CustomButton : UIButton { public CoolButton (IntPtr handle) : base (handle) { } void ReleaseDesignerOutlets() { } } 

De alguma forma, alterar o tipo de botão para CustomButton é suficiente para enganar o coletor de lixo, fazendo com que o controlador filho ainda não seja elegível para coleta.

Como isso é assim?

Este é um efeito colateral infeliz do MonoTouch (que é lixo coletado) ter que viver em um mundo de referência (ObjectiveC).

Existem algumas informações necessárias para entender o que está acontecendo:

  • Para cada object gerenciado (derivado de NSObject), existe um object nativo correspondente.
  • Para classs gerenciadas personalizadas (derivadas de classs de estrutura, como UIButton ou UIView), o object gerenciado deve permanecer ativo até que o object nativo seja liberado [1]. A maneira como isso funciona é que, quando um object nativo tem uma contagem de referência de 1, não impedimos que a instância gerenciada receba lixo coletado. Assim que a contagem de referências aumenta acima de 1, impedimos que a instância gerenciada receba lixo coletado.

O que acontece no seu caso é um ciclo, que atravessa a ponte MonoTouch / ObjectiveC e, devido às regras acima, o GC não pode determinar que o ciclo possa ser coletado.

Isto é o que acontece:

  • Seu ChildViewController tem um sayHiButton. O ChildViewController nativo manterá esse botão, portanto, sua contagem de referência será 2 (uma referência mantida pela instância CustomButton gerenciada + uma referência mantida pelo ChildViewController nativo).
  • O manipulador de events TouchUpInside possui uma referência à ocorrência de ChildViewController.

Agora você vê que a instância CustomButton não será liberada, porque sua contagem de referência é 2. E a ocorrência ChildViewController não será liberada porque o manipulador de events do CustomButton tem uma referência a ela.

Existem algumas maneiras de quebrar o ciclo para corrigir isso:

  • Desanexe o manipulador de events quando não precisar mais dele.
  • Descarte o ChildViewController quando não precisar mais dele.

[1] Isso ocorre porque um object gerenciado pode conter o estado do usuário. Para objects gerenciados que estão espelhando um object nativo correspondente (como a instância gerenciada do UIView), o MonoTouch sabe que a instância não pode conter nenhum estado, portanto, assim que nenhum código gerenciado tiver uma referência à instância gerenciada, o GC poderá coletá-lo. Se uma instância gerenciada for necessária em um estágio posterior, basta criar uma nova.