Padrão de vazamento do Android: assinaturas em visualizações

Tempo de leitura: 3 minutes

No Square Register Android, contamos com visualizações personalizadas para estruturar nosso aplicativo. Às vezes, uma visão escuta as mudanças de um objeto que dura mais tempo do que essa visão.

Por exemplo, um HeaderView pode querer ouvir as alterações de nome de usuário provenientes de um singleton do Authenticator:

public class HeaderView extends FrameLayout {
  private final Authenticator authenticator;

  public HeaderView(Context context, AttributeSet attrs) {...}

  @Override protected void onFinishInflate() {
    final TextView usernameView = (TextView) findViewById(R.id.username);
    authenticator.username().subscribe(new Action1<String>() {
      @Override public void call(String username) {
        usernameView.setText(username);
      }
    });
  }
}

onFinishInflate () é um bom lugar para uma visualização personalizada inflada encontrar suas visualizações filhas, então fazemos isso e então assinamos as alterações de nome de usuário.

O código acima tem um grande bug: Nós nunca cancelamos a assinatura. Quando a visualização vai embora, o Action1 permanece inscrito. Como a Action1 é uma classe anônima, ela mantém uma referência à classe externa, HeaderView. Toda a hierarquia de visualização agora está vazando e não pode ser coletada como lixo.

Para corrigir esse bug, vamos cancelar a assinatura quando a visualização for separada da janela:

public class HeaderView extends FrameLayout {
  private final Authenticator authenticator;
  private Subscription usernameSubscription;

  public HeaderView(Context context, AttributeSet attrs) {...}

  @Override protected void onFinishInflate() {
    final TextView usernameView = (TextView) findViewById(R.id.username);
    usernameSubscription = authenticator.username().subscribe(new Action1<String>() {
      @Override public void call(String username) {...}
    });
  }

   @Override protected void onDetachedFromWindow() {
    super.onDetachedFromWindow();
    usernameSubscription.unsubscribe();
  }
}

Problema resolvido? Não exatamente. Recentemente, eu estava olhando um relatório LeakCanary, que foi causado por um código muito semelhante:

Vejamos o código novamente:

public class HeaderView extends FrameLayout {
  private final Authenticator authenticator;
  private Subscription usernameSubscription;

  public HeaderView(Context context, AttributeSet attrs) {...}

  @Override protected void onFinishInflate() {...}

   @Override protected void onDetachedFromWindow() {
    super.onDetachedFromWindow();
    usernameSubscription.unsubscribe();
  }
}

De alguma forma, View.onDetachedFromWindow () não estava sendo chamado, o que criou o vazamento.

Durante a depuração, percebi que View.onAttachedToWindow () também não foi chamado. Se uma visualização nunca for anexada, obviamente ela não será removida. Portanto, View.onFinishInflate () é chamado, mas não View.onAttachedToWindow ().

Vamos aprender mais sobre View.onAttachedToWindow ():

  • Quando uma visualização é adicionada a uma visualização pai com uma janela, onAttachedToWindow () é chamado imediatamente de addView ().
  • Quando uma visualização é adicionada a uma visualização pai sem janela, onAttachedToWindow () será chamado quando esse pai for anexado a uma janela.

Estamos aumentando a hierarquia de visualizações da maneira típica do Android:

public class MyActivity {
  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.my_activity);
  }
}

Nesse ponto, cada exibição na hierarquia de exibição recebeu o retorno de chamada View.onFinishInflate(), mas não o retorno de chamada View.onAttachedToWindow(). Aqui está o porquê:

“View.onAttachedToWindow() é chamado na primeira travessia de visualização, algum tempo depois de Activity.onStart()”

ViewRootImpl é onde a chamada onAttachedToWindow() é enviada:

public class ViewRootImpl {
  private void performTraversals() {
    // ...
    if (mFirst) {
      host.dispatchAttachedToWindow(mAttachInfo, 0);
    }
    // ...
  }
}

Legal, então não nos apegamos a onCreate (), e depois de onStart ()? Isso não é sempre chamado após onCreate ()?

Nem sempre! O javadoc Activity.onCreate () nos dá a resposta:

Você pode chamar finish () de dentro desta função, caso em que onDestroy() será imediatamente chamado sem qualquer parte do resto do ciclo de vida da atividade (onStart(), onResume(), onPause(), etc) em execução.

Eureka!

Estávamos validando o intent de atividade em onCreate () e chamando imediatamente o finish () com um resultado de erro se o conteúdo desse intent fosse inválido:

public class MyActivity {
  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.my_activity);
    if (!intentValid(getIntent()) {
      setResult(Activity.RESULT_CANCELED, null);
      finish();
    }
  }
}

A hierarquia de visualização foi inflada, mas nunca anexada à janela e, portanto, nunca desanexada.

Esta é uma versão atualizada do bom e velho diagrama de ciclo de vida de atividades:

Com esse conhecimento, agora podemos mover o código de assinatura para onAttachedToWindow ():

public class HeaderView extends FrameLayout {
  private final Authenticator authenticator;
  private Subscription usernameSubscription;

  public HeaderView(Context context, AttributeSet attrs) {...}

  @Override protected void onAttachedToWindow() {
    final TextView usernameView = (TextView) findViewById(R.id.username);
    usernameSubscription = authenticator.username().subscribe(new Action1<String>() {
      @Override public void call(String username) {...}
    });
  }

   @Override protected void onDetachedFromWindow() {
    super.onDetachedFromWindow();
    usernameSubscription.unsubscribe();
  }
}

Isso é para melhor de qualquer maneira: a simetria é boa e, ao contrário da implementação original, podemos adicionar e remover essa visualização quantas vezes forem necessárias.