Skip to content

Commit

Permalink
Update docs for next release (#1495)
Browse files Browse the repository at this point in the history
Fixes #1489
  • Loading branch information
pyricau authored Jul 26, 2019
1 parent 47dab4a commit 2f44a2d
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 100 deletions.
2 changes: 1 addition & 1 deletion docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ If you find a new one, please [create an issue](https://github.com/square/leakca
3. Check if it happens on the latest version of Android, and otherwise use blame to find when it was fixed.
4. If it's still happening, build a simple repro case.
5. File an issue on [b.android.com](http://b.android.com) with the leak trace and the repro case.
6. Create a PR in LeakCanary to update `AndroidExcludedRefs.kt`. Optional: if you find a hack to clear that leak on previous versions of Android, feel free to document it.
6. Create a PR in LeakCanary to update [AndroidReferenceMatchers](/api/shark-android/shark/-android-reference-matchers/). Optional: if you find a hack to clear that leak on previous versions of Android, feel free to document it.

## How do I share a leak trace?

Expand Down
28 changes: 15 additions & 13 deletions docs/fundamentals.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ For example, an Android activity instance is no longer needed after its `onDestr

Most memory leaks are caused by bugs related to the lifecycle of objects. Here are a few common Android mistakes:

* Storing an Activity context as a field in an object that survives activity recreation configuration changes.
* Storing an Activity context as a field in an object that survives activity recreation due to configuration changes.
* Registering a listener, broadcast receiver or RxJava subscription which references an object with lifecycle, and forgetting to unregister when the lifecycle reaches its end.
* Storing a view in a static field, and not clearing that field when the view is detached.

Expand All @@ -24,17 +24,17 @@ When we first enabled LeakCanary in the Square Point Of Sale app, we were able t

### Detecting retained instances

The foundation of LeakCanary is a library called Object Watcher Android. It hooks into the Android lifecycle to automatically detect when activities and fragments are destroyed and should be garbage collected. These destroyed instances are passed to an `ObjectWatcher`, which holds weak references to them. You can also watch any instance that is no longer needed, e.g. a detached view, a destroyed presenter, etc.
The foundation of LeakCanary is a library called ObjectWatcher Android. It hooks into the Android lifecycle to automatically detect when activities and fragments are destroyed and should be garbage collected. These destroyed instances are passed to an `ObjectWatcher`, which holds weak references to them. You can also watch any instance that is no longer needed, for example a detached view, a destroyed presenter, etc.

If the weak references aren't cleared after waiting 5 seconds and running the garbage collector, the watched instances are considered *retained*, and potentially leaking.

### Dumping the heap

When the number of retained instances reaches a threshold, LeakCanary dumps the Java heap into a `.hprof` file stored onto the Android file system. The default threshold is 5 retained instances when the app is visible, 1 otherwise.
When the number of retained instances reaches a threshold, LeakCanary dumps the Java heap into a `.hprof` file stored onto the Android file system. The default threshold is 5 retained instances when the app is visible, and 1 retained instance when the app is not visible.

### Analyzing the heap

LeakCanary parses the `.hprof` file and finds the chain of references that prevents retained instances from being garbage collected: the **leak trace**. Leak trace is another name for the *shortest strong reference path from garbage collection roots to retained instances*. Once the leak trace is determined, LeakCanary uses its built-in knowledge of the Android framework to deduct which instances in the leak trace are leaking (see below [How do I fix a memory leak?](#how-do-i-fix-a-memory-leak)).
LeakCanary parses the `.hprof` file using [Shark](shark.md) and finds the chain of references that prevents retained instances from being garbage collected: the **leak trace**. Leak trace is another name for the *shortest strong reference path from garbage collection roots to a retained object*. Once the leak trace is determined, LeakCanary uses its built-in knowledge of the Android framework to deduct which instances in the leak trace are leaking (see below [How do I fix a memory leak?](#how-do-i-fix-a-memory-leak)).

### Grouping leaks

Expand Down Expand Up @@ -113,7 +113,7 @@ At the top of the leak trace is a garbage-collection (GC) root. GC roots are spe
is true)
```

At the bottom of the leak trace is the leaking instance. This instance was passed to `RefWatcher.watch()` to confirm it would be garbage collected, and it ended up not being garbage collected which triggered LeakCanary.
At the bottom of the leak trace is the leaking instance. This instance was passed to [AppWatcher.objectWatcher](/api/leakcanary-object-watcher-android/leakcanary/-app-watcher/object-watcher/) to confirm it would be garbage collected, and it ended up not being garbage collected which triggered LeakCanary.

### Chain of references

Expand Down Expand Up @@ -143,17 +143,19 @@ The chain of references from the GC root to the leaking instance is what is prev
│ View.mWindowAttachCount=1
```

LeakCanary runs heuristics to determine the lifecycle state of the nodes of the leak trace, and therefore whether they are leaking or not. For example, if a view has `View#mAttachInfo = null` and `mParent != null` then it is detached yet has a parent, so that view is probably leaking. In the leak trace, for each node you'll see `Leaking: YES / NO / UNKNOWN` with an explanation in parenthesis. LeakCanary can also surface extra information about the state of a node, e.g. `View.mWindowAttachCount=1`. You can customize this behavior and add your own heuristics by updating `LeakCanary.Config.leakTraceInspectors`.
LeakCanary runs heuristics to determine the lifecycle state of the nodes of the leak trace, and therefore whether they are leaking or not. For example, if a view has `View#mAttachInfo = null` and `mParent != null` then it is detached yet has a parent, so that view is probably leaking. In the leak trace, for each node you'll see `Leaking: YES / NO / UNKNOWN` with an explanation in parenthesis. LeakCanary can also surface extra information about the state of a node, e.g. `View.mWindowAttachCount=1`. LeakCanary comes with a set of default heuristics ([AndroidObjectInspectors](/api/shark-android/shark/-android-object-inspectors/), you can add your own heuristics by updating [LeakCanary.Config.objectInspectors](/api/leakcanary-android-core/leakcanary/-leak-canary/-config/object-inspectors/).

### Narrowing down the cause of a leak

```
├─ leakcanary.internal.InternalLeakCanary
│ Leaking: NO (it's a GC root and a class is never leaking)
│ ↓ static InternalLeakCanary.application
├─ android.provider.FontsContract
│ Leaking: NO (ExampleApplication↓ is not leaking and a class is never leaking)
│ GC Root: System class
│ ↓ static FontsContract.sContext
├─ com.example.leakcanary.ExampleApplication
│ Leaking: NO (Application is a singleton)
│ ExampleApplication does not wrap an activity context
│ ↓ ExampleApplication.leakedViews
│ ~~~~~~~~~~~
├─ java.util.ArrayList
Expand All @@ -162,13 +164,13 @@ LeakCanary runs heuristics to determine the lifecycle state of the nodes of the
│ ~~~~~~~~~~~
├─ java.lang.Object[]
│ Leaking: UNKNOWN
│ ↓ array Object[].[0]
│ ↓ array Object[].[1]
│ ~~~
├─ android.widget.TextView
│ Leaking: YES (View detached and has parent)
│ Leaking: YES (View.mContext references a destroyed activity)
│ ↓ TextView.mContext
╰→ com.example.leakcanary.MainActivity
​ Leaking: YES (RefWatcher was watching this and MainActivity#mDestroyed is true)
​ Leaking: YES (TextView↑ is leaking and Activity#mDestroyed is true and ObjectWatcher was watching this)
```

If a node is not leaking, then any prior reference that points to it is not the source of the leak, and also not leaking. Similarly, if a node is leaking then any node down the leak trace is also leaking. From that, we can deduce that the leak is caused by a reference that is after the last `Leaking: NO` and before the first `Leaking: YES`.
Expand All @@ -190,7 +192,7 @@ In this example, the last `Leaking: NO` is on `com.example.leakcanary.ExampleApp
...
```

Looking at the [source](https://github.com/square/leakcanary/blob/master/leakcanary-sample/src/main/java/com/example/leakcanary/ExampleApplication.kt#L23), we can see that `ExampleApplication` has a list field:
Looking at the [source](https://github.com/square/leakcanary/blob/master/leakcanary-android-sample/src/main/java/com/example/leakcanary/ExampleApplication.kt#L23), we can see that `ExampleApplication` has a list field:

```
open class ExampleApplication : Application() {
Expand Down
76 changes: 42 additions & 34 deletions docs/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

If you think a recipe might be missing or you're not sure that what you're trying to achieve is possible with the current APIs, please [file an issue](https://github.com/square/leakcanary/issues/new/choose). Your feedback help us make LeakCanary better for the entire community.

## Configuring ObjectWatcher Android
## Configuring AppWatcher in `object-watcher-android`

ObjectWatcher Android can be configured by replacing `AppWatcher.config`:
AppWatcher is in charge of detecting retained objects. Its configuration can be updated at any time by replacing `AppWatcher.config`:
```kotlin
class DebugExampleApplication : ExampleApplication() {

Expand Down Expand Up @@ -55,7 +55,7 @@ dependencies {

In your leak reporting code:
```kotlin
val retainedInstanceCount = AppWatcher.objectWatcher.retainedKeys.size
val retainedInstanceCount = AppWatcher.objectWatcher.retainedObjectCount
```

## Running LeakCanary in instrumentation tests
Expand All @@ -82,7 +82,7 @@ android {
Run the instrumentation tests:

```
./gradlew leakcanary-sample:connectedCheck
./gradlew leakcanary-android-sample:connectedCheck
```

You can extend `FailTestOnLeakRunListener` to customize the behavior.
Expand Down Expand Up @@ -121,15 +121,15 @@ You can change the default behavior to upload the analysis result to a server of
Create a custom `AnalysisResultListener` that delegates to the default:

```kotlin
class LeakUploader : AnalysisResultListener {
override fun invoke(
application: Application,
heapAnalysis: HeapAnalysis
) {
class LeakUploader : OnHeapAnalyzedListener {

val defaultListener = DefaultOnHeapAnalyzedListener.create()

override fun onHeapAnalyzed(heapAnalysis: HeapAnalysis) {
TODO("Upload heap analysis to server")

// Delegate to default behavior (notification and saving result)
DefaultAnalysisResultListener(application, heapAnalysis)
defaultListener.onHeapAnalyzed(heapAnalysis)
}
}
```
Expand All @@ -141,57 +141,65 @@ class DebugExampleApplication : ExampleApplication() {

override fun onCreate() {
super.onCreate()
LeakCanary.config = LeakCanary.config.copy(analysisResultListener = LeakUploader())
LeakCanary.config = LeakCanary.config.copy(onHeapAnalyzedListener = LeakUploader())
}
}
```


## Identifying 3rd party leaks as "won't fix"
## Matching known library leaks

Set `exclusionsFactory` on the LeakCanary config to a `ExclusionsFactory` that delegates to the default one and then and add custom exclusions:
Set [LeakCanary.Config.referenceMatchers](/api/leakcanary-android-core/leakcanary/-leak-canary/-config/reference-matchers/) to a list that builds on top of [AndroidReferenceMatchers.appDefaults](/api/shark-android/shark/-android-reference-matchers/app-defaults/):

```kotlin
class DebugExampleApplication : ExampleApplication() {

override fun onCreate() {
super.onCreate()
LeakCanary.config = LeakCanary.config.copy(exclusionsFactory = { hprofParser ->
val defaultFactory = AndroidExcludedRefs.exclusionsFactory(AndroidExcludedRefs.appDefaults)
val appDefaults = defaultFactory(hprofParser)
val customExclusion = Exclusion(
type = StaticFieldExclusion("com.thirdparty.SomeSingleton", "sContext"),
status = Exclusion.Status.WONT_FIX_LEAK,
reason = "SomeSingleton in library X has a static field leaking a context."
)
appDefaults + customExclusion
})
LeakCanary.config = LeakCanary.config.copy(
referenceMatchers = AndroidReferenceMatchers.appDefaults +
AndroidReferenceMatchers.staticFieldLeak(
className = "com.samsing.SomeSingleton",
fieldName = "sContext",
description = "SomeSingleton has a static field leaking a context.",
patternApplies = {
manufacturer == "Samsing" && sdkInt == 26
}
)
)
}
}
```

## Identifying leaking instances and labeling instances
## Identifying leaking objects and labeling objects

```kotlin
class DebugExampleApplication : ExampleApplication() {

override fun onCreate() {
super.onCreate()
val customLabeler: Labeler = { parser, node ->
listOf("Heap dump object id is ${node.instance}")
val addObjectIdLabel = ObjectInspector { reporter ->
reporter.addLabel("Heap dump object id is ${reporter.heapObject.objectId}")
}
val labelers = AndroidLabelers.defaultAndroidLabelers(this) + customLabeler

val customInspector: LeakInspector = { parser, node ->
with(parser) {
if (node.instance.objectRecord.isInstanceOf("com.example.MySingleton")) {
LeakNodeStatus.notLeaking("MySingleton is a singleton")
} else LeakNodeStatus.unknown()
val singletonsInspector =
AppSingletonInspector("com.example.MySingleton", "com.example.OtherSingleton")

val mmvmInspector = ObjectInspector { reporter ->
reporter.whenInstanceOf("com.mmvm.SomeViewModel") { instance ->
val destroyedField = instance["com.mmvm.SomeViewModel", "destroyed"]!!
if (destroyedField.value.asBoolean!!) {
reportLeaking("SomeViewModel.destroyed is true")
} else {
reportNotLeaking("SomeViewModel.destroyed is false")
}
}
}
val leakInspectors = AndroidLeakInspectors.defaultAndroidInspectors() + customInspector

LeakCanary.config = LeakCanary.config.copy(labelers = labelers, leakInspectors = leakInspectors)
LeakCanary.config = LeakCanary.config.copy(
objectInspectors = AndroidObjectInspectors.appDefaults +
listOf(addObjectIdLabel, singletonsInspector, mmvmInspector)
)
}
}
```
Expand Down
Loading

0 comments on commit 2f44a2d

Please sign in to comment.