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 reachabilityInspectorClass +// : reachabilityInspectorClasses) { +// try { +// Constructor 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 @@ + + + + + + + + + + + + + + +