diff --git a/.gitignore b/.gitignore
index db24b30..6a7ba0d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@
/captures
**/.externalNativeBuild/
.idea/
+.app/build
diff --git a/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/Main.java b/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/Main.java
index 202764e..8fe36e7 100644
--- a/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/Main.java
+++ b/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/Main.java
@@ -9,5 +9,6 @@
public class Main {
public static void main(String[] args) throws IOException {
+
}
}
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..829d089
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,16 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion rootProject.ext.compileSdkVersion
+ buildToolsVersion rootProject.ext.buildToolsVersion
+ defaultConfig {
+ minSdkVersion rootProject.ext.minSdkVersion
+ targetSdkVersion rootProject.ext.targetSdkVersion
+ }
+}
+
+dependencies {
+ compile 'com.squareup.haha:haha:2.0.4'
+ debugCompile 'com.squareup.leakcanary:leakcanary-android:1.6.3'
+ releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.3'
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..e5fa4da
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/android/chap04/AnalysisResult.java b/app/src/main/java/com/android/chap04/AnalysisResult.java
new file mode 100644
index 0000000..8cdafb1
--- /dev/null
+++ b/app/src/main/java/com/android/chap04/AnalysisResult.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2015 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.chap04;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import java.io.Serializable;
+
+public final class AnalysisResult implements Serializable {
+
+ public static final long RETAINED_HEAP_SKIPPED = -1;
+
+ public static @NonNull AnalysisResult noLeak(String className, long analysisDurationMs) {
+ return new AnalysisResult(false, false, className, null, null, 0, analysisDurationMs);
+ }
+
+ public static @NonNull AnalysisResult leakDetected(boolean excludedLeak,
+ @NonNull String className,
+ @NonNull LeakTrace leakTrace, long retainedHeapSize, long analysisDurationMs) {
+ return new AnalysisResult(true, excludedLeak, className, leakTrace, null, retainedHeapSize,
+ analysisDurationMs);
+ }
+
+ public static @NonNull AnalysisResult failure(@NonNull Throwable failure,
+ long analysisDurationMs) {
+ return new AnalysisResult(false, false, null, null, failure, 0, analysisDurationMs);
+ }
+
+ /** True if a leak was found in the heap dump. */
+ public final boolean leakFound;
+
+ /**
+ * True if {@link #leakFound} is true and the only path to the leaking reference is
+ * through excluded references. Usually, that means you can safely ignore this report.
+ */
+ public final boolean excludedLeak;
+
+ /**
+ * Class name of the object that leaked, null if {@link #failure} is not null.
+ * The class name format is the same as what would be returned by {@link Class#getName()}.
+ */
+ @Nullable public final String className;
+
+ /**
+ * Shortest path to GC roots for the leaking object if {@link #leakFound} is true, null
+ * otherwise. This can be used as a unique signature for the leak.
+ */
+ @Nullable public final LeakTrace leakTrace;
+
+ /** Null unless the analysis failed. */
+ @Nullable public final Throwable failure;
+
+ /**
+ * The number of bytes which would be freed if all references to the leaking object were
+ * released. {@link #RETAINED_HEAP_SKIPPED} if the retained heap size was not computed. 0 if
+ * {@link #leakFound} is false.
+ */
+ public final long retainedHeapSize;
+
+ /** Total time spent analyzing the heap. */
+ public final long analysisDurationMs;
+
+ /**
+ *
Creates a new {@link RuntimeException} with a fake stack trace that maps the leak trace.
+ *
+ *
Leak traces uniquely identify memory leaks, much like stack traces uniquely identify
+ * exceptions.
+ *
+ *
This method enables you to upload leak traces as stack traces to your preferred
+ * exception reporting tool and benefit from the grouping and counting these tools provide out
+ * of the box. This also means you can track all leaks instead of relying on individuals
+ * reporting them when they happen.
+ *
+ *
The following example leak trace:
+ *
+ * * com.foo.WibbleActivity has leaked:
+ * * GC ROOT static com.foo.Bar.qux
+ * * references com.foo.Quz.context
+ * * leaks com.foo.WibbleActivity instance
+ *
+ *
+ * Will turn into an exception with the following stacktrace:
+ *
+ * java.lang.RuntimeException: com.foo.WibbleActivity leak from com.foo.Bar (holder=CLASS,
+ * type=STATIC_FIELD)
+ * at com.foo.Bar.qux(Bar.java:42)
+ * at com.foo.Quz.context(Quz.java:42)
+ * at com.foo.WibbleActivity.leaking(WibbleActivity.java:42)
+ *
+ */
+ public @NonNull RuntimeException leakTraceAsFakeException() {
+ if (!leakFound) {
+ throw new UnsupportedOperationException(
+ "leakTraceAsFakeException() can only be called when leakFound is true");
+ }
+ LeakTraceElement firstElement = leakTrace.elements.get(0);
+ String rootSimpleName = classSimpleName(firstElement.className);
+ String leakSimpleName = classSimpleName(className);
+
+ String exceptionMessage = leakSimpleName
+ + " leak from "
+ + rootSimpleName
+ + " (holder="
+ + firstElement.holder
+ + ", type="
+ + firstElement.type
+ + ")";
+ RuntimeException exception = new RuntimeException(exceptionMessage);
+
+ StackTraceElement[] stackTrace = new StackTraceElement[leakTrace.elements.size()];
+ int i = 0;
+ for (LeakTraceElement element : leakTrace.elements) {
+ String methodName = element.referenceName != null ? element.referenceName : "leaking";
+ String file = classSimpleName(element.className) + ".java";
+ stackTrace[i] = new StackTraceElement(element.className, methodName, file, 42);
+ i++;
+ }
+ exception.setStackTrace(stackTrace);
+ return exception;
+ }
+
+ private AnalysisResult(boolean leakFound, boolean excludedLeak, String className,
+ LeakTrace leakTrace, Throwable failure, long retainedHeapSize, long analysisDurationMs) {
+ this.leakFound = leakFound;
+ this.excludedLeak = excludedLeak;
+ this.className = className;
+ this.leakTrace = leakTrace;
+ this.failure = failure;
+ this.retainedHeapSize = retainedHeapSize;
+ this.analysisDurationMs = analysisDurationMs;
+ }
+
+ private String classSimpleName(String className) {
+ int separator = className.lastIndexOf('.');
+ return separator == -1 ? className : className.substring(separator + 1);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/android/chap04/HahaHelper.java b/app/src/main/java/com/android/chap04/HahaHelper.java
new file mode 100644
index 0000000..c2b2b28
--- /dev/null
+++ b/app/src/main/java/com/android/chap04/HahaHelper.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2015 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.chap04;
+
+import com.squareup.haha.perflib.ArrayInstance;
+import com.squareup.haha.perflib.ClassInstance;
+import com.squareup.haha.perflib.ClassObj;
+import com.squareup.haha.perflib.Instance;
+import com.squareup.haha.perflib.Type;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.nio.charset.Charset;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import static com.android.chap04.Preconditions.checkNotNull;
+import static java.util.Arrays.asList;
+
+public final class HahaHelper {
+
+ private static final Set WRAPPER_TYPES = new HashSet<>(
+ asList(Boolean.class.getName(), Character.class.getName(), Float.class.getName(),
+ Double.class.getName(), Byte.class.getName(), Short.class.getName(),
+ Integer.class.getName(), Long.class.getName()));
+
+ static String threadName(Instance holder) {
+ List values = classInstanceValues(holder);
+ Object nameField = fieldValue(values, "name");
+ if (nameField == null) {
+ // Sometimes we can't find the String at the expected memory address in the heap dump.
+ // See https://github.com/square/leakcanary/issues/417 .
+ return "Thread name not available";
+ }
+ return asString(nameField);
+ }
+
+ static boolean extendsThread(ClassObj clazz) {
+ boolean extendsThread = false;
+ ClassObj parentClass = clazz;
+ while (parentClass.getSuperClassObj() != null) {
+ if (parentClass.getClassName().equals(Thread.class.getName())) {
+ extendsThread = true;
+ break;
+ }
+ parentClass = parentClass.getSuperClassObj();
+ }
+ return extendsThread;
+ }
+
+ /**
+ * This returns a string representation of any object or value passed in.
+ */
+ static String valueAsString(Object value) {
+ String stringValue;
+ if (value == null) {
+ stringValue = "null";
+ } else if (value instanceof ClassInstance) {
+ String valueClassName = ((ClassInstance) value).getClassObj().getClassName();
+ if (valueClassName.equals(String.class.getName())) {
+ stringValue = '"' + asString(value) + '"';
+ } else {
+ stringValue = value.toString();
+ }
+ } else {
+ stringValue = value.toString();
+ }
+ return stringValue;
+ }
+
+ /** Given a string instance from the heap dump, this returns its actual string value. */
+ static String asString(Object stringObject) {
+ checkNotNull(stringObject, "stringObject");
+ Instance instance = (Instance) stringObject;
+ List values = classInstanceValues(instance);
+
+ Integer count = fieldValue(values, "count");
+ checkNotNull(count, "count");
+ if (count == 0) {
+ return "";
+ }
+
+ Object value = fieldValue(values, "value");
+ checkNotNull(value, "value");
+
+ Integer offset;
+ ArrayInstance array;
+ if (isCharArray(value)) {
+ array = (ArrayInstance) value;
+
+ offset = 0;
+ // < API 23
+ // As of Marshmallow, substrings no longer share their parent strings' char arrays
+ // eliminating the need for String.offset
+ // https://android-review.googlesource.com/#/c/83611/
+ if (hasField(values, "offset")) {
+ offset = fieldValue(values, "offset");
+ checkNotNull(offset, "offset");
+ }
+
+ char[] chars = array.asCharArray(offset, count);
+ return new String(chars);
+ } else if (isByteArray(value)) {
+ // In API 26, Strings are now internally represented as byte arrays.
+ array = (ArrayInstance) value;
+
+ // HACK - remove when HAHA's perflib is updated to https://goo.gl/Oe7ZwO.
+ try {
+ Method asRawByteArray =
+ ArrayInstance.class.getDeclaredMethod("asRawByteArray", int.class, int.class);
+ asRawByteArray.setAccessible(true);
+ byte[] rawByteArray = (byte[]) asRawByteArray.invoke(array, 0, count);
+ return new String(rawByteArray, Charset.forName("UTF-8"));
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ } else {
+ throw new UnsupportedOperationException("Could not find char array in " + instance);
+ }
+ }
+
+ public static boolean isPrimitiveWrapper(Object value) {
+ if (!(value instanceof ClassInstance)) {
+ return false;
+ }
+ return WRAPPER_TYPES.contains(((ClassInstance) value).getClassObj().getClassName());
+ }
+
+ public static boolean isPrimitiveOrWrapperArray(Object value) {
+ if (!(value instanceof ArrayInstance)) {
+ return false;
+ }
+ ArrayInstance arrayInstance = (ArrayInstance) value;
+ if (arrayInstance.getArrayType() != Type.OBJECT) {
+ return true;
+ }
+ return WRAPPER_TYPES.contains(arrayInstance.getClassObj().getClassName());
+ }
+
+ private static boolean isCharArray(Object value) {
+ return value instanceof ArrayInstance && ((ArrayInstance) value).getArrayType() == Type.CHAR;
+ }
+
+ private static boolean isByteArray(Object value) {
+ return value instanceof ArrayInstance && ((ArrayInstance) value).getArrayType() == Type.BYTE;
+ }
+
+ static List classInstanceValues(Instance instance) {
+ ClassInstance classInstance = (ClassInstance) instance;
+ return classInstance.getValues();
+ }
+
+ @SuppressWarnings({ "unchecked", "TypeParameterUnusedInFormals" })
+ static T fieldValue(List values, String fieldName) {
+ for (ClassInstance.FieldValue fieldValue : values) {
+ if (fieldValue.getField().getName().equals(fieldName)) {
+ return (T) fieldValue.getValue();
+ }
+ }
+ throw new IllegalArgumentException("Field " + fieldName + " does not exists");
+ }
+
+ static boolean hasField(List values, String fieldName) {
+ for (ClassInstance.FieldValue fieldValue : values) {
+ if (fieldValue.getField().getName().equals(fieldName)) {
+ //noinspection unchecked
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private HahaHelper() {
+ throw new AssertionError();
+ }
+}
diff --git a/app/src/main/java/com/android/chap04/HeapAnalyzer.java b/app/src/main/java/com/android/chap04/HeapAnalyzer.java
new file mode 100644
index 0000000..fc1b584
--- /dev/null
+++ b/app/src/main/java/com/android/chap04/HeapAnalyzer.java
@@ -0,0 +1,534 @@
+/*
+ * Copyright (C) 2015 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.chap04;
+
+import android.support.annotation.NonNull;
+
+import com.squareup.haha.perflib.ArrayInstance;
+import com.squareup.haha.perflib.ClassInstance;
+import com.squareup.haha.perflib.ClassObj;
+import com.squareup.haha.perflib.Field;
+import com.squareup.haha.perflib.HprofParser;
+import com.squareup.haha.perflib.Instance;
+import com.squareup.haha.perflib.RootObj;
+import com.squareup.haha.perflib.RootType;
+import com.squareup.haha.perflib.Snapshot;
+import com.squareup.haha.perflib.Type;
+import com.squareup.haha.perflib.io.HprofBuffer;
+import com.squareup.haha.perflib.io.MemoryMappedFileBuffer;
+import com.squareup.leakcanary.AnalyzerProgressListener;
+import com.squareup.leakcanary.ExcludedRefs;
+
+import gnu.trove.THashMap;
+import gnu.trove.TObjectProcedure;
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static android.os.Build.VERSION.SDK_INT;
+import static android.os.Build.VERSION_CODES.N_MR1;
+import static com.android.chap04.AnalysisResult.failure;
+import static com.android.chap04.AnalysisResult.leakDetected;
+import static com.android.chap04.AnalysisResult.noLeak;
+import static com.android.chap04.HahaHelper.asString;
+import static com.android.chap04.HahaHelper.classInstanceValues;
+import static com.android.chap04.HahaHelper.extendsThread;
+import static com.android.chap04.HahaHelper.fieldValue;
+import static com.android.chap04.HahaHelper.hasField;
+import static com.android.chap04.HahaHelper.threadName;
+import static com.android.chap04.HahaHelper.valueAsString;
+import static com.android.chap04.LeakTraceElement.Holder.ARRAY;
+import static com.android.chap04.LeakTraceElement.Holder.CLASS;
+import static com.android.chap04.LeakTraceElement.Holder.OBJECT;
+import static com.android.chap04.LeakTraceElement.Holder.THREAD;
+import static com.android.chap04.LeakTraceElement.Type.ARRAY_ENTRY;
+import static com.android.chap04.LeakTraceElement.Type.INSTANCE_FIELD;
+import static com.android.chap04.LeakTraceElement.Type.STATIC_FIELD;
+import static com.android.chap04.Reachability.REACHABLE;
+import static com.android.chap04.Reachability.UNKNOWN;
+import static com.android.chap04.Reachability.UNREACHABLE;
+import static com.squareup.leakcanary.AnalyzerProgressListener.Step.BUILDING_LEAK_TRACE;
+import static com.squareup.leakcanary.AnalyzerProgressListener.Step.COMPUTING_BITMAP_SIZE;
+import static com.squareup.leakcanary.AnalyzerProgressListener.Step.COMPUTING_DOMINATORS;
+import static com.squareup.leakcanary.AnalyzerProgressListener.Step.DEDUPLICATING_GC_ROOTS;
+import static com.squareup.leakcanary.AnalyzerProgressListener.Step.FINDING_LEAKING_REF;
+import static com.squareup.leakcanary.AnalyzerProgressListener.Step.FINDING_SHORTEST_PATH;
+import static com.squareup.leakcanary.AnalyzerProgressListener.Step.PARSING_HEAP_DUMP;
+import static com.squareup.leakcanary.AnalyzerProgressListener.Step.READING_HEAP_DUMP_FILE;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+
+/**
+ * Analyzes heap dumps generated by a {link RefWatcher} to verify if suspected leaks are real.
+ */
+public final class HeapAnalyzer {
+
+ private static final String ANONYMOUS_CLASS_NAME_PATTERN = "^.+\\$\\d+$";
+
+ private final ExcludedRefs excludedRefs;
+// private final AnalyzerProgressListener listener;
+// private final List reachabilityInspectors;
+
+ public HeapAnalyzer() {
+ this.excludedRefs = ExcludedRefs.builder().build();
+// this.listener = AnalyzerProgressListener.NONE;
+// this.reachabilityInspectors = null;
+ }
+
+ /**
+ * @deprecated Use {@link #HeapAnalyzer(ExcludedRefs, AnalyzerProgressListener, List)}.
+ */
+ @Deprecated
+ public HeapAnalyzer(@NonNull ExcludedRefs excludedRefs) {
+ this(excludedRefs, AnalyzerProgressListener.NONE,
+ Collections.>emptyList());
+ }
+
+ public HeapAnalyzer(@NonNull ExcludedRefs excludedRefs,
+ @NonNull AnalyzerProgressListener listener,
+ @NonNull List> reachabilityInspectorClasses) {
+ this.excludedRefs = excludedRefs;
+// this.listener = listener;
+
+// this.reachabilityInspectors = new ArrayList<>();
+// for (Class extends Reachability.Inspector> reachabilityInspectorClass
+// : reachabilityInspectorClasses) {
+// try {
+// Constructor extends Reachability.Inspector> defaultConstructor =
+// reachabilityInspectorClass.getDeclaredConstructor();
+// reachabilityInspectors.add(defaultConstructor.newInstance());
+// } catch (Exception e) {
+// throw new RuntimeException(e);
+// }
+// }
+ }
+
+ public @NonNull List findTrackedReferences(@NonNull File heapDumpFile) {
+ if (!heapDumpFile.exists()) {
+ throw new IllegalArgumentException("File does not exist: " + heapDumpFile);
+ }
+ try {
+ HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
+ HprofParser parser = new HprofParser(buffer);
+ Snapshot snapshot = parser.parse();
+ deduplicateGcRoots(snapshot);
+
+ ClassObj refClass = snapshot.findClass(KeyedWeakReference.class.getName());
+ List references = new ArrayList<>();
+ for (Instance weakRef : refClass.getInstancesList()) {
+ List values = classInstanceValues(weakRef);
+ String key = asString(fieldValue(values, "key"));
+ String name =
+ hasField(values, "name") ? asString(fieldValue(values, "name")) : "(No name field)";
+ Instance instance = fieldValue(values, "referent");
+ if (instance != null) {
+ String className = getClassName(instance);
+ List fields = describeFields(instance);
+ references.add(new TrackedReference(key, name, className, fields));
+ }
+ }
+ return references;
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Calls {@link #checkForLeak(File, String, boolean)} with computeRetainedSize set to true.
+ *
+ * @deprecated Use {@link #checkForLeak(File, String, boolean)} instead.
+ */
+// @Deprecated
+// public @NonNull
+// AnalysisResult checkForLeak(@NonNull File heapDumpFile,
+// @NonNull String referenceKey) {
+// return checkForLeak(heapDumpFile, referenceKey, true);
+// }
+//
+// /**
+// * Searches the heap dump for a {@link KeyedWeakReference} instance with the corresponding key,
+// * and then computes the shortest strong reference path from that instance to the GC roots.
+// */
+// public @NonNull AnalysisResult checkForLeak(@NonNull File heapDumpFile,
+// @NonNull String referenceKey,
+// boolean computeRetainedSize) {
+// long analysisStartNanoTime = System.nanoTime();
+//
+// if (!heapDumpFile.exists()) {
+// Exception exception = new IllegalArgumentException("File does not exist: " + heapDumpFile);
+// return failure(exception, since(analysisStartNanoTime));
+// }
+//
+// try {
+// listener.onProgressUpdate(READING_HEAP_DUMP_FILE);
+// HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
+// HprofParser parser = new HprofParser(buffer);
+// listener.onProgressUpdate(PARSING_HEAP_DUMP);
+// Snapshot snapshot = parser.parse();
+// listener.onProgressUpdate(DEDUPLICATING_GC_ROOTS);
+// deduplicateGcRoots(snapshot);
+// listener.onProgressUpdate(FINDING_LEAKING_REF);
+// Instance leakingRef = findLeakingReference(referenceKey, snapshot);
+//
+// // False alarm, weak reference was cleared in between key check and heap dump.
+// if (leakingRef == null) {
+// String className = leakingRef.getClassObj().getClassName();
+// return noLeak(className, since(analysisStartNanoTime));
+// }
+// return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, computeRetainedSize);
+// } catch (Throwable e) {
+// return failure(e, since(analysisStartNanoTime));
+// }
+// }
+
+ /**
+ * Pruning duplicates reduces memory pressure from hprof bloat added in Marshmallow.
+ */
+ void deduplicateGcRoots(Snapshot snapshot) {
+ // THashMap has a smaller memory footprint than HashMap.
+ final THashMap uniqueRootMap = new THashMap<>();
+
+ final Collection gcRoots = snapshot.getGCRoots();
+ for (RootObj root : gcRoots) {
+ String key = generateRootKey(root);
+ if (!uniqueRootMap.containsKey(key)) {
+ uniqueRootMap.put(key, root);
+ }
+ }
+
+ // Repopulate snapshot with unique GC roots.
+ gcRoots.clear();
+ uniqueRootMap.forEach(new TObjectProcedure() {
+ @Override public boolean execute(String key) {
+ return gcRoots.add(uniqueRootMap.get(key));
+ }
+ });
+ }
+
+ private String generateRootKey(RootObj root) {
+ return String.format("%s@0x%08x", root.getRootType().getName(), root.getId());
+ }
+
+ private Instance findLeakingReference(String key, Snapshot snapshot) {
+ 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 keysFound = new ArrayList<>();
+ for (Instance instance : refClass.getInstancesList()) {
+ List values = classInstanceValues(instance);
+ Object keyFieldValue = fieldValue(values, "key");
+ if (keyFieldValue == null) {
+ keysFound.add(null);
+ continue;
+ }
+ String keyCandidate = asString(keyFieldValue);
+ if (keyCandidate.equals(key)) {
+ return fieldValue(values, "referent");
+ }
+ keysFound.add(keyCandidate);
+ }
+ throw new IllegalStateException(
+ "Could not find weak reference with key " + key + " in " + keysFound);
+ }
+
+ /**
+ * 重写 查询引用链
+ * */
+ public LeakTrace findLeakTrace(Snapshot snapshot, Instance instance) {
+ ShortestPathFinder pathFinder = new ShortestPathFinder(excludedRefs);
+ ShortestPathFinder.Result result = pathFinder.findPath(snapshot, instance);
+ return buildLeakTrace(result.leakingNode);
+ }
+
+// private AnalysisResult findLeakTrace(long analysisStartNanoTime, Snapshot snapshot,
+// Instance leakingRef, boolean computeRetainedSize) {
+//
+// listener.onProgressUpdate(FINDING_SHORTEST_PATH);
+// ShortestPathFinder pathFinder = new ShortestPathFinder(excludedRefs);
+// ShortestPathFinder.Result result = pathFinder.findPath(snapshot, leakingRef);
+//
+// String className = leakingRef.getClassObj().getClassName();
+//
+// // False alarm, no strong reference path to GC Roots.
+// if (result.leakingNode == null) {
+// return noLeak(className, since(analysisStartNanoTime));
+// }
+//
+// listener.onProgressUpdate(BUILDING_LEAK_TRACE);
+// LeakTrace leakTrace = buildLeakTrace(result.leakingNode);
+//
+// long retainedSize;
+// if (computeRetainedSize) {
+//
+// listener.onProgressUpdate(COMPUTING_DOMINATORS);
+// // Side effect: computes retained size.
+// snapshot.computeDominators();
+//
+// Instance leakingInstance = result.leakingNode.instance;
+//
+// retainedSize = leakingInstance.getTotalRetainedSize();
+//
+// // TODO: check O sources and see what happened to android.graphics.Bitmap.mBuffer
+// if (SDK_INT <= N_MR1) {
+// listener.onProgressUpdate(COMPUTING_BITMAP_SIZE);
+// retainedSize += computeIgnoredBitmapRetainedSize(snapshot, leakingInstance);
+// }
+// } else {
+// retainedSize = AnalysisResult.RETAINED_HEAP_SKIPPED;
+// }
+//
+// return leakDetected(result.excludingKnownLeaks, className, leakTrace, retainedSize,
+// since(analysisStartNanoTime));
+// }
+
+ /**
+ * Bitmaps and bitmap byte arrays are sometimes held by native gc roots, so they aren't included
+ * in the retained size because their root dominator is a native gc root.
+ * To fix this, we check if the leaking instance is a dominator for each bitmap instance and then
+ * add the bitmap size.
+ *
+ * From experience, we've found that bitmap created in code (Bitmap.createBitmap()) are correctly
+ * accounted for, however bitmaps set in layouts are not.
+ */
+ private long computeIgnoredBitmapRetainedSize(Snapshot snapshot, Instance leakingInstance) {
+ long bitmapRetainedSize = 0;
+ ClassObj bitmapClass = snapshot.findClass("android.graphics.Bitmap");
+
+ for (Instance bitmapInstance : bitmapClass.getInstancesList()) {
+ if (isIgnoredDominator(leakingInstance, bitmapInstance)) {
+ ArrayInstance mBufferInstance = fieldValue(classInstanceValues(bitmapInstance), "mBuffer");
+ // Native bitmaps have mBuffer set to null. We sadly can't account for them.
+ if (mBufferInstance == null) {
+ continue;
+ }
+ long bufferSize = mBufferInstance.getTotalRetainedSize();
+ long bitmapSize = bitmapInstance.getTotalRetainedSize();
+ // Sometimes the size of the buffer isn't accounted for in the bitmap retained size. Since
+ // the buffer is large, it's easy to detect by checking for bitmap size < buffer size.
+ if (bitmapSize < bufferSize) {
+ bitmapSize += bufferSize;
+ }
+ bitmapRetainedSize += bitmapSize;
+ }
+ }
+ return bitmapRetainedSize;
+ }
+
+ private boolean isIgnoredDominator(Instance dominator, Instance instance) {
+ boolean foundNativeRoot = false;
+ while (true) {
+ Instance immediateDominator = instance.getImmediateDominator();
+ if (immediateDominator instanceof RootObj
+ && ((RootObj) immediateDominator).getRootType() == RootType.UNKNOWN) {
+ // Ignore native roots
+ instance = instance.getNextInstanceToGcRoot();
+ foundNativeRoot = true;
+ } else {
+ instance = immediateDominator;
+ }
+ if (instance == null) {
+ return false;
+ }
+ if (instance == dominator) {
+ return foundNativeRoot;
+ }
+ }
+ }
+
+ private LeakTrace buildLeakTrace(LeakNode leakingNode) {
+ List elements = new ArrayList<>();
+ // We iterate from the leak to the GC root
+ LeakNode node = new LeakNode(null, null, leakingNode, null);
+ while (node != null) {
+ LeakTraceElement element = buildLeakElement(node);
+ if (element != null) {
+ elements.add(0, element);
+ }
+ node = node.parent;
+ }
+
+ List expectedReachability =
+ computeExpectedReachability(elements);
+
+ return new LeakTrace(elements, expectedReachability);
+ }
+
+ private List computeExpectedReachability(
+ List elements) {
+ int lastReachableElement = 0;
+ int lastElementIndex = elements.size() - 1;
+ int firstUnreachableElement = lastElementIndex;
+ // No need to inspect the first and last element. We know the first should be reachable (gc
+ // root) and the last should be unreachable (watched instance).
+// elementLoop:
+// for (int i = 1; i < lastElementIndex; i++) {
+// LeakTraceElement element = elements.get(i);
+//
+// for (Reachability.Inspector reachabilityInspector : reachabilityInspectors) {
+// Reachability reachability = reachabilityInspector.expectedReachability(element);
+// if (reachability == REACHABLE) {
+// lastReachableElement = i;
+// break;
+// } else if (reachability == UNREACHABLE) {
+// firstUnreachableElement = i;
+// break elementLoop;
+// }
+// }
+// }
+
+ List expectedReachability = new ArrayList<>();
+ for (int i = 0; i < elements.size(); i++) {
+ Reachability status;
+ if (i <= lastReachableElement) {
+ status = REACHABLE;
+ } else if (i >= firstUnreachableElement) {
+ status = UNREACHABLE;
+ } else {
+ status = UNKNOWN;
+ }
+ expectedReachability.add(status);
+ }
+ return expectedReachability;
+ }
+
+ private LeakTraceElement buildLeakElement(LeakNode node) {
+ if (node.parent == null) {
+ // Ignore any root node.
+ return null;
+ }
+ Instance holder = node.parent.instance;
+
+ if (holder instanceof RootObj) {
+ return null;
+ }
+ LeakTraceElement.Holder holderType;
+ String className;
+ String extra = null;
+ List leakReferences = describeFields(holder);
+
+ className = getClassName(holder);
+
+ List classHierarchy = new ArrayList<>();
+ classHierarchy.add(className);
+ String rootClassName = Object.class.getName();
+ if (holder instanceof ClassInstance) {
+ ClassObj classObj = holder.getClassObj();
+ while (!(classObj = classObj.getSuperClassObj()).getClassName().equals(rootClassName)) {
+ classHierarchy.add(classObj.getClassName());
+ }
+ }
+
+ if (holder instanceof ClassObj) {
+ holderType = CLASS;
+ } else if (holder instanceof ArrayInstance) {
+ holderType = ARRAY;
+ } else {
+ ClassObj classObj = holder.getClassObj();
+ if (extendsThread(classObj)) {
+ holderType = THREAD;
+ String threadName = threadName(holder);
+ extra = "(named '" + threadName + "')";
+ } else if (className.matches(ANONYMOUS_CLASS_NAME_PATTERN)) {
+ String parentClassName = classObj.getSuperClassObj().getClassName();
+ if (rootClassName.equals(parentClassName)) {
+ holderType = OBJECT;
+ try {
+ // This is an anonymous class implementing an interface. The API does not give access
+ // to the interfaces implemented by the class. We check if it's in the class path and
+ // use that instead.
+ Class> actualClass = Class.forName(classObj.getClassName());
+ Class>[] interfaces = actualClass.getInterfaces();
+ if (interfaces.length > 0) {
+ Class> implementedInterface = interfaces[0];
+ extra = "(anonymous implementation of " + implementedInterface.getName() + ")";
+ } else {
+ extra = "(anonymous subclass of java.lang.Object)";
+ }
+ } catch (ClassNotFoundException ignored) {
+ }
+ } else {
+ holderType = OBJECT;
+ // Makes it easier to figure out which anonymous class we're looking at.
+ extra = "(anonymous subclass of " + parentClassName + ")";
+ }
+ } else {
+ holderType = OBJECT;
+ }
+ }
+ return new LeakTraceElement(node.leakReference, holderType, classHierarchy, extra,
+ node.exclusion, leakReferences);
+ }
+
+ private List describeFields(Instance instance) {
+ List leakReferences = new ArrayList<>();
+ if (instance instanceof ClassObj) {
+ ClassObj classObj = (ClassObj) instance;
+ for (Map.Entry entry : classObj.getStaticFieldValues().entrySet()) {
+ String name = entry.getKey().getName();
+ String stringValue = valueAsString(entry.getValue());
+ leakReferences.add(new LeakReference(STATIC_FIELD, name, stringValue));
+ }
+ } else if (instance instanceof ArrayInstance) {
+ ArrayInstance arrayInstance = (ArrayInstance) instance;
+ if (arrayInstance.getArrayType() == Type.OBJECT) {
+ Object[] values = arrayInstance.getValues();
+ for (int i = 0; i < values.length; i++) {
+ String name = Integer.toString(i);
+ String stringValue = valueAsString(values[i]);
+ leakReferences.add(new LeakReference(ARRAY_ENTRY, name, stringValue));
+ }
+ }
+ } else {
+ ClassObj classObj = instance.getClassObj();
+ for (Map.Entry entry : classObj.getStaticFieldValues().entrySet()) {
+ String name = entry.getKey().getName();
+ String stringValue = valueAsString(entry.getValue());
+ leakReferences.add(new LeakReference(STATIC_FIELD, name, stringValue));
+ }
+ ClassInstance classInstance = (ClassInstance) instance;
+ for (ClassInstance.FieldValue field : classInstance.getValues()) {
+ String name = field.getField().getName();
+ String stringValue = valueAsString(field.getValue());
+ leakReferences.add(new LeakReference(INSTANCE_FIELD, name, stringValue));
+ }
+ }
+ return leakReferences;
+ }
+
+ private String getClassName(Instance instance) {
+ String className;
+ if (instance instanceof ClassObj) {
+ ClassObj classObj = (ClassObj) instance;
+ className = classObj.getClassName();
+ } else if (instance instanceof ArrayInstance) {
+ ArrayInstance arrayInstance = (ArrayInstance) instance;
+ className = arrayInstance.getClassObj().getClassName();
+ } else {
+ ClassObj classObj = instance.getClassObj();
+ className = classObj.getClassName();
+ }
+ return className;
+ }
+
+ private long since(long analysisStartNanoTime) {
+ return NANOSECONDS.toMillis(System.nanoTime() - analysisStartNanoTime);
+ }
+}
diff --git a/app/src/main/java/com/android/chap04/KeyedWeakReference.java b/app/src/main/java/com/android/chap04/KeyedWeakReference.java
new file mode 100644
index 0000000..caf9420
--- /dev/null
+++ b/app/src/main/java/com/android/chap04/KeyedWeakReference.java
@@ -0,0 +1,19 @@
+//
+// Source code recreated from a .class file by IntelliJ IDEA
+// (powered by Fernflower decompiler)
+//
+
+package com.android.chap04;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+
+final class KeyedWeakReference extends WeakReference {
+ public final String key;
+ public final String name;
+
+ KeyedWeakReference(Object referent, String key, String name, ReferenceQueue referenceQueue) {
+ super(Preconditions.checkNotNull(referent, "referent"), (ReferenceQueue)Preconditions.checkNotNull(referenceQueue, "referenceQueue"));
+ this.key = (String)Preconditions.checkNotNull(key, "key");
+ this.name = (String)Preconditions.checkNotNull(name, "name");
+ }
+}
diff --git a/app/src/main/java/com/android/chap04/LeakNode.java b/app/src/main/java/com/android/chap04/LeakNode.java
new file mode 100644
index 0000000..38ca062
--- /dev/null
+++ b/app/src/main/java/com/android/chap04/LeakNode.java
@@ -0,0 +1,23 @@
+//
+// Source code recreated from a .class file by IntelliJ IDEA
+// (powered by Fernflower decompiler)
+//
+
+package com.android.chap04;
+
+import com.squareup.haha.perflib.Instance;
+import com.squareup.leakcanary.Exclusion;
+
+final class LeakNode {
+ final Exclusion exclusion;
+ final Instance instance;
+ final LeakNode parent;
+ final LeakReference leakReference;
+
+ LeakNode(Exclusion exclusion, Instance instance, LeakNode parent, LeakReference leakReference) {
+ this.exclusion = exclusion;
+ this.instance = instance;
+ this.parent = parent;
+ this.leakReference = leakReference;
+ }
+}
diff --git a/app/src/main/java/com/android/chap04/LeakReference.java b/app/src/main/java/com/android/chap04/LeakReference.java
new file mode 100644
index 0000000..675da64
--- /dev/null
+++ b/app/src/main/java/com/android/chap04/LeakReference.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2015 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.chap04;
+import java.io.Serializable;
+
+/**
+ * A single field in a {@link LeakTraceElement}.
+ */
+public final class LeakReference implements Serializable {
+
+ public final LeakTraceElement.Type type;
+ public final String name;
+ public final String value;
+
+ public LeakReference(LeakTraceElement.Type type, String name, String value) {
+ this.type = type;
+ this.name = name;
+ this.value = value;
+ }
+
+ public String getDisplayName() {
+ switch (type) {
+ case ARRAY_ENTRY:
+ return "[" + name + "]";
+ case STATIC_FIELD:
+ case INSTANCE_FIELD:
+ return name;
+ case LOCAL:
+ return "";
+ default:
+ throw new IllegalStateException(
+ "Unexpected type " + type + " name = " + name + " value = " + value);
+ }
+ }
+
+ @Override public String toString() {
+ switch (type) {
+ case ARRAY_ENTRY:
+ case INSTANCE_FIELD:
+ return getDisplayName() + " = " + value;
+ case STATIC_FIELD:
+ return "static " + getDisplayName() + " = " + value;
+ case LOCAL:
+ return getDisplayName();
+ default:
+ throw new IllegalStateException(
+ "Unexpected type " + type + " name = " + name + " value = " + value);
+ }
+ }
+}
diff --git a/app/src/main/java/com/android/chap04/LeakTrace.java b/app/src/main/java/com/android/chap04/LeakTrace.java
new file mode 100644
index 0000000..5fe32a5
--- /dev/null
+++ b/app/src/main/java/com/android/chap04/LeakTrace.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2015 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.chap04;
+import android.support.annotation.NonNull;
+
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * A chain of references that constitute the shortest strong reference path from a leaking instance
+ * to the GC roots. Fixing the leak usually means breaking one of the references in that chain.
+ */
+public final class LeakTrace implements Serializable {
+
+ @NonNull public final List elements;
+ @NonNull public final List expectedReachability;
+
+ LeakTrace(List elements, List expectedReachability) {
+ this.elements = elements;
+ this.expectedReachability = expectedReachability;
+ }
+
+ @Override public String toString() {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < elements.size(); i++) {
+ LeakTraceElement element = elements.get(i);
+ sb.append("* ");
+ if (i != 0) {
+ sb.append("↳ ");
+ }
+ boolean maybeLeakCause = false;
+ Reachability currentReachability = expectedReachability.get(i);
+ if (currentReachability == Reachability.UNKNOWN) {
+ maybeLeakCause = true;
+ } else if (currentReachability == Reachability.REACHABLE) {
+ if (i < elements.size() - 1) {
+ Reachability nextReachability = expectedReachability.get(i + 1);
+ if (nextReachability != Reachability.REACHABLE) {
+ maybeLeakCause = true;
+ }
+ } else {
+ maybeLeakCause = true;
+ }
+ }
+ sb.append(element.toString(maybeLeakCause)).append("\n");
+ }
+ return sb.toString();
+ }
+
+ public @NonNull String toDetailedString() {
+ String string = "";
+ for (LeakTraceElement element : elements) {
+ string += element.toDetailedString();
+ }
+ return string;
+ }
+}
diff --git a/app/src/main/java/com/android/chap04/LeakTraceElement.java b/app/src/main/java/com/android/chap04/LeakTraceElement.java
new file mode 100644
index 0000000..b6e77f9
--- /dev/null
+++ b/app/src/main/java/com/android/chap04/LeakTraceElement.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2015 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.chap04;
+import com.squareup.leakcanary.Exclusion;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import static java.util.Collections.unmodifiableList;
+import static java.util.Locale.US;
+
+import static com.android.chap04.LeakTraceElement.Holder.ARRAY;
+import static com.android.chap04.LeakTraceElement.Holder.CLASS;
+import static com.android.chap04.LeakTraceElement.Holder.THREAD;
+import static com.android.chap04.LeakTraceElement.Type.STATIC_FIELD;
+
+
+/** Represents one reference in the chain of references that holds a leaking object in memory. */
+public final class LeakTraceElement implements Serializable {
+
+ public enum Type {
+ INSTANCE_FIELD, STATIC_FIELD, LOCAL, ARRAY_ENTRY
+ }
+
+ public enum Holder {
+ OBJECT, CLASS, THREAD, ARRAY
+ }
+
+ /**
+ * Information about the reference that points to the next {@link LeakTraceElement} in the leak
+ * chain. Null if this is the last element in the leak trace, ie the leaking object.
+ */
+ public final LeakReference reference;
+
+ /**
+ * @deprecated Use {@link #reference} and {@link LeakReference#getDisplayName()} instead.
+ * Null if this is the last element in the leak trace, ie the leaking object.
+ */
+ @Deprecated
+ public final String referenceName;
+
+ /**
+ * @deprecated Use {@link #reference} and {@link LeakReference#type} instead.
+ * Null if this is the last element in the leak trace, ie the leaking object.
+ */
+ @Deprecated
+ public final Type type;
+
+ public final Holder holder;
+
+ /**
+ * Class hierarchy for that object. The first element is {@link #className}. {@link Object}
+ * is excluded. There is always at least one element.
+ */
+ public final List classHierarchy;
+
+ public final String className;
+
+ /** Additional information, may be null. */
+ public final String extra;
+
+ /** If not null, there was no path that could exclude this element. */
+ public final Exclusion exclusion;
+
+ /** List of all fields (member and static) for that object. */
+ public final List fieldReferences;
+
+ /**
+ * @deprecated Use {@link #fieldReferences} instead.
+ */
+ @Deprecated
+ public final List fields;
+
+ LeakTraceElement(LeakReference reference, Holder holder, List classHierarchy,
+ String extra, Exclusion exclusion, List leakReferences) {
+ this.reference = reference;
+ this.referenceName = reference == null ? null : reference.getDisplayName();
+ this.type = reference == null ? null : reference.type;
+ this.holder = holder;
+ this.classHierarchy = Collections.unmodifiableList(new ArrayList<>(classHierarchy));
+ this.className = classHierarchy.get(0);
+ this.extra = extra;
+ this.exclusion = exclusion;
+ this.fieldReferences = unmodifiableList(new ArrayList<>(leakReferences));
+ List stringFields = new ArrayList<>();
+ for (LeakReference leakReference : leakReferences) {
+ stringFields.add(leakReference.toString());
+ }
+ fields = Collections.unmodifiableList(stringFields);
+ }
+
+ /**
+ * Returns the string value of the first field reference that has the provided referenceName, or
+ * null if no field reference with that name was found.
+ */
+ public String getFieldReferenceValue(String referenceName) {
+ for (LeakReference fieldReference : fieldReferences) {
+ if (fieldReference.name.equals(referenceName)) {
+ return fieldReference.value;
+ }
+ }
+ return null;
+ }
+
+ /** @see #isInstanceOf(String) */
+ public boolean isInstanceOf(Class> expectedClass) {
+ return isInstanceOf(expectedClass.getName());
+ }
+
+ /**
+ * Returns true if this element is an instance of the provided class name, false otherwise.
+ */
+ public boolean isInstanceOf(String expectedClassName) {
+ for (String className : classHierarchy) {
+ if (className.equals(expectedClassName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns {@link #className} without the package.
+ */
+ public String getSimpleClassName() {
+ int separator = className.lastIndexOf('.');
+ if (separator == -1) {
+ return className;
+ } else {
+ return className.substring(separator + 1);
+ }
+ }
+
+ @Override public String toString() {
+ return toString(false);
+ }
+
+ public String toString(boolean maybeLeakCause) {
+ String string = "";
+
+ if (reference != null && reference.type == STATIC_FIELD) {
+ string += "static ";
+ }
+
+ if (holder == ARRAY || holder == THREAD) {
+ string += holder.name().toLowerCase(US) + " ";
+ }
+
+ string += getSimpleClassName();
+
+ if (reference != null) {
+ String referenceName = reference.getDisplayName();
+ if (maybeLeakCause) {
+ referenceName = "!(" + referenceName + ")!";
+ }
+ string += "." + referenceName;
+ }
+
+ if (extra != null) {
+ string += " " + extra;
+ }
+
+ if (exclusion != null) {
+ string += " , matching exclusion " + exclusion.matching;
+ }
+
+ return string;
+ }
+
+ public String toDetailedString() {
+ String string = "* ";
+ if (holder == ARRAY) {
+ string += "Array of";
+ } else if (holder == CLASS) {
+ string += "Class";
+ } else {
+ string += "Instance of";
+ }
+ string += " " + className + "\n";
+ for (LeakReference leakReference : fieldReferences) {
+ string += "| " + leakReference + "\n";
+ }
+ return string;
+ }
+}
diff --git a/app/src/main/java/com/android/chap04/MainActivity.java b/app/src/main/java/com/android/chap04/MainActivity.java
new file mode 100644
index 0000000..9cfbd95
--- /dev/null
+++ b/app/src/main/java/com/android/chap04/MainActivity.java
@@ -0,0 +1,185 @@
+package com.android.chap04;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Bundle;
+import android.os.Handler;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.ImageView;
+
+import com.squareup.haha.perflib.ArrayInstance;
+import com.squareup.haha.perflib.ClassInstance;
+import com.squareup.haha.perflib.ClassObj;
+import com.squareup.haha.perflib.Heap;
+import com.squareup.haha.perflib.HprofParser;
+import com.squareup.haha.perflib.Instance;
+import com.squareup.haha.perflib.Snapshot;
+import com.squareup.haha.perflib.io.HprofBuffer;
+import com.squareup.haha.perflib.io.MemoryMappedFileBuffer;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author zhoujun
+ * @date 19-1-17
+ * @email jun_zhou1@hnair.com
+ */
+public class MainActivity extends Activity {
+
+ public final static String TAG = "hproftest";
+ private static String NAME_DUMP = "hprof_dump.hprof";
+ private String mPath;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ initView();
+ initListener();
+ File storageFile = this.getExternalFilesDir("");
+ if(storageFile != null) {
+ mPath = storageFile.getAbsolutePath() + File.separator + NAME_DUMP;
+ }
+ }
+
+ private void initView() {
+ ImageView iv01 = findViewById(R.id.iv_01);
+ ImageView iv02 = findViewById(R.id.iv_02);
+ ImageView iv03 = findViewById(R.id.iv_03);
+ ImageView iv04 = findViewById(R.id.iv_04);
+
+ Bitmap bitmap01 = BitmapFactory.decodeResource(getResources(), R.drawable.iv02);
+ Bitmap bitmap02 = BitmapFactory.decodeResource(getResources(), R.drawable.iv02);
+ Bitmap bitmap03 = BitmapFactory.decodeResource(getResources(), R.drawable.iv02);
+// Bitmap bitmap04 = BitmapFactory.decodeResource(getResources(), R.drawable.iv02);
+
+ iv01.setImageBitmap(bitmap01);
+ iv02.setImageBitmap(bitmap02);
+ iv03.setImageBitmap(bitmap03);
+// iv04.setImageBitmap(bitmap04);
+ }
+
+ private void initListener() {
+ Button btnDump = findViewById(R.id.btn_dump);
+ Button btnAnalyze = findViewById(R.id.btn_analyze);
+ btnDump.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Log.d(TAG, "mPath = " + mPath);
+ if(TextUtils.isEmpty(mPath)) {
+ return ;
+ }
+ try {
+ android.os.Debug.dumpHprofData(mPath);
+ Log.d(TAG, "create dumpHprofData is ok..");
+ } catch (IOException e) {
+ e.printStackTrace();
+ Log.d(TAG, "create dumpHprofData is failed......");
+ }
+
+ }
+ });
+
+ btnAnalyze.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Log.d(TAG, "ready to analyze file path = " + mPath);
+ if(TextUtils.isEmpty(mPath)) {
+ return ;
+ }
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ HprofBuffer buffer = new MemoryMappedFileBuffer(new File(mPath));
+ HprofParser parser = new HprofParser(buffer);
+ Snapshot snapShot = parser.parse();
+ ClassObj bitmapCls = snapShot.findClass("android.graphics.Bitmap");
+ Collection heapList = snapShot.getHeaps();
+ Log.d(TAG, "size = " + heapList.size());
+ Iterator it = heapList.iterator();
+ while(it.hasNext()) {
+ Heap heap = it.next();
+ String name = heap.getName();
+ int id = heap.getId();
+ Log.d(TAG, "the itemId = " + id + "; name = " + name);
+ // 从heap中获得所有的bitmap实例, 然后只是分析app和default即可.
+ if("app".equals(name) || "default".equals(name)) {
+ analyzeHeap(id, bitmapCls, snapShot);
+ }
+
+ }
+
+ Log.d(TAG, "==>>>>Ok. done..");
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }).start();
+
+ }
+ });
+ }
+
+ private void analyzeHeap(int id, ClassObj bitmapCls, Snapshot snapshot) {
+ Map> instanceMap = new HashMap<>();
+
+ final List bitmapInstances = bitmapCls.getHeapInstances(id);
+ for(Instance instance : bitmapInstances) {
+ Log.d(TAG, "bitmapInstance = " + instance.toString());
+ // 从bitmap实例中获得buffer数组
+ ArrayInstance buffer = HahaHelper.fieldValue(((ClassInstance)instance).getValues(), "mBuffer");
+ if(buffer == null) {
+ continue;
+ }
+ int hashCode = Arrays.hashCode(buffer.getValues());
+ Log.d(TAG, "===>>>instance hashCode = " + hashCode);
+ ArrayList instanceList;
+ if(instanceMap.containsKey(hashCode)) {
+ instanceList = instanceMap.get(hashCode);
+ instanceList.add(instance);
+ } else {
+ instanceList = new ArrayList<>();
+ instanceList.add(instance);
+ }
+ instanceMap.put(hashCode, instanceList);
+ }
+
+ HeapAnalyzer heapAnalyzer = new HeapAnalyzer();
+ for(int key : instanceMap.keySet()) {
+ ArrayList instanceList = instanceMap.get(key);
+ // 假如有大于1个对象
+ if(instanceList.size() > 1) {
+ Integer height = HahaHelper.fieldValue(((ClassInstance)instanceList.get(0)).getValues(), "mHeight");
+ Integer width = HahaHelper.fieldValue(((ClassInstance)instanceList.get(0)).getValues(),"mWidth");
+ Log.e(TAG, "Duplicate pics = " + instanceList.size());
+ Log.e(TAG, "hashcode = " + key);
+ Log.e(TAG, "height = " + height);
+ Log.e(TAG, "width = " + width);
+
+ for(Instance instance : instanceList) {
+ LeakTrace leakTrace = heapAnalyzer.findLeakTrace(snapshot, instance);
+ Log.e(TAG, "引用链: " + leakTrace.toString());
+ }
+ }
+ }
+
+ }
+
+}
diff --git a/app/src/main/java/com/android/chap04/Preconditions.java b/app/src/main/java/com/android/chap04/Preconditions.java
new file mode 100644
index 0000000..52167a0
--- /dev/null
+++ b/app/src/main/java/com/android/chap04/Preconditions.java
@@ -0,0 +1,24 @@
+package com.android.chap04;
+
+/**
+ * @author zhoujun
+ * @date 19-1-23
+ * @email jun_zhou1@hnair.com
+ */
+public class Preconditions {
+ /**
+ * Returns instance unless it's null.
+ *
+ * @throws NullPointerException if instance is null
+ */
+ static T checkNotNull(T instance, String name) {
+ if (instance == null) {
+ throw new NullPointerException(name + " must not be null");
+ }
+ return instance;
+ }
+
+ private Preconditions() {
+ throw new AssertionError();
+ }
+}
diff --git a/app/src/main/java/com/android/chap04/Reachability.java b/app/src/main/java/com/android/chap04/Reachability.java
new file mode 100644
index 0000000..58d4cd3
--- /dev/null
+++ b/app/src/main/java/com/android/chap04/Reachability.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.chap04;
+/** Result returned by {@link Inspector#expectedReachability(LeakTraceElement)}. */
+public enum Reachability {
+ /** The instance was needed and therefore expected to be reachable. */
+ REACHABLE,
+
+ /** The instance was no longer needed and therefore expected to be unreachable. */
+ UNREACHABLE,
+
+ /** No decision can be made about the provided instance. */
+ UNKNOWN;
+
+ /**
+ * Evaluates whether a {@link LeakTraceElement} should be reachable or not.
+ *
+ * Implementations should have a public zero argument constructor as instances will be created
+ * via reflection in the LeakCanary analysis process.
+ */
+ public interface Inspector {
+
+ Reachability expectedReachability(LeakTraceElement element);
+ }
+}
diff --git a/app/src/main/java/com/android/chap04/ShortestPathFinder.java b/app/src/main/java/com/android/chap04/ShortestPathFinder.java
new file mode 100644
index 0000000..36033bd
--- /dev/null
+++ b/app/src/main/java/com/android/chap04/ShortestPathFinder.java
@@ -0,0 +1,297 @@
+//
+// Source code recreated from a .class file by IntelliJ IDEA
+// (powered by Fernflower decompiler)
+//
+
+package com.android.chap04;
+
+import com.android.chap04.LeakTraceElement.Type;
+import com.squareup.haha.perflib.ArrayInstance;
+import com.squareup.haha.perflib.ClassInstance;
+import com.squareup.haha.perflib.ClassObj;
+import com.squareup.haha.perflib.Field;
+import com.squareup.haha.perflib.HahaSpy;
+import com.squareup.haha.perflib.Instance;
+import com.squareup.haha.perflib.RootObj;
+import com.squareup.haha.perflib.RootType;
+import com.squareup.haha.perflib.Snapshot;
+import com.squareup.haha.perflib.ClassInstance.FieldValue;
+import com.squareup.leakcanary.ExcludedRefs;
+import com.squareup.leakcanary.Exclusion;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+
+final class ShortestPathFinder {
+ private final ExcludedRefs excludedRefs;
+ private final Deque toVisitQueue;
+ private final Deque toVisitIfNoPathQueue;
+ private final LinkedHashSet toVisitSet;
+ private final LinkedHashSet toVisitIfNoPathSet;
+ private final LinkedHashSet visitedSet;
+ private boolean canIgnoreStrings;
+
+ ShortestPathFinder(ExcludedRefs excludedRefs) {
+ this.excludedRefs = excludedRefs;
+ this.toVisitQueue = new ArrayDeque();
+ this.toVisitIfNoPathQueue = new ArrayDeque();
+ this.toVisitSet = new LinkedHashSet();
+ this.toVisitIfNoPathSet = new LinkedHashSet();
+ this.visitedSet = new LinkedHashSet();
+ }
+
+ ShortestPathFinder.Result findPath(Snapshot snapshot, Instance leakingRef) {
+ this.clearState();
+ this.canIgnoreStrings = !this.isString(leakingRef);
+ this.enqueueGcRoots(snapshot);
+ boolean excludingKnownLeaks = false;
+ LeakNode leakingNode = null;
+
+ while(!this.toVisitQueue.isEmpty() || !this.toVisitIfNoPathQueue.isEmpty()) {
+ LeakNode node;
+ if (!this.toVisitQueue.isEmpty()) {
+ node = (LeakNode)this.toVisitQueue.poll();
+ } else {
+ node = (LeakNode)this.toVisitIfNoPathQueue.poll();
+ if (node.exclusion == null) {
+ throw new IllegalStateException("Expected node to have an exclusion " + node);
+ }
+
+ excludingKnownLeaks = true;
+ }
+
+ if (node.instance == leakingRef) {
+ leakingNode = node;
+ break;
+ }
+
+ if (!this.checkSeen(node)) {
+ if (node.instance instanceof RootObj) {
+ this.visitRootObj(node);
+ } else if (node.instance instanceof ClassObj) {
+ this.visitClassObj(node);
+ } else if (node.instance instanceof ClassInstance) {
+ this.visitClassInstance(node);
+ } else {
+ if (!(node.instance instanceof ArrayInstance)) {
+ throw new IllegalStateException("Unexpected type for " + node.instance);
+ }
+
+ this.visitArrayInstance(node);
+ }
+ }
+ }
+
+ return new ShortestPathFinder.Result(leakingNode, excludingKnownLeaks);
+ }
+
+ private void clearState() {
+ this.toVisitQueue.clear();
+ this.toVisitIfNoPathQueue.clear();
+ this.toVisitSet.clear();
+ this.toVisitIfNoPathSet.clear();
+ this.visitedSet.clear();
+ }
+
+ private void enqueueGcRoots(Snapshot snapshot) {
+ Iterator var2 = HahaSpy.allGcRoots(snapshot).iterator();
+
+ while(var2.hasNext()) {
+ RootObj rootObj = (RootObj)var2.next();
+ switch(rootObj.getRootType()) {
+ case JAVA_LOCAL:
+ Instance thread = HahaSpy.allocatingThread(rootObj);
+ String threadName = HahaHelper.threadName(thread);
+ Exclusion params = (Exclusion)this.excludedRefs.threadNames.get(threadName);
+ if (params == null || !params.alwaysExclude) {
+ this.enqueue(params, (LeakNode)null, rootObj, (LeakReference)null);
+ }
+ case INTERNED_STRING:
+ case DEBUGGER:
+ case INVALID_TYPE:
+ case UNREACHABLE:
+ case UNKNOWN:
+ case FINALIZING:
+ break;
+ case SYSTEM_CLASS:
+ case VM_INTERNAL:
+ case NATIVE_LOCAL:
+ case NATIVE_STATIC:
+ case THREAD_BLOCK:
+ case BUSY_MONITOR:
+ case NATIVE_MONITOR:
+ case REFERENCE_CLEANUP:
+ case NATIVE_STACK:
+ case JAVA_STATIC:
+ this.enqueue((Exclusion)null, (LeakNode)null, rootObj, (LeakReference)null);
+ break;
+ default:
+ throw new UnsupportedOperationException("Unknown root type:" + rootObj.getRootType());
+ }
+ }
+
+ }
+
+ private boolean checkSeen(LeakNode node) {
+ return !this.visitedSet.add(node.instance);
+ }
+
+ private void visitRootObj(LeakNode node) {
+ RootObj rootObj = (RootObj)node.instance;
+ Instance child = rootObj.getReferredInstance();
+ if (rootObj.getRootType() == RootType.JAVA_LOCAL) {
+ Instance holder = HahaSpy.allocatingThread(rootObj);
+ Exclusion exclusion = null;
+ if (node.exclusion != null) {
+ exclusion = node.exclusion;
+ }
+
+ LeakNode parent = new LeakNode((Exclusion)null, holder, (LeakNode)null, (LeakReference)null);
+ this.enqueue(exclusion, parent, child, new LeakReference(Type.LOCAL, (String)null, (String)null));
+ } else {
+ this.enqueue((Exclusion)null, node, child, (LeakReference)null);
+ }
+
+ }
+
+ private void visitClassObj(LeakNode node) {
+ ClassObj classObj = (ClassObj)node.instance;
+ Map ignoredStaticFields = (Map)this.excludedRefs.staticFieldNameByClassName.get(classObj.getClassName());
+ Iterator var4 = classObj.getStaticFieldValues().entrySet().iterator();
+
+ while(var4.hasNext()) {
+ Entry entry = (Entry)var4.next();
+ Field field = (Field)entry.getKey();
+ if (field.getType() == com.squareup.haha.perflib.Type.OBJECT) {
+ String fieldName = field.getName();
+ if (!fieldName.equals("$staticOverhead")) {
+ Instance child = (Instance)entry.getValue();
+ boolean visit = true;
+ String fieldValue = entry.getValue() == null ? "null" : entry.getValue().toString();
+ LeakReference leakReference = new LeakReference(Type.STATIC_FIELD, fieldName, fieldValue);
+ if (ignoredStaticFields != null) {
+ Exclusion params = (Exclusion)ignoredStaticFields.get(fieldName);
+ if (params != null) {
+ visit = false;
+ if (!params.alwaysExclude) {
+ this.enqueue(params, node, child, leakReference);
+ }
+ }
+ }
+
+ if (visit) {
+ this.enqueue((Exclusion)null, node, child, leakReference);
+ }
+ }
+ }
+ }
+
+ }
+
+ private void visitClassInstance(LeakNode node) {
+ ClassInstance classInstance = (ClassInstance)node.instance;
+ Map ignoredFields = new LinkedHashMap();
+ ClassObj superClassObj = classInstance.getClassObj();
+
+ Exclusion classExclusion;
+ for(classExclusion = null; superClassObj != null; superClassObj = superClassObj.getSuperClassObj()) {
+ Exclusion params = (Exclusion)this.excludedRefs.classNames.get(superClassObj.getClassName());
+ if (params != null && (classExclusion == null || !classExclusion.alwaysExclude)) {
+ classExclusion = params;
+ }
+
+ Map classIgnoredFields = (Map)this.excludedRefs.fieldNameByClassName.get(superClassObj.getClassName());
+ if (classIgnoredFields != null) {
+ ignoredFields.putAll(classIgnoredFields);
+ }
+ }
+
+ if (classExclusion == null || !classExclusion.alwaysExclude) {
+ Iterator var14 = classInstance.getValues().iterator();
+
+ while(true) {
+ Exclusion fieldExclusion;
+ Field field;
+ FieldValue fieldValue;
+ do {
+ if (!var14.hasNext()) {
+ return;
+ }
+
+ fieldValue = (FieldValue)var14.next();
+ fieldExclusion = classExclusion;
+ field = fieldValue.getField();
+ } while(field.getType() != com.squareup.haha.perflib.Type.OBJECT);
+
+ Instance child = (Instance)fieldValue.getValue();
+ String fieldName = field.getName();
+ Exclusion params = (Exclusion)ignoredFields.get(fieldName);
+ if (params != null && (classExclusion == null || params.alwaysExclude && !classExclusion.alwaysExclude)) {
+ fieldExclusion = params;
+ }
+
+ String value = fieldValue.getValue() == null ? "null" : fieldValue.getValue().toString();
+ this.enqueue(fieldExclusion, node, child, new LeakReference(Type.INSTANCE_FIELD, fieldName, value));
+ }
+ }
+ }
+
+ private void visitArrayInstance(LeakNode node) {
+ ArrayInstance arrayInstance = (ArrayInstance)node.instance;
+ com.squareup.haha.perflib.Type arrayType = arrayInstance.getArrayType();
+ if (arrayType == com.squareup.haha.perflib.Type.OBJECT) {
+ Object[] values = arrayInstance.getValues();
+
+ for(int i = 0; i < values.length; ++i) {
+ Instance child = (Instance)values[i];
+ String name = Integer.toString(i);
+ String value = child == null ? "null" : child.toString();
+ this.enqueue((Exclusion)null, node, child, new LeakReference(Type.ARRAY_ENTRY, name, value));
+ }
+ }
+
+ }
+
+ private void enqueue(Exclusion exclusion, LeakNode parent, Instance child, LeakReference leakReference) {
+ if (child != null) {
+ if (!HahaHelper.isPrimitiveOrWrapperArray(child) && !HahaHelper.isPrimitiveWrapper(child)) {
+ if (!this.toVisitSet.contains(child)) {
+ boolean visitNow = exclusion == null;
+ if (visitNow || !this.toVisitIfNoPathSet.contains(child)) {
+ if (!this.canIgnoreStrings || !this.isString(child)) {
+ if (!this.visitedSet.contains(child)) {
+ LeakNode childNode = new LeakNode(exclusion, child, parent, leakReference);
+ if (visitNow) {
+ this.toVisitSet.add(child);
+ this.toVisitQueue.add(childNode);
+ } else {
+ this.toVisitIfNoPathSet.add(child);
+ this.toVisitIfNoPathQueue.add(childNode);
+ }
+
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private boolean isString(Instance instance) {
+ return instance.getClassObj() != null && instance.getClassObj().getClassName().equals(String.class.getName());
+ }
+
+ static final class Result {
+ final LeakNode leakingNode;
+ final boolean excludingKnownLeaks;
+
+ Result(LeakNode leakingNode, boolean excludingKnownLeaks) {
+ this.leakingNode = leakingNode;
+ this.excludingKnownLeaks = excludingKnownLeaks;
+ }
+ }
+}
diff --git a/app/src/main/java/com/android/chap04/TrackedReference.java b/app/src/main/java/com/android/chap04/TrackedReference.java
new file mode 100644
index 0000000..bf73218
--- /dev/null
+++ b/app/src/main/java/com/android/chap04/TrackedReference.java
@@ -0,0 +1,33 @@
+package com.android.chap04;
+
+import android.support.annotation.NonNull;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * An instance tracked by a {@link KeyedWeakReference} that hadn't been cleared when the
+ * heap was dumped. May or may not point to a leaking reference.
+ */
+public class TrackedReference {
+
+ /** Corresponds to {@link KeyedWeakReference#key}. */
+ @NonNull public final String key;
+
+ /** Corresponds to {@link KeyedWeakReference#name}. */
+ @NonNull public final String name;
+
+ /** Class of the tracked instance. */
+ @NonNull public final String className;
+
+ /** List of all fields (member and static) for that instance. */
+ @NonNull public final List fields;
+
+ public TrackedReference(@NonNull String key, @NonNull String name, @NonNull String className,
+ @NonNull List fields) {
+ this.key = key;
+ this.name = name;
+ this.className = className;
+ this.fields = Collections.unmodifiableList(new ArrayList<>(fields));
+ }
+}
diff --git a/app/src/main/res/drawable/iv02.jpg b/app/src/main/res/drawable/iv02.jpg
new file mode 100644
index 0000000..8bcbbb7
Binary files /dev/null and b/app/src/main/res/drawable/iv02.jpg differ
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..662d693
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index cdf1d5d..86cc135 100644
--- a/build.gradle
+++ b/build.gradle
@@ -7,7 +7,7 @@ buildscript {
}
dependencies {
- classpath 'com.android.tools.build:gradle:3.2.0'
+ classpath 'com.android.tools.build:gradle:3.0.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@@ -25,3 +25,11 @@ allprojects {
task clean(type: Delete) {
delete rootProject.buildDir
}
+
+ext {
+ compileSdkVersion = 26
+ buildToolsVersion = "26.0.2"
+ minSdkVersion = 17
+ targetSdkVersion = 26
+ supportVersion = "26.1.0"
+}
diff --git a/settings.gradle b/settings.gradle
index 6262513..ef4edcd 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1 @@
-include ':DuplicatedBitmapAnalyzer'
+include ':app', ':DuplicatedBitmapAnalyzer'