EventBus es una biblioteca diseñada para facilitar la comunicación entre componentes en aplicaciones Android y Java, implementando el patrón publicador-suscriptor. Actúa como un centro de eventos centralizado que permite a las distintas partes de una aplicación intercambiar mensajes sin necesidad de tener un conocimiento explícito o una dependencia directa entre sí.
Características Principales
- Desacoplamiento Total: Los componentes que envían eventos (publicadores) y los que los reciben (suscriptores) no necesitan conocerse mutuamente, lo que simplifica la arquitectura y el mantenimiento del código.
- Gestión de Hilos: Ofrece dievrsas estrategias para la entrega de eventos, permitiendo el cambio de hilos de ejecución de forma sencilla (por ejemplo, del hilo de fondo al hilo principal de Android).
- Prioridad y Eventos Persistentes: Soporta la definición de prioridades para los suscriptores y la capacidad de manejar eventos "pegajosos" o persistentes.
- Rendimiento Optimizado: Es una solución rápida, ligera y con una base de código relativamente sencillla.
La Anotación @Subscribe
Para que un método sea reconocido como un suscriptor de eventos en EventBus, debe estar anotado con @Subscribe. Este método público declarará un único parámetro que representará el tipo de evento al que desea responder. A continuación, se muestra la declaración de la anotación:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Subscribe {
ThreadMode threadMode() default ThreadMode.POSTING;
boolean sticky() default false;
int priority() default 0;
}
Los campos de esta anotación definen cómo se debe gestionar la suscripción:
threadMode(Modo de Hilo): Determina la estrategia de ejecución del método suscriptor en relación con el hilo donde se publicó el evento.sticky(Persistente): Indica si el suscriptor debe recibir eventos de ese tipo que hayan sido publicados antes de su registro.priority(Prioridad): Establece el orden en que los suscriptores reciben un mismo evento, donde un valor más alto significa una mayor prioridad.
La enumeración ThreadMode ofrece las siguientes opciones para la gestión de hilos:
| Modo | Descripción |
|---|---|
POSTING |
El método suscriptor se ejecuta en el mismo hilo en el que se publicó el evento, sin incurrir en costos de cambio de hilo. |
MAIN |
El método se invoca en el hilo principle de Android. Si el evento se publica en este hilo, la llamada es directa; de lo contrario, se encola para su procesamiento en el hilo principal de forma ordenada. |
MAIN_ORDERED |
Similar a MAIN, pero el evento siempre se encola para ser procesado secuencialmente en el hilo principal, garantizando un orden estricto. |
BACKGROUND |
El método se ejecuta en un hilo de fondo. Si el evento no se publica en el hilo principal, se invoca directamente. Si se publica en el hilo principal, se encola y se procesa en un hilo de fondo. |
ASYNC |
El método suscriptor se ejecuta en un hilo independiente, asegurando que el hilo de publicación y el hilo de ejecución del suscriptor estén completamente desacoplados. |
Registro de Suscriptores
Para que un objeto comience a recibir eventos, debe registrarse con EventBus. La forma más común es utilizando la instancia por defecto:
MiEventBus.obtenerInstancia().registrar(this);
Internamente, el método registrar(Object componente) realiza dos pasos fundamentales: primero, identifica todos los métodos del componente anotados con @Subscribe; segundo, establece la relación entre el componente y estos métodos para futuras entregas de eventos.
public void registrar(Object componente) {
Class<?> claseComponente = componente.getClass();
// Paso 1: Descubrir métodos de suscripción en el componente
List<MetodoSuscriptor> metodosEncontrados = gestorMetodos.buscarMetodosSuscriptor(claseComponente);
// Paso 2: Asociar el componente con sus métodos de suscripción
synchronized (this) {
for (MetodoSuscriptor metodo : metodosEncontrados) {
establecerSuscripcion(componente, metodo);
}
}
}
El proceso de buscarMetodosSuscriptor prioriza la eficiencia buscando primero en una caché interna. Si los métodos no se encuentran en la caché, EventBus puede usar dos enfoques: reflexión en tiempo de ejecución o un índice pregenerado (generalmente mediante procesamiento de anotaciones, APT) para obtener la información de los métodos. Este último es más eficiente ya que evita la sobrecarga de la reflexión.
List<MetodoSuscriptor> buscarMetodosSuscriptor(Class<?> claseObjetivo) {
List<MetodoSuscriptor> metodosCacheados = CACHE_METODOS_SUSCRIPTOR.get(claseObjetivo);
if (metodosCacheados != null) {
return metodosCacheados; // Retornar desde caché
}
List<MetodoSuscriptor> listaMetodos;
if (usarIndiceGenerado) { // Si se usa APT (Annotation Processing Tool)
listaMetodos = buscarMedianteIndice(claseObjetivo);
} else { // Si no, se usa reflexión
listaMetodos = buscarMedianteReflexion(claseObjetivo);
}
if (listaMetodos.isEmpty()) {
throw new EventBusException("No se encontraron métodos @Subscribe en la clase " + claseObjetivo.getName());
} else {
CACHE_METODOS_SUSCRIPTOR.put(claseObjetivo, listaMetodos);
return listaMetodos;
}
}
EventBus también considera la herencia de clases al buscar métodos. Una estructura interna, a menudo llamada EstadoBusqueda (FindState), ayuda a gestionar el estado durante la búsqueda, asegurando que los métodos de las clases padre se incluyan y que no haya duplicados si un método se sobrescribe.
Una vez descubiertos los métodos, el siguiente paso es establecerSuscripcion(componente, metodo). Este método actualiza las estructuras de datos internas de EventBus, que típicamente incluyen mapas para organizar las suscripciones:
private void establecerSuscripcion(Object suscriptor, MetodoSuscriptor metodoSuscriptor) {
Class<?> tipoEvento = metodoSuscriptor.getTipoEvento();
RegistroSuscripcion nuevoRegistro = new RegistroSuscripcion(suscriptor, metodoSuscriptor);
CopyOnWriteArrayList<RegistroSuscripcion> listaRegistros = suscripcionesPorTipoEvento.get(tipoEvento);
if (listaRegistros == null) {
listaRegistros = new CopyOnWriteArrayList<>();
suscripcionesPorTipoEvento.put(tipoEvento, listaRegistros);
} else {
if (listaRegistros.contains(nuevoRegistro)) {
throw new EventBusException("El suscriptor ya está registrado para este tipo de evento.");
}
}
// Insertar el nuevo registro manteniendo el orden por prioridad
int indiceInsercion = 0;
while (indiceInsercion < listaRegistros.size() &&
metodoSuscriptor.getPrioridad() <= listaRegistros.get(indiceInsercion).getMetodo().getPrioridad()) {
indiceInsercion++;
}
listaRegistros.add(indiceInsercion, nuevoRegistro);
List<Class<?>> eventosSuscritos = tiposEventosPorSuscriptor.get(suscriptor);
if (eventosSuscritos == null) {
eventosSuscritos = new ArrayList<>();
tiposEventosPorSuscriptor.put(suscriptor, eventosSuscritos);
}
eventosSuscritos.add(tipoEvento);
// ... lógica para eventos persistentes ...
}
EventBus mantiene principalmente dos mapas para gestionar los suscriptores y sus eventos:
suscripcionesPorTipoEvento: Asocia un tipo de evento con una lista de todos los suscriptores interesados en ese evento. Es crucial para el proceso de publicación.tiposEventosPorSuscriptor: Asocia un suscriptor con una lista de todos los tipos de eventos a los que está suscrito. Utilizado para verificar registros y para la cancelación de suscripciones.
Publicación de Eventos
Para enviar un evento a todos los suscriptores registrados, se utiliza el método emitirEvento (originalmente post):
MiEventBus.obtenerInstancia().emitirEvento(miObjetoEvento);
EventBus utiliza una clase auxiliar (EstadoHiloPublicacion o PostingThreadState) ligada a cada hilo mediante ThreadLocal. Esto permite que cada hilo mantenga su propia cola de eventos y estado de publicación.
public void emitirEvento(Object evento) {
EstadoHiloPublicacion estadoActual = estadoPublicacionPorHilo.get();
List<Object> colaEventos = estadoActual.colaEventos;
colaEventos.add(evento);
if (!estadoActual.estaPublicando) {
estadoActual.esHiloPrincipal = esHiloPrincipal();
estadoActual.estaPublicando = true;
try {
while (!colaEventos.isEmpty()) {
Object eventoAServir = colaEventos.remove(0);
procesarEventoIndividual(eventoAServir, estadoActual);
}
} finally {
estadoActual.estaPublicando = false;
estadoActual.esHiloPrincipal = false;
}
}
}
El método emitirEvento agrega el evento a una cola local del hilo y luego procesa los eventos uno por uno para asegurar un orden. La función procesarEventoIndividual es la encargada de encontrar y notificar a los suscriptores.
private void procesarEventoIndividual(Object evento, EstadoHiloPublicacion estado) {
Class<?> claseEvento = evento.getClass();
boolean suscriptorEncontrado = false;
// Si la herencia de eventos está activada, buscar suscriptores para todas las superclases/interfaces
if (soporteHerenciaEventos) {
List<Class<?>> tiposCompatibles = buscarTodosTiposCompatibles(claseEvento);
for (Class<?> tipo : tiposCompatibles) {
suscriptorEncontrado |= notificarSuscriptoresParaTipo(evento, estado, tipo);
}
} else {
// De lo contrario, solo para el tipo de evento exacto
suscriptorEncontrado = notificarSuscriptoresParaTipo(evento, estado, claseEvento);
}
// ... gestión de eventos no manejados ...
}
La función notificarSuscriptoresParaTipo recupera la lista de suscripciones activas para un tipo de evento específico desde suscripcionesPorTipoEvento. Luego, itera sobre cada suscripción y delega la ejecución del método suscriptor al "Póster" (Poster) correspondiente, según el ThreadMode definido.
private boolean notificarSuscriptoresParaTipo(Object evento, EstadoHiloPublicacion estado, Class<?> tipoEvento) {
CopyOnWriteArrayList<RegistroSuscripcion> listaSuscripciones;
synchronized (this) {
listaSuscripciones = suscripcionesPorTipoEvento.get(tipoEvento);
}
if (listaSuscripciones != null && !listaSuscripciones.isEmpty()) {
for (RegistroSuscripcion registro : listaSuscripciones) {
estado.eventoActual = evento;
estado.suscripcionActual = registro;
try {
// Ejecutar el método del suscriptor según su ThreadMode
ejecutarMetodoSuscriptor(registro, evento, estado.esHiloPrincipal);
} finally {
estado.eventoActual = null;
estado.suscripcionActual = null;
}
// Si el evento fue cancelado por algún suscriptor, detener la propagación
if (estado.eventoCancelado) {
break;
}
}
return true;
}
return false;
}
EventBus emplea diferentes implementaciones de "Pósteres" (Poster) para gestionar la entrega de eventos en distintos hilos:
HandlerPoster: ExtiendeHandlerde Android. Los eventos se encolan y se procesan en el hilo principal. Utiliza un mecanismo de mensajes para ejecutar las tareas, controlando el tiempo máximo de procesamiento para evitar bloqueos del hilo UI.BackgroundPoster: Los eventos se encolan y se procesan secuencialmente en un único hilo de fondo gestionado por EventBus.AsyncPoster: Cada evento se envía a unExecutorService(pool de hilos) de EventBus para ser procesado de forma asíncrona, en un hilo diferente para cada evento.
Eventos Persistentes (Sticky Events)
A diferencia de los eventos normales que requieren que un suscriptor esté registrado antes de la publicación, los eventos persistentes permiten que un evento se publique primero y luego sea recibido por los suscriptores que se registren posteriormente. Para publicar un evento persistente, se utiliza postSticky:
MiEventBus.obtenerInstancia().emitirEventoPersistente(miObjetoEvento);
Este método primero almacena el evento en un mapa especial (eventosPersistentes) y luego lo publica como un evento normal. Cuando un nuevo suscriptor se registra, EventBus verifica si alguno de sus métodos está marcado como sticky=true. Si es así, busca el evento correspondiente en eventosPersistentes y lo entrega al suscriptor.
public void emitirEventoPersistente(Object evento) {
synchronized (mapaEventosPersistentes) {
mapaEventosPersistentes.put(evento.getClass(), evento);
}
// Una vez guardado, se emite el evento para suscriptores ya registrados.
emitirEvento(evento);
}
Durante el proceso de registro del suscriptor, si un método está anotado con sticky=true, EventBus verifica el mapa de eventosPersistentes:
private void establecerSuscripcion(Object suscriptor, MetodoSuscriptor metodoSuscriptor) {
// ... lógica de registro normal ...
if (metodoSuscriptor.esPersistente()) {
if (soporteHerenciaEventos) {
for (Map.Entry<Class<?>, Object> entrada : mapaEventosPersistentes.entrySet()) {
if (metodoSuscriptor.getTipoEvento().isAssignableFrom(entrada.getKey())) {
evaluarEntregaEventoPersistente(nuevoRegistro, entrada.getValue());
}
}
} else {
Object eventoGuardado = mapaEventosPersistentes.get(metodoSuscriptor.getTipoEvento());
evaluarEntregaEventoPersistente(nuevoRegistro, eventoGuardado);
}
}
}
private void evaluarEntregaEventoPersistente(RegistroSuscripcion registro, Object eventoPersistente) {
if (eventoPersistente != null) {
// Entregar el evento persistente al nuevo suscriptor
ejecutarMetodoSuscriptor(registro, eventoPersistente, esHiloPrincipal());
}
}
Para eliminar un evento persistente, existe el método eliminarEventoPersistente(Class<?> tipoEvento), que simplemente lo retira del mapa eventosPersistentes.
Cancelación de Registro
Es fundamental cancelar el registro de los suscriptores cuando ya no sean necesarios (por ejemplo, en el ciclo de vida de una Actividad o Fragmento) para evitar fugas de memoria. Esto se realiza con el método cancelarRegistro (originalmente unregister):
MiEventBus.obtenerInstancia().cancelarRegistro(this);
El proceso de cancelación involucra la eliminación de las referencias del suscriptor de las estructuras de datos internas:
public synchronized void cancelarRegistro(Object suscriptor) {
List<Class<?>> eventosSuscritos = tiposEventosPorSuscriptor.get(suscriptor);
if (eventosSuscritos != null) {
for (Class<?> tipoEvento : eventosSuscritos) {
eliminarSuscripcionPorTipoEvento(suscriptor, tipoEvento);
}
tiposEventosPorSuscriptor.remove(suscriptor); // Eliminar todas las asociaciones del suscriptor
} else {
// Mensaje de advertencia si el suscriptor no estaba registrado
}
}
La función eliminarSuscripcionPorTipoEvento se encarga de recorrer la lista de suscripciones para un tipoEvento dado y eliminar cualquier RegistroSuscripcion que corresponda al suscriptor. Antes de eliminar, a menudo se marca una propiedad activo del registro como false para evitar que se entreguen eventos a un suscriptor que está a punto de ser removido.
private void eliminarSuscripcionPorTipoEvento(Object suscriptor, Class<?> tipoEvento) {
List<RegistroSuscripcion> listaRegistros = suscripcionesPorTipoEvento.get(tipoEvento);
if (listaRegistros != null) {
int i = 0;
while (i < listaRegistros.size()) {
RegistroSuscripcion registro = listaRegistros.get(i);
if (registro.getSuscriptor() == suscriptor) {
registro.establecerActivo(false); // Marcar como inactivo
listaRegistros.remove(i);
// No incrementar 'i' porque el tamaño de la lista ha disminuido y el siguiente elemento se ha movido
} else {
i++;
}
}
}
}
Es importante notar que, aunque se eliminan las referencias de las estructuras de suscripción, la caché de métodos (CACHE_METODOS_SUSCRIPTOR) que almacena la información de los métodos anotados @Subscribe para una clase específica, permanece intacta. Esto permite que, si el mismo componente se registra nuevamente en el futuro, el proceso de descubrimiento de métodos sea mucho más rápido.