How does LeakCanary work? Join me in unraveling its mystery.
I. What is LeakCanary
LeakCanary is square’s open source memory leak detection tool. At present, most apps will access this tool to detect potential memory leaks during the development and testing phase. A better App may build a server to save memory leaks on each device and then centrally deal with them.
This article is published on my wechat public number: Android Development Lab. Welcome to pay attention and learn Android together with me.
Two, why use LeakCanary
We know that there are many ways to detect Memory leaks, such as Android Studio’s own Profile Tool, MAT(Memory Analyzer Tool), and LeakCanary. LeakCanary was chosen as the preferred memory leak detection tool primarily because it detects leaks in real time and shows the cause of memory leaks in a very intuitive call chain.
Iii. LeakCanary Cannot Do (to be determined)
Although LeakCanary has many advantages, but it can not do, such as detection of OOM problems caused by applying for large memory, Bitmap memory not released, memory leaks in the Service may not be detected, etc.
Four, LeakCanary source analysis
This chapter is highly dependent and is recommended to be read sequentially.
4.1 ActivityLifecycleCallbacks FragmentLifeCycleCallbacks
Before starting LeakCanary principle analysis, it is necessary to simple ActivityLifecycleCallbacks and FragmentLifeCycleCallbacks said.
/ / ActivityLifecycleCallbacks interface
public interface ActivityLifecycleCallbacks {
void onActivityCreated(Activity var1, Bundle var2);
void onActivityStarted(Activity var1);
void onActivityResumed(Activity var1);
void onActivityPaused(Activity var1);
void onActivityStopped(Activity var1);
void onActivitySaveInstanceState(Activity var1, Bundle var2);
void onActivityDestroyed(Activity var1);
}
Copy the code
Application class provides registerActivityLifecycleCallbacks and unregisterActivityLifecycleCallbacks method used for the registration and the registration Activity lifecycle listening in class, This allows us to do some unified processing for all Activity lifecycle callbacks in the Application.
public abstract static class FragmentLifecycleCallbacks {
public void onFragmentCreated(FragmentManager fm, Fragment f, Bundle savedInstanceState) {}
public void onFragmentViewDestroyed(FragmentManager fm, Fragment f) {}
public void onFragmentDestroyed(FragmentManager fm, Fragment f) {}
// Omit the rest of the lifecycle...
}
Copy the code
FragmentManager class provides registerFragmentLifecycleCallbacks and user registration and the registration fragments unregisterFragmentLifecycleCallbacks method The lifecycle listener class, so that we can get all the Fragment lifecycle callbacks by registering each Activity.
4.2 Use of LeakCanary
4.2.1 Usage
Let’s just add the code directly to the Application class.
public class ExampleApplication extends Application {
@Override public void onCreate(a) {
super.onCreate();
setupLeakCanary();
}
protected void setupLeakCanary(a) {
// Enable strict mode
enabledStrictMode();
// Check whether HeapAnalyzerService belongs to the process
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
/ / register LeakCanary
LeakCanary.install(this);
}
private static void enabledStrictMode(a) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() //
.detectAll() //
.penaltyLog() //
.penaltyDeath() //.build()); }}Copy the code
<service
android:name=".internal.HeapAnalyzerService"
android:process=":leakcanary"
android:enabled="false"
/>
Copy the code
Since LeakCanary’s core hROPF file resolution service HeapAnalyzerService belongs to a process that is independent of the main process, we need to exclude other processes in setupLeakCanary. Register leakCanary Monitor processing only for the LeakCanary process.
Android :enabled=”false” HeapAnalyzerService is not available by default, so if you dynamically enable this component in your code, you can use the following methods:
public static void setEnabledBlocking(Context appContext, Class<? > componentClass,boolean enabled) {
ComponentName component = new ComponentName(appContext, componentClass);
PackageManager packageManager = appContext.getPackageManager();
int newState = enabled ? COMPONENT_ENABLED_STATE_ENABLED : COMPONENT_ENABLED_STATE_DISABLED;
// Blocks on IPC.
packageManager.setComponentEnabledSetting(component, newState, DONT_KILL_APP);
}
Copy the code
4.3 What did LeakCanary. Install (this) do
LeakCanary’s install method actually constructs a RefWatcher,
/**
* Creates a {@link RefWatcher} that works out of the box, and starts watching activity
* references (on ICS+).
*/
public static @NonNull RefWatcher install(@NonNull Application application) {
return refWatcher(application).listenerServiceClass(DisplayLeakService.class)
.excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
.buildAndInstall();
}
Copy the code
Let’s look at the registration method one by one. First is refWatcher method constructs a AndroidRefWatcherBuilder, incoming parameters is the Context of the current Application.
public static @NonNull AndroidRefWatcherBuilder refWatcher(@NonNull Context context) {
return new AndroidRefWatcherBuilder(context);
}
Copy the code
The listenerServiceClass and excludedRefs methods pass in the analysis Service based on the builder pattern and exclude known leak problems AndroidExcludedRefs, which I won’t post code for here.
Let’s focus on the buildAndInstall method. This method visually indicates that we are going to build the last step of the builder pattern and register some listeners. Let’s look at the code below:
public @NonNull RefWatcher buildAndInstall(a) {
// Install only once
if(LeakCanaryInternals.installedRefWatcher ! =null) {
throw new UnsupportedOperationException("buildAndInstall() should only be called once.");
}
// The last step of the builder pattern is to construct the object
RefWatcher refWatcher = build();
// Check whether LeakCanary is enabled. If not, a DISABLED object is returned by default
if(refWatcher ! = DISABLED) {// Manually start the DisplayLeakActivity component to display an entry on the desktop to view the results of the memory leak
LeakCanaryInternals.setEnabledAsync(context, DisplayLeakActivity.class, true);
// Whether to detect memory leaks in the Activity. This function is enabled by default
if (watchActivities) {
ActivityRefWatcher.install(context, refWatcher);
}
// Whether to detect Fragment memory leaks. This function is enabled by default
if(watchFragments) { FragmentRefWatcher.Helper.install(context, refWatcher); }}// Copy to a global static variable to prevent a second call
LeakCanaryInternals.installedRefWatcher = refWatcher;
return refWatcher;
}
Copy the code
Above code effect most comments in the code, the remaining ActivityRefWatcher. Install and FragmentRefWatcher Helper. Install method without comment. Let’s take a look at exactly what these two methods do.
(1). ActivityRefWatcher.install
ActivityRefWatcher static method of capturing the install the Application, and then added a lifecycle listener ActivityLifecycleCallbacks, The lifecycleCallbacks only focus on the onActivityDestroyed callback, where the incoming object Activity is monitored, refwatcher.watch (Activity); We will analyze the specific code of.
public static void install(@NonNull Context context, @NonNull RefWatcher refWatcher) {
Application application = (Application) context.getApplicationContext();
ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);
}
private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
new ActivityLifecycleCallbacksAdapter() {
@Override public void onActivityDestroyed(Activity activity) { refWatcher.watch(activity); }};Copy the code
(2). FragmentRefWatcher. Helper. Install FragmentRefWatcher. The Helper of the static methods in the install will also register a ActivityLifecycleCallbacks for listening The onActivityCreated callback in the Activity lifecycle registers the Fragment lifecycle listener for the Activity after it has been created. Install method is first to determine whether a system is greater than or equal to the Android O, if so will use Android app. FragmentManager register, If need to be compatible with Android O the following need to rely on the add dependence on leakcanary – support – fragment components, and then through reflection SupportFragmentRefWatcher is constructed; Then remove all fragmentRefWatchers listeners and add the Fragment’s health listener after the Activity is created. Focus on onFragmentViewDestroyed and onFragmentDestroyed methods of the Fragment. The specific code is as follows:
public static void install(Context context, RefWatcher refWatcher) {
List<FragmentRefWatcher> fragmentRefWatchers = new ArrayList<>();
/ / system is greater than or equal to the Android O, if it is, add AndroidOFragmentRefWatcher
if (SDK_INT >= O) {
fragmentRefWatchers.add(new AndroidOFragmentRefWatcher(refWatcher));
}
/ / if you add the leakcanary - support - fragments of dependence, through reflection can construct SupportFragmentRefWatcher
try{ Class<? > fragmentRefWatcherClass = Class.forName(SUPPORT_FRAGMENT_REF_WATCHER_CLASS_NAME); Constructor<? > constructor = fragmentRefWatcherClass.getDeclaredConstructor(RefWatcher.class); FragmentRefWatcher supportFragmentRefWatcher = (FragmentRefWatcher) constructor.newInstance(refWatcher); fragmentRefWatchers.add(supportFragmentRefWatcher); }catch (Exception ignored) {
}
if (fragmentRefWatchers.size() == 0) {
return;
}
Helper helper = new Helper(fragmentRefWatchers);
// Listen to the callback when the Activity is created
Application application = (Application) context.getApplicationContext();
application.registerActivityLifecycleCallbacks(helper.activityLifecycleCallbacks);
}
private final Application.ActivityLifecycleCallbacks activityLifecycleCallbacks =
new ActivityLifecycleCallbacksAdapter() {
@Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
// After the Activity is created, register the Fragment in the Activity to listen for the lifecycle
for(FragmentRefWatcher watcher : fragmentRefWatchers) { watcher.watchFragments(activity); }}};Copy the code
// AndroidOFragmentRefWatcher.java
private final FragmentManager.FragmentLifecycleCallbacks fragmentLifecycleCallbacks =
new FragmentManager.FragmentLifecycleCallbacks() {
// Fragment When the View in the Fragment is destroyed
@Override public void onFragmentViewDestroyed(FragmentManager fm, Fragment fragment) {
View view = fragment.getView();
if(view ! =null) { refWatcher.watch(view); }}// Fragment Destruction
@Override
public void onFragmentDestroyed(FragmentManager fm, Fragment fragment) { refWatcher.watch(fragment); }};@Override public void watchFragments(Activity activity) {
/ / by FragmentManager FragmentLifecycleCallbacks registration
FragmentManager fragmentManager = activity.getFragmentManager();
fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true);
}
Copy the code
We have fully understood the above process, which can be expressed in a flowchart:
4.4 LeakCanary Memory leak detection Principle
In terms of structure, this section should belong to the latter half of the previous section, but RefWatcher’s watch method is important and complex enough, so it is necessary to set up a separate section to explain the internal principles in detail.
4.4.1 basic knowledge — WeakReference WeakReference and ReferenceQueue ReferenceQueue
For more information on reference types and reference queues, you can refer to the Vernacular JVM – In-depth Object References, which I think is fairly clear.
Here, I’ll give you a simple example. When a weak reference is defined, you can specify the reference object and a ReferenceQueue. When the garbage collector executes the collection method of a weak reference object, if the original object is only referred to by this weak reference object, it will reclaim the original object and add the weak reference object to the ReferenceQueue. Using the poll method of ReferenceQueue, we can fetch this weak reference object and get some information about the weak reference object itself. Look at this example.
mReferenceQueue = new ReferenceQueue<>();
// Define an object
o = new Object();
// Define a weak reference object reference o and specify the reference queue as mReferenceQueue
weakReference = new WeakReference<Object>(o, mReferenceQueue);
// Remove strong references
o = null;
// Triggers the application to garbage collect
Runtime.getRuntime().gc();
// hack: delay 100ms until the GC completes
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
Reference ref = null;
// Iterate over mReferenceQueue, fetching all weak references
while((ref = mReferenceQueue.poll()) ! =null) {
System.out.println("============ \n ref in queue");
}
Copy the code
The printed result is:
============ ref in queue
4.4.2 Basics — the hprof file
The hprof file shows how the Java heap is being used at any given time. From this file, we can locate memory leaks by analyzing which objects are consuming a lot of memory and not being freed at the right time.
Overall, Android generates hprof files in two ways:
- Using the adb command
adb shell am dumpheap <processname> <FileName>
Copy the code
- . The use of the android OS. Debug. DumpHprofData method directly using the Debug class provides dumpHprofData method.
Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
Copy the code
The Memory module of Android Profiler in Android Studio uses method 1. The.hprof files generated by these two methods are in Dalvik format and need to be converted into J2SE hprof format using the hprof-conv tool provided by the AndroidSDK before viewing in standard hprof tools such as MAT.
hprof-conv dump.hprof converted-dump.hprof
Copy the code
As for the internal format of Hprof, I will not introduce it in detail in this article, and I will have the opportunity to write a separate article to explain it in detail in the future. LeakCanary parsed the.hprof file using another square open source project: Haha.
4.4.3 watch method
Finally, the key to LeakCanary. Starting with the watch method, the previous code is all about robustness, we start directly by generating the unique ID, LeakCanary constructs a weak reference object with a key, and sets queue to the reference queue of the weak reference object.
Why do we need to create a WeakReference object with key? Can we use WeakReference directly? For example, if there is a memory leak in OneActivity, the Activity object will definitely not be reclaimed when GC operation is performed, so the WeakReference object will not be reclaimed either. If N oneActivities are currently started, we can get all the oneActivities in memory when we Dump memory, but when we try to detect a leak in one of them, we cannot match. However, if WeakReference object with key is used, the value of key will also be saved in dump when leakage occurs, so that we can map to an Activity according to the one-to-one mapping relationship of key.
LeakCanary then calls the ensureGoneAsync method to detect the memory leak.
public void watch(Object watchedReference, String referenceName) {
if (this == DISABLED) {
return;
}
checkNotNull(watchedReference, "watchedReference");
checkNotNull(referenceName, "referenceName");
final long watchStartNanoTime = System.nanoTime();
// Set a unique ID for the currently monitored object
String key = UUID.randomUUID().toString();
// Add to Set
retainedKeys.add(key);
// Construct a WeakReference object with ID
final KeyedWeakReference reference =
new KeyedWeakReference(watchedReference, key, referenceName, queue);
// Check whether the object is reclaimed
ensureGoneAsync(watchStartNanoTime, reference);
}
Copy the code
4.4.4 ensureGoneAsync method
The ensureGoneAsync method constructs a Retryable object and passes it to the Execute method of the watchExecutor.
private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
// watchExecutor is an instance of AndroidWatchExecutor
watchExecutor.execute(new Retryable() {
@Override public Retryable.Result run(a) {
returnensureGone(reference, watchStartNanoTime); }}); }Copy the code
WatchExecutor is an instance of AndroidWatchExecutor. The Execute method of AndroidWatchExecutor is used to determine whether the current thread is the main thread. If it is, Then execute the waitForIdle method directly, otherwise switch to the main thread via the Handler’s POST method and execute the waitForIdle method again.
@Override public void execute(@NonNull Retryable retryable) {
// Check whether the current thread is the main thread
if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
waitForIdle(retryable, 0);
} else {
postWaitForIdle(retryable, 0); }}Copy the code
WaitForIdle method by calling addIdleHandler method, specify when the main event, there is no need to deal with in the process of execution during this free postToBackgroundWithDelay method.
private void waitForIdle(final Retryable retryable, final int failedAttempts) {
Looper.myqueue () ¶ Looper.myQueue() ¶ Looper.myQueue(); looper.myQueue ()
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override public boolean queueIdle(a) {
postToBackgroundWithDelay(retryable, failedAttempts);
// Return false to remove the event immediately after execution
return false; }}); }Copy the code
DelayMillis postToBackgroundWithDelay method first, will delay time, the delay is a exponentialBackoffFactor factors (index) multiplied by the initial delay time, ExponentialBackoffFactor will take a smaller value in 2^n and long.max_value/initialDelayMillis, DelayMillis = initialDelayMillis * 2^n and cannot exceed long.max_value.
private void postToBackgroundWithDelay(final Retryable retryable, final int failedAttempts) {
long exponentialBackoffFactor = (long) Math.min(Math.pow(2, failedAttempts), maxBackoffFactor);
// Calculate the delay time
long delayMillis = initialDelayMillis * exponentialBackoffFactor;
// Switch to the child thread to execute
backgroundHandler.postDelayed(new Runnable() {
@Override public void run(a) {
// Execute the run method in retryable
Retryable.Result result = retryable.run();
// If you need to retry, then add to the main thread during idle execution
if (result == RETRY) {
postWaitForIdle(retryable, failedAttempts + 1);
}
}
}, delayMillis);
}
Copy the code
PostToBackgroundWithDelay method each will increase exponentially delay time, delay time after, will perform in Retryable method, if return to try again, so can increase the delay time and execute the next.
What does the run method of retryable. Run () execute? Don’t forget the code in our ensureGoneAsync, the code that has been retried officially ensureGone method.
private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
watchExecutor.execute(new Retryable() {
@Override public Retryable.Result run(a) {
returnensureGone(reference, watchStartNanoTime); }}); }Copy the code
4.4.5 ensureGone method
I’ll now post the complete code for the ensureGone method, which we’ll analyze line by line:
Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
long gcStartNanoTime = System.nanoTime();
// There is a retry mechanism, which counts how long it has taken since the first execution
long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
// Remove all weakly referenced reachable objects, more on this later
removeWeaklyReachableReferences();
// Check whether USB debugging is currently enabled. LeakCanary explains that debugging may trigger an incorrect memory leak
if (debuggerControl.isDebuggerAttached()) {
// The debugger can create false leaks.
return RETRY;
}
/ / perform removeWeaklyReachableReferences method above, distinguish the monitoring objects have been recycled, if be recycled, so that no memory leaks occur, directly to the end
if (gone(reference)) {
return DONE;
}
// Trigger a garbage collection manually
gcTrigger.runGc();
// Remove all weakly referenced reachable objects again
removeWeaklyReachableReferences();
// If the object is not reclaimed
if(! gone(reference)) {long startDumpHeap = System.nanoTime();
long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
// Dump the current heap memory using the Debug class
File heapDumpFile = heapDumper.dumpHeap();
// If dumpHeap fails, the retry mechanism will be used
if (heapDumpFile == RETRY_LATER) {
// Could not dump the heap.
return RETRY;
}
long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
// Construct a HeapDump object from the hprof file, key, and other attributes
HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key)
.referenceName(reference.name)
.watchDurationMs(watchDurationMs)
.gcDurationMs(gcDurationMs)
.heapDumpDurationMs(heapDumpDurationMs)
.build();
// heapdumpListener analyzes heapDump objects
heapdumpListener.analyze(heapDump);
}
return DONE;
}
Copy the code
After looking at the above code, the general process of leak detection has gone through, and now let’s look at some specific details.
(1). RemoveWeaklyReachableReferences method
RemoveWeaklyReachableReferences remove all weak references to objects is how work?
private void removeWeaklyReachableReferences(a) {
KeyedWeakReference ref;
while((ref = (KeyedWeakReference) queue.poll()) ! =null) { retainedKeys.remove(ref.key); }}Copy the code
If this object is reclaimed, then the weak reference object will be added to the queue when it is reclaimed. The poll operation will retrieve the weak reference. At this point we remove the key from the retainedKeys, indicating that the object has been reclaimed and no longer needs to be monitored.
So now, it’s a little easier to tell if this object has been reclaimed, right?
private boolean gone(KeyedWeakReference reference) {
// If the retainedKeys do not contain reference.key, the object has been reclaimed
return! retainedKeys.contains(reference.key); }Copy the code
(2). DumpHeap method
Heapdumper.dumpheap () is the execution method that generates hProf. HeapDumper is an object of AndroidHeapDumper. Let’s take a look at its dump method.
public File dumpHeap(a) {
// Generate a file to store hProf
File heapDumpFile = leakDirectoryProvider.newHeapDumpFile();
// Failed to create file
if (heapDumpFile == RETRY_LATER) {
return RETRY_LATER;
}
// The FutureResult has a CountDownLatch inside for counting down
FutureResult<Toast> waitingForToast = new FutureResult<>();
// Switch to the main thread and display toast
showToast(waitingForToast);
// Wait 5 seconds to make sure the toast is displayed
if(! waitingForToast.wait(5, SECONDS)) {
CanaryLog.d("Did not dump heap, too much time waiting for Toast.");
return RETRY_LATER;
}
// Create a notification
Notification.Builder builder = new Notification.Builder(context)
.setContentTitle(context.getString(R.string.leak_canary_notification_dumping));
Notification notification = LeakCanaryInternals.buildNotification(context, builder);
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
int notificationId = (int) SystemClock.uptimeMillis();
notificationManager.notify(notificationId, notification);
Toast toast = waitingForToast.get();
try {
// Start to dump memory to the specified file
Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
cancelToast(toast);
notificationManager.cancel(notificationId);
return heapDumpFile;
} catch (Exception e) {
CanaryLog.d(e, "Could not dump heap");
// Abort heap dump
returnRETRY_LATER; }}Copy the code
In this code we need to look at the showToast() method and how it ensures that the toast is displayed (a bit of a dark tech touch).
private void showToast(final FutureResult<Toast> waitingForToast) {
mainHandler.post(new Runnable() {
@Override public void run(a) {
// If the current Activity has been paused, return directly
if (resumedActivity == null) {
waitingForToast.set(null);
return;
}
// Create a toast object
final Toast toast = new Toast(resumedActivity);
toast.setGravity(Gravity.CENTER_VERTICAL, 0.0);
toast.setDuration(Toast.LENGTH_LONG);
LayoutInflater inflater = LayoutInflater.from(resumedActivity);
toast.setView(inflater.inflate(R.layout.leak_canary_heap_dump_toast, null));
// Add toast to the display queue
toast.show();
// Waiting for Idle to make sure Toast gets rendered.
// Add the idle time operation to the main thread. If the main thread is idle, CountDownLatch performs the countDown operation
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override public boolean queueIdle(a) {
waitingForToast.set(toast);
return false; }}); }}); }Copy the code
The first thing we need to know is that all Toast objects are not immediately displayed when we call the show method. NotificationServiceManager will from mToastQueue polling remove Toast object for display. If the display of the Toast is not real time, how do we know if the display of the Toast is complete? We call addIdleHandler after the show method is called from Toast to subtract one from CountDownLatch when the main process is idle. Because we know what actions we are adding to the message queue on the main thread in sequence: display Toast first, then CountDownLatch by one. So in if (! Waitingfortoast.wait (5, SECONDS)), we can wait up to 5 SECONDS. If the timeout occurs, the retry mechanism will be used. If CountDownLatch has already performed the operation of minus one, the subsequent process will be normal. We can also deduce that the toast must have been displayed before it.
Debug.dumpHprofData(heapDumpFile.getAbsolutePath()); The system Debug class provides the method, I will not do the detailed analysis.
(3). HeapdumpListener. Analyze (heapdumps) method
HeapdumpListener ServiceHeapDumpListener is an object, finally carried out HeapAnalyzerService. RunAnalysis method.
@Override public void analyze(@NonNull HeapDump heapDump) {
checkNotNull(heapDump, "heapDump");
HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass);
}
Copy the code
// Start the foreground service
public static void runAnalysis(Context context, HeapDump heapDump, Class
listenerServiceClass) {
setEnabledBlocking(context, HeapAnalyzerService.class, true);
setEnabledBlocking(context, listenerServiceClass, true);
Intent intent = new Intent(context, HeapAnalyzerService.class);
intent.putExtra(LISTENER_CLASS_EXTRA, listenerServiceClass.getName());
intent.putExtra(HEAPDUMP_EXTRA, heapDump);
ContextCompat.startForegroundService(context, intent);
}
Copy the code
HeapAnalyzerService inherits from IntentService. I won’t go into detail about how IntentService works. IntentService executes the onHandleIntent method sequentially for all concurrent service startup operations.
@Override protected void onHandleIntentInForeground(@Nullable Intent intent) {
if (intent == null) {
CanaryLog.d("HeapAnalyzerService received a null intent, ignoring.");
return;
}
// The class that listens for hprof file analysis results
String listenerClassName = intent.getStringExtra(LISTENER_CLASS_EXTRA);
// Hprof file class
HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAPDUMP_EXTRA);
HeapAnalyzer heapAnalyzer =
new HeapAnalyzer(heapDump.excludedRefs, this, heapDump.reachabilityInspectorClasses);
// checkForLeak will call the tool in the haha component to analyze the hprof file
AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey,
heapDump.computeRetainedHeapSize);
// Send the result to the listener listenerClassName
AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
}
Copy the code
Let’s look at the checkForLeak method, let’s look at it.
public @NonNull AnalysisResult checkForLeak(@NonNull File heapDumpFile,
@NonNull String referenceKey,
boolean computeRetainedSize) {
long analysisStartNanoTime = System.nanoTime();
// Return if the file does not exist
if(! heapDumpFile.exists()) { Exception exception =new IllegalArgumentException("File does not exist: " + heapDumpFile);
return failure(exception, since(analysisStartNanoTime));
}
try {
// Update progress callback
listener.onProgressUpdate(READING_HEAP_DUMP_FILE);
// Parses the hprof file into snapshots
HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
HprofParser parser = new HprofParser(buffer);
listener.onProgressUpdate(PARSING_HEAP_DUMP);
Snapshot snapshot = parser.parse();
listener.onProgressUpdate(DEDUPLICATING_GC_ROOTS);
// Remove the same GC root entry
deduplicateGcRoots(snapshot);
listener.onProgressUpdate(FINDING_LEAKING_REF);
// Find the memory leak item
Instance leakingRef = findLeakingReference(referenceKey, snapshot);
// False alarm, weak reference was cleared in between key check and heap dump.
// If it is not found, it means there is no leak
if (leakingRef == null) {
return noLeak(since(analysisStartNanoTime));
}
// Find the reference chain at the leak
return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, computeRetainedSize);
} catch (Throwable e) {
returnfailure(e, since(analysisStartNanoTime)); }}Copy the code
The parsing of the hprof file is done by the open source project Haha, which I won’t expand on too much here.
The findLeakingReference method looks for the leaked reference. Let’s look at the code:
private Instance findLeakingReference(String key, Snapshot snapshot) {
// Find all instances of KeyedWeakReference from the objects saved in the hprof file
ClassObj refClass = snapshot.findClass(KeyedWeakReference.class.getName());
if (refClass == null) {
throw new IllegalStateException(
"Could not find the " + KeyedWeakReference.class.getName() + " class in the heap dump.");
}
List<String> keysFound = new ArrayList<>();
// Iterate over KeyedWeakReference instance list
for (Instance instance : refClass.getInstancesList()) {
// Get all fields in each instance
List<ClassInstance.FieldValue> values = classInstanceValues(instance);
// Find the value corresponding to the key field
Object keyFieldValue = fieldValue(values, "key");
if (keyFieldValue == null) {
keysFound.add(null);
continue;
}
// Convert the keyFieldValue to a String
String keyCandidate = asString(keyFieldValue);
// If the key of the object is the same as the key we are looking for, then return the original object held by the weak object
if (keyCandidate.equals(key)) {
return fieldValue(values, "referent");
}
keysFound.add(keyCandidate);
}
throw new IllegalStateException(
"Could not find weak reference with key " + key + " in " + keysFound);
}
Copy the code
By now, we have read the source code for LeakCanary’s entire procedure for detecting memory leaks. Personally, I think LeakCanary source code is well written, highly readable, find the call relationship is also more convenient (here black Bilibili DanmakusFlameMaster).
5. Copyright notice
This article is published on my wechat public number: Android Development Lab. Welcome to pay attention and learn Android together with me. Shall not be reproduced without permission.