Site menu O Handler é seu amigo

O Handler é seu amigo

Este artigo é técnico, não é sobre política, mas também vai chutar um cachorro morto. Digo isto porque imagino que a grande maioria dos desenvolvedores Android já conhece a classe Handler, então este artigo está alguns anos atrasado. Mas, a quem ainda possa interessar, lá vai.

Uma experiência recorrente que tive com desenvolvedores Android é a tendência de usar Thread para muita coisa que não devia. Exemplo: quando uma tarefa precisa ser executada depois de um certo tempo, a tática utilizada é criar uma thread e colocá-la para dormir com Thread.sleep(). Outro exemplo: quando é preciso agendar uma tarefa para execução postergada.

Creio que seja cacoete de desenvolvedor Java experiente, porque a API Java "padrão" não tem muitos recursos assíncronos, mas o suporte a threads na própria linguagem é bom. Então usa-se threads para tudo que cheire a assincronia. Exemplo: o método amplamente aceito para implementar um servidor TCP/IP em Java é criar uma thread por conexão — uma heresia para quem tem background em UNIX, Node.js, Python/Twisted, etc.

Acredito que essa decisão de design do Java se deve ao fato da Sun fabricar processadores SPARC, muito mais otimizados para threads que os processadores Intel x86, por exemplo. Mas é uma conjectura pessoal. (Que me faz desconfiar de qualquer linguagem que tem um único mantenedor, pois este vai impor sua agenda.)

Só que depurar threads é um inferno, threads têm de ser evitadas sempre que possível, não interessa se a linguagem oferece suporte de primeira classe a elas.

Quem conhece Java, segue a trilha usual e acaba encontrando uma classe do Android chamada AsyncTask, que encapsula a conversação entre a thread secundária (da tarefa) e a thread da UI, ou "thread principal", que é a única que pode manipular a UI. Só que AsyncTask continua rodando a tarefa postergada numa thread separada, com todas as complicações que isto pode trazer, principalmente nos corner cases — por exemplo, quando o aplicativo está fechando.

Mas a API do Android também oferece a classe Handler, que resolve 95% dos problemas de execução postergada de tarefas, sem usar threads!

Curiosamente, encontrei esta classe porque minha experiência em Java tendia a zero antes de desenvolver para Android, e minha experiência pregressa gira em torno de UNIX, então por "instinto" busquei uma API que lidasse com assincronia sem envolver threads.

Handler para tarefas postergadas

Basta criar um Handler para agendar todas as tarefas postergadas da classe ou do aplicativo inteiro, e chamar post() ou postDelayed() conforme a necessidade. Estes métodos aceitam objetos Runnable, tal qual threads, o que até facilita a migração.

	// na criação da classse
	h = new Handler();

	// tarefa postergada por 100 milissegundos
	h.postDelayed(new Runnable() {
		public void run() {
			fazQualquerCoisa();
		}
	}, 100);

	// tarefa postergada para tão logo quanto possível
	h.post(new Runnable() {
		public void run() {
			fazQualquerCoisa();
		}
	});

Naturalmente, nos exemplos acima, fazQualquerCoisa() vai rodar no contexto da thread principal, então seu aplicativo vai bloquear até que a tarefa termine. Isto não é um problema para tarefas de curta duração, mas realmente não serve se a tarefa envolve processamento longo, ou comunicação de rede. Nestes casos AsyncTask seria a ferramenta correta.

Rodar a tarefa na thread principal tem a enorme vantagem da previsibilidade. Supondo que você queira executar a tarefa imeditamente após o término do método atual. Basta usar post(tarefa). Não há nenhuma chance da tarefa concorrer com o método em execução, porque este já está ocupando a thread principal. Como post(tarefa) vai entrar na fila de tarefas pendentes, é garantido que a tarefa já pode lidar com a UI (se agendada durante Activity.OnCreate()) porque a tarefa e a construção da UI usam a mesma thread e o segundo item tem prioridade.

A classe Handler faz muita coisa e tem inúmeros métodos; vale a pena ler a documentação; muitos problemas de assincronia encontram solução ali. Handler também implementa filas de mensagens, o que facilita a comunicação assíncrona entre threads ou mesmo entre partes desacopladas do seu aplicativo. (Este recurso em particular eu nunca usei, porque não precisei ainda, mas está lá.)

Handler para conectar threads

A comunicação entre threads diferentes é o grande problema recorrente de usar threads. Para muitos efeitos práticos, devemos pensar a thread como um processo separado, e para dois processos conversarem é preciso usar um canal de dados — compartilhar diretamente objetos é procurar encrenca. Também neste caso, Handler presta um grande serviço.

Bem, quando eu usaria uma Thread em vez de um AsyncTask? Num caso de uso recente, a thread roda continuamente, durante toda a vida do aplicativo, então o código executado pela thread não pode realmente ser encapsulado como uma tarefa, porque ele não tem duração determinada. Além do mais, a comunicação entre thread principal e secundária é bidirecional e constante, enquanto AsyncTask facilita a comunicação apenas em uma direção.

A classe que será executada na thread principal, denominada Principal, que inclusive cria a thread secundária, eu implemento mais ou menos assim:

public class Principal {
	Handler h;

	public Principal() {
		h = new Handler();
		t = new Secundaria();
		t.start();
	}

	/* pode ser invocado por qualquer thread */
	public void g_metodo1() {
		h.post(new Runnable() {
			public void run() {
				metodo1();
			}
		}
	}

	/* garantidamente roda no contexto da thread principal */
	private void metodo1() {
		...
	}
}

O método público g_metodo1() existe sob medida para que a thread Secundaria possa invocar um metodo da classe Principal de forma segura, sem precisar se preocupar com a sincronização. Como a execução de metodo1() só pode acontecer mediante agendamento no Handler, e o objeto h foi criado no contexto da thread principal, é garantido que metodo1() executa na thread principal.

Já a classe Secundaria, fica assim:

public class Secundaria extends Thread {
	Handler h;

	public Secundaria() {
		...
	}

	@Override
	public void run() {
		Looper.prepare();
		h = new Handler();
		Looper.loop();
	}

	/* pode ser invocado por qualquer thread */
	public void g_metodo2() {
		h.post(new Runnable() {
			public void run() {
				metodo2();
			}
		}
	}

	/* garantidamente roda no contexto da thread secundaria */
	private void metodo2() {
		...
	}

	/* deve haver uma forma de 'matar' a thread */
	public void parar() {
		h.post(new Runnable() {
			public void run() {
				Looper.myLooper().quit();
			}
		}
	}
}

A classe Secundaria tem algumas semelhanças com a anterior. A forma de garantir a execução de metodo2() no contexto correto, que é a thread secundária, é o mesmo: agendando a execução através de um Handler. Os clientes da classe devem invocar o método público g_metodo2(), que faz esse agendamento.

Desta forma, as duas classes podem estabelecer comunicação bidirecional, uma invocando os métodos públicos da outra. A única deficiência desta técnica é não poder retornar dados, porque a execução do método real (privado) é sempre postergada. Para o chamador obter um retorno qualquer, seria necessário usar um callback.

O grande detalhe de implementação de Secundaria é o método run(), onde a classe Looper é utilizada para criar uma fila de eventos para a thread secundária. O Handler depende desta fila para funcionar. Note que run() não faz muita coisa por si só; neste exemplo a thread secundária só faz algo útil quando metodo2() é invocado, fora isso fica dormindo.

Outra forma de estabelecer comunicação entre thread principal e secundária seria a troca de mensagens, em vez de invocação de métodos.

Finalização da thread

Um problema bem típico de threads é a finalização. Uma thread sempre deve tomar a iniciativa de terminar por si mesma. Não existe forma segura de "matar" uma thread a partir de outra, e a coleta de lixo do Java não "mata" threads abandonadas.

No próprio exemplo acima, a classe Principal contém um objeto da classe Secundaria, mas que não será destruído quando o objeto Principal tornar-se dispensável. Num caso mais típico, se uma Activity cria uma thread e é destruída em seguida, essa thread continua rodando, e conforme o usuário abre e fecha o aplicativo várias vezes, as threads vão se acumulando.

Como um embrião de solução para este problema, temos o método Secundaria.parar(), que encerra a fila de eventos. Isto permite que o método run() retorne, o que sinaliza o fim da thread. Obviamente, parar() tem de ser invocado num lugar oportuno, como por exemplo em Activity.OnDestroy(). É uma coleta de lixo explícita.

É o tipo de problema que você só enfrenta se usa threads, então reitero que elas devem ser evitadas tanto quanto possível.