androiddebuggingloggingmemory-leaksleakcanary

How to log memory leaks using a custom logger?


I started using LeakCanary in my app. Based on various sources, I found that with this dependency:

 debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'

LeakCanary watches the following components by default: Activities, Fragments (both androidx.fragment.app.Fragment and android.app.Fragment), ViewModels, Services, Broadcast Receivers (especially dynamically registered ones), and View Roots.

Now, I want to log memory leaks detected in any of these components. So I wrote the following code:

  private void setLeakCanaryData() {
                if (BuildConfig.DEBUG) {
                    LeakCanary.Config oldConfig = LeakCanary.getConfig();
                    List<EventListener> newEventListeners = getEventListeners(oldConfig);
                    LeakCanary.Config newConfig = new LeakCanary.Config(
                            oldConfig.getDumpHeap(),
                            oldConfig.getDumpHeapWhenDebugging(),
                            oldConfig.getRetainedVisibleThreshold(),
                            oldConfig.getReferenceMatchers(),
                            oldConfig.getObjectInspectors(),
                            oldConfig.getOnHeapAnalyzedListener(),
                            oldConfig.getMetadataExtractor(),
                            oldConfig.getComputeRetainedHeapSize(),
                            oldConfig.getMaxStoredHeapDumps(),
                            oldConfig.getRequestWriteExternalStoragePermission(),
                            oldConfig.getLeakingObjectFinder(),
                            oldConfig.getHeapDumper(),
                            newEventListeners,
                            oldConfig.getShowNotifications(),
                            oldConfig.getUseExperimentalLeakFinders()
                    );
                    LeakCanary.setConfig(newConfig);
                }
            }
        
            private static @NonNull List<EventListener> getEventListeners(LeakCanary.Config oldConfig) {
                List<EventListener> newEventListeners = new ArrayList<>(oldConfig.getEventListeners());
                newEventListeners.add(event -> {
                    if (event instanceof EventListener.Event.HeapAnalysisDone) {
                        HeapAnalysis analysis = ((EventListener.Event.HeapAnalysisDone<?>) event).getHeapAnalysis();
        
                        if (analysis instanceof HeapAnalysisSuccess) {
                            HeapAnalysisSuccess success = (HeapAnalysisSuccess) analysis;
        
                            for (ApplicationLeak leak : success.getApplicationLeaks()) {
                               sendLogInfo("Application Leak: " + leak.toString());
                            }
        
                            for (LibraryLeak leak : success.getLibraryLeaks()) {
                               sendLogInfo("Library Leak: " + leak.toString());
                            }
                        } else if (analysis instanceof HeapAnalysisFailure) {
                            HeapAnalysisFailure failure = (HeapAnalysisFailure) analysis;
                            sendLogInfo("Heap analysis failed: " + failure.getException().getMessage());
                        }
                    }
                });
                return newEventListeners;
            }

My questions are:

  1. Is this the correct and recommended way to log memory leaks using
    LeakCanary?

  2. Is there any cleaner or more efficient method to attach event listeners in Java?

Any help or suggestions are appreciated. Thank you!


Solution

  • Your approach is perfectly fine, but you can write the code in a cleaner way:-

    1. Create an Interface for Logging

      public interface LeakLogger {
          void logApplicationLeak(String message);
          void logLibraryLeak(String message);
          void logAnalysisFailure(String message);
      }
      
    2. Implement the Logger (e.g., ConsoleLogger)

      public class ConsoleLeakLogger implements LeakLogger {
          @Override
          public void logApplicationLeak(String message) {
              Log.d("LeakCanary", "Application Leak: " + message);
          }
      
          @Override
          public void logLibraryLeak(String message) {
              Log.d("LeakCanary", "Library Leak: " + message);
          }
      
          @Override
          public void logAnalysisFailure(String message) {
              Log.e("LeakCanary", "Heap analysis failed: " + message);
          }
      }
      
    3. Create an EventListener Provider

      public class LeakCanaryEventListener implements EventListener {
          private final LeakLogger logger;
      
          public LeakCanaryEventListener(LeakLogger logger) {
              this.logger = logger;
          }
      
          @Override
          public void onEvent(Event event) {
              if (event instanceof Event.HeapAnalysisDone) {
                  HeapAnalysis analysis = ((Event.HeapAnalysisDone<?>) event).getHeapAnalysis();
      
                  if (analysis instanceof HeapAnalysisSuccess) {
                      HeapAnalysisSuccess success = (HeapAnalysisSuccess) analysis;
      
                      for (ApplicationLeak leak : success.getApplicationLeaks()) {
                          logger.logApplicationLeak(leak.toString());
                      }
      
                      for (LibraryLeak leak : success.getLibraryLeaks()) {
                          logger.logLibraryLeak(leak.toString());
                      }
      
                  } else if (analysis instanceof HeapAnalysisFailure) {
                      HeapAnalysisFailure failure = (HeapAnalysisFailure) analysis;
                      logger.logAnalysisFailure(failure.getException().getMessage());
                  }
              }
          }
      }
      
    4. Apply Configuration in a Setup Class

      public class LeakCanaryInitializer {
      
          public static void setupLeakCanary() {
              if (!BuildConfig.DEBUG) return;
      
              LeakLogger logger = new ConsoleLeakLogger(); // or use DI
              EventListener customListener = new LeakCanaryEventListener(logger);
      
              LeakCanary.Config oldConfig = LeakCanary.getConfig();
              List<EventListener> newListeners = new ArrayList<>(oldConfig.getEventListeners());
              newListeners.add(customListener);
      
              LeakCanary.Config newConfig = new LeakCanary.Config(
                  oldConfig.getDumpHeap(),
                  oldConfig.getDumpHeapWhenDebugging(),
                  oldConfig.getRetainedVisibleThreshold(),
                  oldConfig.getReferenceMatchers(),
                  oldConfig.getObjectInspectors(),
                  oldConfig.getOnHeapAnalyzedListener(),
                  oldConfig.getMetadataExtractor(),
                  oldConfig.getComputeRetainedHeapSize(),
                  oldConfig.getMaxStoredHeapDumps(),
                  oldConfig.getRequestWriteExternalStoragePermission(),
                  oldConfig.getLeakingObjectFinder(),
                  oldConfig.getHeapDumper(),
                  newListeners,
                  oldConfig.getShowNotifications(),
                  oldConfig.getUseExperimentalLeakFinders()
              );
      
              LeakCanary.setConfig(newConfig);
          }
      }
      
      
    5. And finally in Application class

    @Override
    public void onCreate() {
        super.onCreate();
        LeakCanaryInitializer.setupLeakCanary();
    }