Olá, colegas desenvolvedores!
Hoje, vamos mergulhar no módulo EventBus do framework TKClient (EventBus.java). Gerenciar a comunicação entre componentes em aplicativos complexos pode ser um desafio considerável. O EventBus surge como uma solução elegante para simplificar esse processo.
Por Que Precisamos de um Event Bus?
Em desenvolvimento de aplicativos, como os componentes se comunicam? Por exemplo, ao alterar o tema em uma página de configurações, desejamos que a página inicial atualize sua interface imediatamente. Ou, ao receber uma nova mensagem, queremos que várias telas exibam notificações.
Métodos tradicionais incluem:
- Callbacks de interface
- Broadcasts
- Variáveis globais compartilhadas
- Transmissão de dados via Intent
No entanto, cada um desses métodos apresenta suas desvantagens: callbacks podem causar vazamentos de memória, broadcasts têm eficiência limitada, variáveis globais são difíceis de gerenciar e Intents são adequados apenas para comunicação entre Activities.
O Event Bus foi projetado para superar essas limitações, oferecendo um mecanismo de comunicação de componentes com baixo acoplamento.
Arquitetura Geral do Event Bus do TKClient
O Event Bus do TKClient adota o padrão Publish-Subscribe, com os seguintes componentes principais:
EventBus: A classe central, responsável pelo registro, envio e cancelamento de registro de eventos.Event: Uma classe base para eventos; todos os eventos personalizados devem herdar dela.Subscriber: Uma interface que define métodos de tratamento de eventos.ThreadMode: Um enum que controla em qual thread o tratamento do evento será executado.
Aqui está uma estrutura básica do EventBus:
public class EventBus {
private static final String TAG = "EventBus";
private static volatile EventBus instance;
private final Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType;
private final Map<Object, List<Class<?>>> typesBySubscriber;
private final Map<Class<?>, Object> stickyEvents;
private final Executor mainThreadExecutor;
private final Executor backgroundThreadExecutor;
private final Executor asyncThreadExecutor;
private EventBus() {
this.subscriptionsByEventType = new HashMap<>();
this.typesBySubscriber = new HashMap<>();
this.stickyEvents = new HashMap<>();
this.mainThreadExecutor = new MainThreadExecutor(); // Assumindo uma implementação de Executor para a thread principal
this.backgroundThreadExecutor = Executors.newSingleThreadExecutor();
this.asyncThreadExecutor = Executors.newCachedThreadPool();
}
public static EventBus getDefault() {
if (instance == null) {
synchronized (EventBus.class) {
if (instance == null) {
instance = new EventBus();
}
}
}
return instance;
}
// Outros métodos aqui...
}
Esta é uma implementação típica do padrão Singleton, garantindo uma única instância do Event Bus em toda a aplicação. As estruturas de dados centrais são dois Maps: um mapeia tipos de eventos para seus assinantes, e o outro mapeia assinantes para os tipos de eventos que eles subscrevem. Esse mapeamento bidirecional permite a busca eficiente tanto por tipo de evento quanto por assinante.
Implementação de Registro e Cancelamento de Registro
O primeiro passo para usar o Event Bus é registrar os assinantes. O EventBus do TKClient oferece uma API de registro intuitiva:
/**
* Registra um assinante.
*/
public void register(Object subscriber) {
Class<?> subscriberClass = subscriber.getClass();
List<SubscriberMethod> methods = findSubscriberMethods(subscriberClass);
synchronized (this) {
for (SubscriberMethod method : methods) {
subscribe(subscriber, method);
}
}
}
/**
* Encontra métodos de tratamento de eventos na classe do assinante.
*/
private List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
List<SubscriberMethod> methods = new ArrayList<>();
Method[] declaredMethods = subscriberClass.getDeclaredMethods();
for (Method method : declaredMethods) {
Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);
if (subscribeAnnotation != null) {
Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length != 1) {
throw new EventBusException("@Subscribe method must have exactly 1 parameter");
}
Class<?> eventType = parameterTypes[0];
ThreadMode threadMode = subscribeAnnotation.threadMode();
SubscriberMethod subscriberMethod = new SubscriberMethod(
method,
eventType,
threadMode,
subscribeAnnotation.priority(),
subscribeAnnotation.sticky()
);
methods.add(subscriberMethod);
}
}
return methods;
}
/**
* Assina um método específico.
*/
private void subscribe(Object subscriber, SubscriberMethod method) {
Class<?> eventType = method.eventType;
Subscription subscription = new Subscription(subscriber, method);
CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
if (subscriptions == null) {
subscriptions = new CopyOnWriteArrayList<>();
subscriptionsByEventType.put(eventType, subscriptions);
}
// Insere a assinatura priorizando
int size = subscriptions.size();
for (int i = 0; i <= size; i++) {
if (i == size || subscription.priority > subscriptions.get(i).priority) {
subscriptions.add(i, subscription);
break;
}
}
// Atualiza o mapeamento do assinante para os tipos de evento
List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber);
if (subscribedEvents == null) {
subscribedEvents = new ArrayList<>();
typesBySubscriber.put(subscriber, subscribedEvents);
}
subscribedEvents.add(eventType);
// Lida com eventos pegajosos (sticky)
if (method.sticky) {
Object stickyEvent = stickyEvents.get(eventType);
if (stickyEvent != null) {
postToSubscription(subscription, stickyEvent, false);
}
}
}
O processo de registro envolve:
- Encontrar métodos anotados com
@Subscribevia reflexão. - Criar e armazenar informações de assinatura nos mapas correspondentes.
- Enviar eventos pegajosos em cache para novos assinantes, se aplicável.
Para evitar vazamentos de memória, é crucial cancelar o registro quando não for mais necessário:
/**
* Cancela o registro de um assinante.
*/
public synchronized void unregister(Object subscriber) {
List<Class<?>> subscribedTypes = typesBySubscriber.remove(subscriber);
if (subscribedTypes == null) {
Log.w(TAG, "Subscriber to unregister was not registered before: " + subscriber.getClass());
return;
}
for (Class<?> eventType : subscribedTypes) {
CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
if (subscriptions != null) {
Iterator<Subscription> iterator = subscriptions.iterator();
while (iterator.hasNext()) {
Subscription subscription = iterator.next();
if (subscription.subscriber == subscriber) {
iterator.remove();
}
}
if (subscriptions.isEmpty()) {
subscriptionsByEventType.remove(eventType);
}
}
}
}
O cancelamento de registro limpa as estruturas de dados para evitar manter referências a assinantes desnecessários.
Mecanismo de Publicação de Eventos
Após o registro, os eventos podem ser publicados. O EventBus do TKClient oferece vários métodos de publicação:
/**
* Publica um evento.
*/
public void post(Object event) {
PostingThreadState postingState = currentPostingThreadState.get();
List<Object> eventQueue = postingState.eventQueue;
eventQueue.add(event);
if (!postingState.isPosting) {
postingState.isPosting = true;
try {
while (!eventQueue.isEmpty()) {
postSingleEvent(eventQueue.remove(0), postingState);
}
} finally {
postingState.isPosting = false;
}
}
}
/**
* Publica um único evento.
*/
private void postSingleEvent(Object event, PostingThreadState postingState) {
Class<?> eventClass = event.getClass();
boolean posted = false;
posted = postToSubscriptions(subscriptionsByEventType.get(eventClass), event, postingState);
if (!posted) {
for (Class<?> superClass : eventClass.getSuperclasses()) {
if (superClass != Object.class) {
posted |= postToSubscriptions(subscriptionsByEventType.get(superClass), event, postingState);
}
}
for (Class<?> interfaceClass : eventClass.getInterfaces()) {
posted |= postToSubscriptions(subscriptionsByEventType.get(interfaceClass), event, postingState);
}
}
if (!posted) {
Log.d(TAG, "No subscribers registered for event " + eventClass);
}
}
/**
* Envia um evento para todos os assinantes na lista.
*/
private boolean postToSubscriptions(CopyOnWriteArrayList<Subscription> subscriptions, Object event, PostingThreadState postingState) {
if (subscriptions == null || subscriptions.isEmpty()) {
return false;
}
for (Subscription subscription : subscriptions) {
postingState.subscription = subscription;
postToSubscription(subscription, event, postingState.canceled);
if (postingState.canceled) {
break;
}
}
return true;
}
/**
* Envia um evento para um único assinante.
*/
private void postToSubscription(Subscription subscription, Object event, boolean canceled) {
if (canceled) {
return;
}
switch (subscription.subscriberMethod.threadMode) {
case POSTING:
invokeSubscriber(subscription, event);
break;
case MAIN:
if (isMainThread()) { // Assume que isMainThread() verifica a thread principal
invokeSubscriber(subscription, event);
} else {
mainThreadExecutor.execute(() -> {
if (!canceled) {
invokeSubscriber(subscription, event);
}
});
}
break;
case BACKGROUND:
if (!isMainThread()) {
invokeSubscriber(subscription, event);
} else {
backgroundThreadExecutor.execute(() -> {
if (!canceled) {
invokeSubscriber(subscription, event);
}
});
}
break;
case ASYNC:
asyncThreadExecutor.execute(() -> {
if (!canceled) {
invokeSubscriber(subscription, event);
}
});
break;
}
}
/**
* Invoca o método do assinante.
*/
private void invokeSubscriber(Subscription subscription, Object event) {
try {
subscription.subscriberMethod.method.invoke(subscription.subscriber, event);
} catch (IllegalAccessException e) {
throw new IllegalStateException("Unexpected exception", e);
} catch (InvocationTargetException e) {
handleSubscriberException(subscription, event, e.getCause());
}
}
O processo de publicação de eventos envolve:
- Adicionar o evento a uma fila.
- Localizar todos os assinantes cujos tipos de evento correspondem ao evento publicado.
- Invocar os métodos de tratamento de eventos nas threads apropriadas, com base no
ThreadModeespecificado.
A enumeração ThreadMode oferece flexibilidade:
public enum ThreadMode {
/** Invoca o método do assinante na thread que publicou o evento. */
POSTING,
/** Invoca o método do assinante na thread principal. */
MAIN,
/** Invoca o método do assinante em uma thread de background. Se a thread de publicação já for de background, chama diretamente. */
BACKGROUND,
/** Invoca o método do assinante sempre em uma nova thread. */
ASYNC
}
Essa abordagem permite escolher o modo de thread mais adequado para cada cenário:
POSTING: Para tratamentos de eventos simples que não envolvem atualizações de UI.MAIN: Para atualizações de UI.BACKGROUND: Para operações demoradas onde a ordem de execução não é crítica.ASYNC: Quando cada evento requer seu próprio thread independente.
Implementação de Eventos Pegajosos (Sticky Events)
Eventos pegajosos permitem que assinantes recebam eventos publicados antes de seu registro. Isso é útil para estados como status de login ou conexão de rede.
O Event Bus do TKClient implementa eventos pegajosos através dos seguintes métodos:
/**
* Publica um evento pegajoso.
*/
public void postSticky(Object event) {
synchronized (stickyEvents) {
stickyEvents.put(event.getClass(), event);
}
post(event); // Também publica como um evento normal
}
/**
* Obtém um evento pegajoso do tipo especificado.
*/
public <T> T getStickyEvent(Class<T> eventType) {
synchronized (stickyEvents) {
return eventType.cast(stickyEvents.get(eventType));
}
}
/**
* Remove um evento pegajoso do tipo especificado.
*/
public <T> T removeStickyEvent(Class<T> eventType) {
synchronized (stickyEvents) {
return eventType.cast(stickyEvents.remove(eventType));
}
}
/**
* Remove um evento pegajoso específico.
*/
public boolean removeStickyEvent(Object event) {
synchronized (stickyEvents) {
Class<?> eventType = event.getClass();
Object existingEvent = stickyEvents.get(eventType);
if (event.equals(existingEvent)) {
stickyEvents.remove(eventType);
return true;
}
return false;
}
}
/**
* Remove todos os eventos pegajosos.
*/
public void removeAllStickyEvents() {
synchronized (stickyEvents) {
stickyEvents.clear();
}
}
A lógica para eventos pegajosos envolve armazenar o evento em um cache. Quando um novo assinante se registra com o modificador sticky = true em sua anotação @Subscribe, ele recebe imediatamente o evento pegajoso em cache.
Exemplo de Uso
Vamos ver um exemplo prático:
// Definição de um evento normal
public class MessageEvent {
private String message;
public MessageEvent(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
// Definição de um evento pegajoso
public class UserLoginEvent {
private boolean loggedIn;
private String username;
public UserLoginEvent(boolean loggedIn, String username) {
this.loggedIn = loggedIn;
this.username = username;
}
public boolean isLoggedIn() {
return loggedIn;
}
public String getUsername() {
return username;
}
}
// Uso em uma Activity
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
EventBus.getDefault().register(this);
findViewById(R.id.sendButton).setOnClickListener(v -> {
EventBus.getDefault().post(new MessageEvent("Hello from MainActivity"));
});
findViewById(R.id.loginButton).setOnClickListener(v -> {
EventBus.getDefault().postSticky(new UserLoginEvent(true, "Alice"));
startActivity(new Intent(this, ProfileActivity.class));
});
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMessageEvent(MessageEvent event) {
Toast.makeText(this, "Received: " + event.getMessage(), Toast.LENGTH_SHORT).show();
}
@Override
protected void onDestroy() {
EventBus.getDefault().unregister(this);
super.onDestroy();
}
}
// Uso em outra Activity
public class ProfileActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_profile);
EventBus.getDefault().register(this);
findViewById(R.id.sendButton).setOnClickListener(v -> {
EventBus.getDefault().post(new MessageEvent("Hello from ProfileActivity"));
});
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMessageEvent(MessageEvent event) {
Toast.makeText(this, "Received: " + event.getMessage(), Toast.LENGTH_SHORT).show();
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
public void onUserLoginEvent(UserLoginEvent event) {
if (event.isLoggedIn()) {
TextView usernameText = findViewById(R.id.usernameText);
usernameText.setText("Welcome, " + event.getUsername());
} else {
finish();
}
}
@Override
protected void onDestroy() {
EventBus.getDefault().unregister(this);
super.onDestroy();
}
}
Este exemplo ilustra:
- Comunicação inter-Activity usando eventos normais.
- Transmissão de status de login com eventos pegajosos.
- Atualizações de UI na thread principal.
Análise de Cenários de Aplicação
O Event Bus é adequado para:
- Comunicação entre componentes independentes (Activities, Fragments).
- Desacoplamento de dependências complexas.
- Notificação de múltiplos componentes não relacionados.
- Sincronização de estado global.
Pode não ser ideal para:
- Comunicação simples entre pai e filho (onde chamadas diretas são mais claras).
- Operações que exigem um valor de retorno imediato.
- Eventos de alta frequência (como scroll ou touch), que podem impactar o desempenoh.
- Cenários que exigem ordem estrita de processamento de eventos.
Considerações de Otimização de Desempenho
Ao usar um Event Bus, considere:
- Custo de Reflexão: O uso extensivo de reflexão pode impactar o desempenho em chamadas de alta frequência.
- Risco de Vazamento de Memória: Esquecer de cancelar o registro pode impedir a coleta de lixo dos assinantes.
- Custo de Troca de Threads: A troca entre diferentes modos de thread tem um custo associado.
O Event Bus do TKClient inclui otimizações como cache de métodos para reduzir o custo de reflexão:
// Cache para reduzir o custo de reflexão
private static final Map<Class<?>, List<SubscriberMethod>> METHOD_CACHE = new ConcurrentHashMap<>();
/**
* Encontra métodos de tratamento de eventos na classe do assinante (versão otimizada).
*/
private List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass);
if (subscriberMethods != null) {
return subscriberMethods;
}
subscriberMethods = new ArrayList<>();
// ... código de reflexão ...
METHOD_CACHE.put(subscriberClass, subscriberMethods);
return subscriberMethods;
}
Práticas recomendadas para uso incluem:
- Registrar e cancelar o registro em métodos de ciclo de vida apropriados (por exemplo,
onCreateeonDestroyde uma Activity). - Evitar classes internas anônimas como assinantes para prevenir vazamentos de memória.
- Utilizar técnicas de throttling ou debouncing para eventos de alta frequência.
- Escolher o
ThreadModeapropriado para minimizar trocas de thread desnecessárias.
Resumo e Melhores Práticas
O Event Bus do TKClient, baseado no padrão Publish-Subscribe, facilita a comunicação desacoplada entre componentes. Suas carcaterísticas incluem:
- API limpa com reflexão e anotações.
- Suporte a múltiplos modos de thread.
- Funcionalidade de evento pegajoso para sincronização de estado.
Melhores práticas:
- Projete classes de evento claras e com significado de negócio explícito.
- Use os modos de thread adequados (
MAINparra UI,BACKGROUND/ASYNCpara operações demoradas). - Sempre chame
registereunregisterem pares. - Use eventos pegajosos com cautela, lembrando-se de removê-los quando não forem mais necessários.
- Não abuse do Event Bus; prefira chamadas diretas para comunicação simples entre pai e filho.