diff --git a/DuplicatedBitmapAnalyzer/build.gradle b/DuplicatedBitmapAnalyzer/build.gradle index 912e192..d91aae9 100644 --- a/DuplicatedBitmapAnalyzer/build.gradle +++ b/DuplicatedBitmapAnalyzer/build.gradle @@ -1,11 +1,10 @@ apply plugin: 'java' - - version 1.0 dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') + compile 'com.squareup.haha:haha:2.0.4' } @@ -15,6 +14,14 @@ jar { attributes 'Manifest-Version': version } + sourceSets{ + main{ + java{ + srcDir 'src' + } + } + } + from { exclude 'META-INF/MANIFEST.MF' exclude 'META-INF/*.SF' diff --git a/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/Main.java b/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/Main.java index 202764e..ba5cd84 100644 --- a/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/Main.java +++ b/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/Main.java @@ -1,13 +1,103 @@ package com.hprof.bitmap; -import java.io.FileInputStream; +import com.hprof.bitmap.entry.BitmapInstance; +import com.hprof.bitmap.entry.DuplicatedCollectInfo; +import com.hprof.bitmap.utils.HahaHelper; +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.nio.ByteBuffer; -import java.nio.channels.FileChannel; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; public class Main { public static void main(String[] args) throws IOException { + File heapDumpFile = new File("./myhprof.hprof"); + + boolean isExists = heapDumpFile.exists(); + System.out.println("heapDumpFile is exists:" + isExists); + + HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile); + HprofParser parser = new HprofParser(buffer); + Snapshot snapshot = parser.parse(); + snapshot.computeDominators(); + + ClassObj bitmapClass = snapshot.findClass("android.graphics.Bitmap"); + + // 只分析 default 和 app + Heap defaultHeap = snapshot.getHeap("default"); + Heap appHeap = snapshot.getHeap("app"); + // 从 heap 中获取 bitmap instance 实例 + List defaultBmInstance = bitmapClass.getHeapInstances(defaultHeap.getId()); + List appBmInstance = bitmapClass.getHeapInstances(appHeap.getId()); + defaultBmInstance.addAll(appBmInstance); + + List collectInfos = collectSameBitmap(snapshot, defaultBmInstance); + for (DuplicatedCollectInfo info : collectInfos) { + println(info.string()); + } + } + + private static List collectSameBitmap(Snapshot snapshot, List bmInstanceList) { + Map> collectSameMap = new HashMap<>(); + ArrayList duplicatedCollectInfos = new ArrayList<>(); + + // 收集 + for (Instance instance : bmInstanceList) { + List classFieldList = HahaHelper.classInstanceValues(instance); + + ArrayInstance arrayInstance = HahaHelper.fieldValue(classFieldList, "mBuffer"); + byte[] mBufferByte = HahaHelper.getByteArray(arrayInstance); + int mBufferHashCode = Arrays.hashCode(mBufferByte); + String hashKey = String.valueOf(mBufferHashCode); + + if (collectSameMap.containsKey(hashKey)) { + collectSameMap.get(hashKey).add(instance); + } else { + List bmList = new ArrayList<>(); + bmList.add(instance); + collectSameMap.put(hashKey, bmList); + } + + } + + // 去除只有一例的 + Iterator>> it = collectSameMap.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry> entry = it.next(); + if (entry.getValue().size() <= 1) { + it.remove(); + } + } + + // 留下重复的图片,创建 duplicatedCollectInfo 对象存入数组中 + for (Map.Entry> entry : collectSameMap.entrySet()) { + DuplicatedCollectInfo info = new DuplicatedCollectInfo(entry.getKey()); + for (Instance instance : entry.getValue()) { + info.addBitmapInstance(new BitmapInstance(snapshot,entry.getKey(), instance)); + } + info.internalSetValue(); + duplicatedCollectInfos.add(info); + } + + return duplicatedCollectInfos; + } + + + private static void println(String content) { + System.out.println(content); } } diff --git a/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/entry/BitmapInstance.java b/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/entry/BitmapInstance.java new file mode 100644 index 0000000..b0361a4 --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/entry/BitmapInstance.java @@ -0,0 +1,108 @@ +package com.hprof.bitmap.entry; + +import com.hprof.bitmap.utils.HahaHelper; +import com.hprof.bitmap.utils.TraceUtils; +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.Snapshot; +import com.squareup.leakcanary.AnalysisResult; +import com.squareup.leakcanary.AnalyzerProgressListener; +import com.squareup.leakcanary.ExcludedRefs; +import com.squareup.leakcanary.HeapAnalyzer; +import com.squareup.leakcanary.Reachability; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * @author Zengshaoyi + * @version 1.0

Features draft description.主要功能介绍

+ * @since 2018/12/26 15:35 + */ +public class BitmapInstance { + + private String mHash; + + private Instance mInstance; + + private ArrayList mTraceStack = new ArrayList<>(); + + private int mWith; + + private int mHeight; + + private int size; + + private Snapshot mSnapshot; + + public BitmapInstance(Snapshot snapshot, String hash, Instance instance) { + mSnapshot = snapshot; + mHash = hash; + mInstance = instance; + mTraceStack.addAll(TraceUtils.getTraceFromInstance(mInstance)); + + List classFieldList = HahaHelper.classInstanceValues(instance); + mWith = HahaHelper.fieldValue(classFieldList, "mWidth"); + mHeight = HahaHelper.fieldValue(classFieldList, "mHeight"); + + ArrayInstance arrayInstance = HahaHelper.fieldValue(classFieldList, "mBuffer"); + byte[] mBufferByte = HahaHelper.getByteArray(arrayInstance); + if (mBufferByte != null) { + size = mBufferByte.length; + } + } + + public String getHash() { + return mHash; + } + + public Instance getInstance() { + return mInstance; + } + + public String getTrace() { + StringBuilder builder = new StringBuilder(); + builder.append("["); + for (int i = 0; i < mTraceStack.size(); i++) { + String className; + if (mTraceStack.get(i) instanceof ClassObj) { + className = ((ClassObj) mTraceStack.get(i)).getClassName(); + } else { + className = mTraceStack.get(i).getClassObj().getClassName(); + } + + builder.append("\"").append(className).append("\""); + if (i != mTraceStack.size() - 1) { + builder.append(","); + }else{ + builder.append("]"); + } + builder.append("\n"); + } + return builder.toString(); + } + + public String getTraceFromLeakCanary(){ + HeapAnalyzer analyzer = new HeapAnalyzer(ExcludedRefs.builder().build(),AnalyzerProgressListener.NONE, + Collections.>emptyList()); + + + AnalysisResult ar = analyzer.findLeakTrace(System.nanoTime(), mSnapshot, mInstance,true); + return ar.leakTrace.toString(); + } + + public int getWith() { + return mWith; + } + + public int getHeight() { + return mHeight; + } + + public int getSize() { + return size; + } +} diff --git a/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/entry/DuplicatedCollectInfo.java b/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/entry/DuplicatedCollectInfo.java new file mode 100644 index 0000000..2ade862 --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/entry/DuplicatedCollectInfo.java @@ -0,0 +1,66 @@ +package com.hprof.bitmap.entry; + +import java.util.ArrayList; + +/** + * @author Zengshaoyi + * @version 1.0

Features draft description.主要功能介绍

+ * @since 2018/12/26 15:50 + */ +public class DuplicatedCollectInfo { + + // hash + private String mHash; + + // 相同 hash 的 Bitmap + private ArrayList mBitmapInstances = new ArrayList<>(); + + // mBitmapInstances size + private int duplicatedCount; + + private int size; + + private int width; + + private int height; + + public DuplicatedCollectInfo(String hash) { + this.mHash = hash; + } + + public void addBitmapInstance(BitmapInstance bitmapInstance) { + mBitmapInstances.add(bitmapInstance); + } + + public void internalSetValue() { + duplicatedCount = mBitmapInstances.size(); + if(mBitmapInstances.size() > 0){ + BitmapInstance instance = mBitmapInstances.get(0); + this.width = instance.getWith(); + this.height = instance.getHeight(); + this.size = instance.getSize(); + } + } + + public String string() { + StringBuilder builder = new StringBuilder(); + builder.append("{\n") + .append("\t\"hash\":").append(mHash).append(",\n") + .append("\t\"size\":").append(size).append(",\n") + .append("\t\"width\":").append(width).append(",\n") + .append("\t\"height\":").append(height).append(",\n") + .append("\t\"duplicatedCount\":").append(duplicatedCount).append(",\n") + .append("\t\"stack\":").append("[\n"); + + for(int i=0;i 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); + } + + public 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. + */ + public 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. + */ + public 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; + } + + public static List classInstanceValues(Instance instance) { + ClassInstance classInstance = (ClassInstance) instance; + return classInstance.getValues(); + } + + @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) + public 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"); + } + + public 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(); + } + + public static byte[] getByteArray(Object arrayInstance) { + if (isByteArray(arrayInstance)) { + try { + Method asRawByteArray = + ArrayInstance.class.getDeclaredMethod("asRawByteArray", int.class, int.class); + asRawByteArray.setAccessible(true); + Field length = ArrayInstance.class.getDeclaredField("mLength"); + length.setAccessible(true); + int lengthValue = (int) length.get(arrayInstance); + byte[] rawByteArray = (byte[]) asRawByteArray.invoke(arrayInstance, 0, lengthValue); + return rawByteArray; + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + return null; + } +} diff --git a/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/utils/Preconditions.java b/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/utils/Preconditions.java new file mode 100644 index 0000000..bc53f66 --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/utils/Preconditions.java @@ -0,0 +1,35 @@ +/* + * 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.hprof.bitmap.utils; + +final 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/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/utils/TraceUtils.java b/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/utils/TraceUtils.java new file mode 100644 index 0000000..9cad93b --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/utils/TraceUtils.java @@ -0,0 +1,30 @@ +package com.hprof.bitmap.utils; + +import com.squareup.haha.perflib.Instance; + +import java.util.ArrayList; + +/** + * @author Zengshaoyi + * @version 1.0

Features draft description.主要功能介绍

+ * @since 2018/12/26 15:41 + */ +public class TraceUtils { + + /** + * 获取 trace + * + * @param instance + * @return + */ + public static ArrayList getTraceFromInstance(Instance instance) { + ArrayList arrayList = new ArrayList<>(); + Instance nextInstance = null; + while ((nextInstance = instance.getNextInstanceToGcRoot()) != null) { + arrayList.add(nextInstance); + instance = nextInstance; + } + return arrayList; + } + +} \ No newline at end of file diff --git a/DuplicatedBitmapAnalyzer/src/com/squareuo/haha/perflib/HahaSpy.java b/DuplicatedBitmapAnalyzer/src/com/squareuo/haha/perflib/HahaSpy.java new file mode 100644 index 0000000..e72baaf --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/squareuo/haha/perflib/HahaSpy.java @@ -0,0 +1,60 @@ +/* + * 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.squareuo.haha.perflib; + +import com.squareup.haha.perflib.Instance; +import com.squareup.haha.perflib.RootObj; +import com.squareup.haha.perflib.Snapshot; +import com.squareup.haha.perflib.StackTrace; +import com.squareup.haha.perflib.ThreadObj; + +import java.lang.reflect.Field; + +public final class HahaSpy { + + public static Instance allocatingThread(Instance instance) { + Snapshot snapshot = (Snapshot) reflectField(instance.getHeap(),"mSnapshot"); + int threadSerialNumber; + if (instance instanceof RootObj) { + threadSerialNumber = (int) reflectField(((RootObj)instance),"mThread"); + } else { + StackTrace stackTrace = (StackTrace) reflectField(instance,"mStack"); + threadSerialNumber = (int) reflectField(stackTrace, "mThreadSerialNumber"); + } + ThreadObj thread = snapshot.getThread(threadSerialNumber); + return snapshot.findInstance((Long) reflectField(thread,"mId")); + } + + private static Object reflectField(Object object, String fieldName){ + if(object == null){ + return null; + } + try { + Field field = object.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(object); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + return null; + } + + private HahaSpy() { + throw new AssertionError(); + } +} diff --git a/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/AnalysisResult.java b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/AnalysisResult.java new file mode 100644 index 0000000..81b983a --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/AnalysisResult.java @@ -0,0 +1,151 @@ +/* + * 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.squareup.leakcanary; + +import java.io.Serializable; + +public final class AnalysisResult implements Serializable { + + public static final long RETAINED_HEAP_SKIPPED = -1; + + public static + AnalysisResult noLeak(long analysisDurationMs) { + return new AnalysisResult(false, false, null, null, null, 0, analysisDurationMs); + } + + public static + AnalysisResult leakDetected(boolean excludedLeak, + String className, + LeakTrace leakTrace, long retainedHeapSize, long analysisDurationMs) { + return new AnalysisResult(true, excludedLeak, className, leakTrace, null, retainedHeapSize, + analysisDurationMs); + } + + public static + AnalysisResult failure( 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 if {@link #leakFound} is true, null otherwise. + * The class name format is the same as what would be returned by {@link Class#getName()}. + */ + 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. + */ + public final LeakTrace leakTrace; + + /** Null unless the analysis failed. */ + 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 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/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/AnalyzerProgressListener.java b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/AnalyzerProgressListener.java new file mode 100644 index 0000000..11bb2da --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/AnalyzerProgressListener.java @@ -0,0 +1,24 @@ +package com.squareup.leakcanary; + +public interface AnalyzerProgressListener { + + + AnalyzerProgressListener NONE = new AnalyzerProgressListener() { + @Override public void onProgressUpdate( AnalyzerProgressListener.Step step) { + } + }; + + // These steps should be defined in the order in which they occur. + enum Step { + READING_HEAP_DUMP_FILE, + PARSING_HEAP_DUMP, + DEDUPLICATING_GC_ROOTS, + FINDING_LEAKING_REF, + FINDING_SHORTEST_PATH, + BUILDING_LEAK_TRACE, + COMPUTING_DOMINATORS, + COMPUTING_BITMAP_SIZE, + } + + void onProgressUpdate( Step step); +} \ No newline at end of file diff --git a/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/DebuggerControl.java b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/DebuggerControl.java new file mode 100644 index 0000000..fdd03be --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/DebuggerControl.java @@ -0,0 +1,30 @@ +/* + * 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.squareup.leakcanary; + +/** + * Gives the opportunity to skip checking if a reference is gone when the debugger is connected. + * An attached debugger might retain references and create false positives. + */ +public interface DebuggerControl { + DebuggerControl NONE = new DebuggerControl() { + @Override public boolean isDebuggerAttached() { + return false; + } + }; + + boolean isDebuggerAttached(); +} \ No newline at end of file diff --git a/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/ExcludedRefs.java b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/ExcludedRefs.java new file mode 100644 index 0000000..6ac369e --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/ExcludedRefs.java @@ -0,0 +1,192 @@ +/* + * 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.squareup.leakcanary; + +import java.io.Serializable; +import java.util.LinkedHashMap; +import java.util.Map; + +import static com.squareup.leakcanary.Preconditions.checkNotNull; +import static java.util.Collections.unmodifiableMap; + +/** + * Prevents specific references from being taken into account when computing the shortest strong + * reference path from a suspected leaking instance to the GC roots. + * + * This class lets you ignore known memory leaks that you known about. If the shortest path + * matches {@link ExcludedRefs}, than the heap analyzer should look for a longer path with nothing + * matching in {@link ExcludedRefs}. + */ +public final class ExcludedRefs implements Serializable { + + public static Builder builder() { + return new BuilderWithParams(); + } + + public final Map> fieldNameByClassName; + public final Map> staticFieldNameByClassName; + public final Map threadNames; + public final Map classNames; + + ExcludedRefs(BuilderWithParams builder) { + this.fieldNameByClassName = unmodifiableRefStringMap(builder.fieldNameByClassName); + this.staticFieldNameByClassName = unmodifiableRefStringMap(builder.staticFieldNameByClassName); + this.threadNames = unmodifiableRefMap(builder.threadNames); + this.classNames = unmodifiableRefMap(builder.classNames); + } + + private Map> unmodifiableRefStringMap( + Map> mapmap) { + LinkedHashMap> fieldNameByClassName = new LinkedHashMap<>(); + for (Map.Entry> entry : mapmap.entrySet()) { + fieldNameByClassName.put(entry.getKey(), unmodifiableRefMap(entry.getValue())); + } + return unmodifiableMap(fieldNameByClassName); + } + + private Map unmodifiableRefMap(Map fieldBuilderMap) { + Map fieldMap = new LinkedHashMap<>(); + for (Map.Entry fieldEntry : fieldBuilderMap.entrySet()) { + fieldMap.put(fieldEntry.getKey(), new Exclusion(fieldEntry.getValue())); + } + return unmodifiableMap(fieldMap); + } + + @Override public String toString() { + String string = ""; + for (Map.Entry> classes : fieldNameByClassName.entrySet()) { + String clazz = classes.getKey(); + for (Map.Entry field : classes.getValue().entrySet()) { + String always = field.getValue().alwaysExclude ? " (always)" : ""; + string += "| Field: " + clazz + "." + field.getKey() + always + "\n"; + } + } + for (Map.Entry> classes : staticFieldNameByClassName.entrySet()) { + String clazz = classes.getKey(); + for (Map.Entry field : classes.getValue().entrySet()) { + String always = field.getValue().alwaysExclude ? " (always)" : ""; + string += "| Static field: " + clazz + "." + field.getKey() + always + "\n"; + } + } + for (Map.Entry thread : threadNames.entrySet()) { + String always = thread.getValue().alwaysExclude ? " (always)" : ""; + string += "| Thread:" + thread.getKey() + always + "\n"; + } + for (Map.Entry clazz : classNames.entrySet()) { + String always = clazz.getValue().alwaysExclude ? " (always)" : ""; + string += "| Class:" + clazz.getKey() + always + "\n"; + } + return string; + } + + static final class ParamsBuilder { + String name; + String reason; + boolean alwaysExclude; + final String matching; + + ParamsBuilder(String matching) { + this.matching = matching; + } + } + + public interface Builder { + BuilderWithParams instanceField(String className, String fieldName); + + BuilderWithParams staticField(String className, String fieldName); + + BuilderWithParams thread(String threadName); + + BuilderWithParams clazz(String className); + + ExcludedRefs build(); + } + + public static final class BuilderWithParams implements Builder { + + private final Map> fieldNameByClassName = + new LinkedHashMap<>(); + private final Map> staticFieldNameByClassName = + new LinkedHashMap<>(); + private final Map threadNames = new LinkedHashMap<>(); + private final Map classNames = new LinkedHashMap<>(); + + private ParamsBuilder lastParams; + + BuilderWithParams() { + } + + @Override public BuilderWithParams instanceField(String className, String fieldName) { + checkNotNull(className, "className"); + checkNotNull(fieldName, "fieldName"); + Map excludedFields = fieldNameByClassName.get(className); + if (excludedFields == null) { + excludedFields = new LinkedHashMap<>(); + fieldNameByClassName.put(className, excludedFields); + } + lastParams = new ParamsBuilder("field " + className + "#" + fieldName); + excludedFields.put(fieldName, lastParams); + return this; + } + + @Override public BuilderWithParams staticField(String className, String fieldName) { + checkNotNull(className, "className"); + checkNotNull(fieldName, "fieldName"); + Map excludedFields = staticFieldNameByClassName.get(className); + if (excludedFields == null) { + excludedFields = new LinkedHashMap<>(); + staticFieldNameByClassName.put(className, excludedFields); + } + lastParams = new ParamsBuilder("static field " + className + "#" + fieldName); + excludedFields.put(fieldName, lastParams); + return this; + } + + @Override public BuilderWithParams thread(String threadName) { + checkNotNull(threadName, "threadName"); + lastParams = new ParamsBuilder("any threads named " + threadName); + threadNames.put(threadName, lastParams); + return this; + } + + /** Ignores all fields and static fields of all subclasses of the provided class name. */ + @Override public BuilderWithParams clazz(String className) { + checkNotNull(className, "className"); + lastParams = new ParamsBuilder("any subclass of " + className); + classNames.put(className, lastParams); + return this; + } + + public BuilderWithParams named(String name) { + lastParams.name = name; + return this; + } + + public BuilderWithParams reason(String reason) { + lastParams.reason = reason; + return this; + } + + public BuilderWithParams alwaysExclude() { + lastParams.alwaysExclude = true; + return this; + } + + @Override public ExcludedRefs build() { + return new ExcludedRefs(this); + } + } +} diff --git a/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/Exclusion.java b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/Exclusion.java new file mode 100644 index 0000000..4d87d05 --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/Exclusion.java @@ -0,0 +1,17 @@ +package com.squareup.leakcanary; + +import java.io.Serializable; + +public final class Exclusion implements Serializable { + public final String name; + public final String reason; + public final boolean alwaysExclude; + public final String matching; + + Exclusion(ExcludedRefs.ParamsBuilder builder) { + this.name = builder.name; + this.reason = builder.reason; + this.alwaysExclude = builder.alwaysExclude; + this.matching = builder.matching; + } +} diff --git a/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/GcTrigger.java b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/GcTrigger.java new file mode 100644 index 0000000..ff5afad --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/GcTrigger.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2011 Google 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.squareup.leakcanary; + +/** + * Called when a watched reference is expected to be weakly reachable, but hasn't been enqueued + * in the reference queue yet. This gives the application a hook to run the GC before the {@link + * RefWatcher} checks the reference queue again, to avoid taking a heap dump if possible. + */ +public interface GcTrigger { + GcTrigger DEFAULT = new GcTrigger() { + @Override public void runGc() { + // Code taken from AOSP FinalizationTest: + // https://android.googlesource.com/platform/libcore/+/master/support/src/test/java/libcore/ + // java/lang/ref/FinalizationTester.java + // System.gc() does not garbage collect every time. Runtime.gc() is + // more likely to perform a gc. + Runtime.getRuntime().gc(); + enqueueReferences(); + System.runFinalization(); + } + + private void enqueueReferences() { + // Hack. We don't have a programmatic way to wait for the reference queue daemon to move + // references to the appropriate queues. + try { + Thread.sleep(100); + } catch (InterruptedException e) { + throw new AssertionError(); + } + } + }; + + void runGc(); +} diff --git a/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/HahaHelper.java b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/HahaHelper.java new file mode 100644 index 0000000..c58b7d8 --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/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.squareup.leakcanary; + +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.squareup.leakcanary.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/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/HeapAnalyzer.java b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/HeapAnalyzer.java new file mode 100644 index 0000000..2ebed31 --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/HeapAnalyzer.java @@ -0,0 +1,525 @@ +/* + * 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.squareup.leakcanary; + + +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 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 gnu.trove.THashMap; +import gnu.trove.TObjectProcedure; + +import static com.squareup.leakcanary.AnalysisResult.failure; +import static com.squareup.leakcanary.AnalysisResult.leakDetected; +import static com.squareup.leakcanary.AnalysisResult.noLeak; +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 com.squareup.leakcanary.HahaHelper.asString; +import static com.squareup.leakcanary.HahaHelper.classInstanceValues; +import static com.squareup.leakcanary.HahaHelper.extendsThread; +import static com.squareup.leakcanary.HahaHelper.fieldValue; +import static com.squareup.leakcanary.HahaHelper.hasField; +import static com.squareup.leakcanary.HahaHelper.threadName; +import static com.squareup.leakcanary.HahaHelper.valueAsString; +import static com.squareup.leakcanary.LeakTraceElement.Holder.ARRAY; +import static com.squareup.leakcanary.LeakTraceElement.Holder.CLASS; +import static com.squareup.leakcanary.LeakTraceElement.Holder.OBJECT; +import static com.squareup.leakcanary.LeakTraceElement.Holder.THREAD; +import static com.squareup.leakcanary.LeakTraceElement.Type.ARRAY_ENTRY; +import static com.squareup.leakcanary.LeakTraceElement.Type.INSTANCE_FIELD; +import static com.squareup.leakcanary.LeakTraceElement.Type.STATIC_FIELD; +import static com.squareup.leakcanary.Reachability.REACHABLE; +import static com.squareup.leakcanary.Reachability.UNKNOWN; +import static com.squareup.leakcanary.Reachability.UNREACHABLE; +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; + + /** + * @deprecated Use {@link #HeapAnalyzer(ExcludedRefs, AnalyzerProgressListener, List)}. + */ + @Deprecated + public HeapAnalyzer( ExcludedRefs excludedRefs) { + this(excludedRefs, AnalyzerProgressListener.NONE, + Collections.>emptyList()); + } + + public HeapAnalyzer( ExcludedRefs excludedRefs, + AnalyzerProgressListener listener, + 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 List findTrackedReferences( 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 AnalysisResult checkForLeak( File heapDumpFile, + 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 AnalysisResult checkForLeak( File heapDumpFile, + 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); + // 解析过程,是基于 google 的 preflib 库,根据 hprof 的格式进行解析 + Snapshot snapshot = parser.parse(); + listener.onProgressUpdate(DEDUPLICATING_GC_ROOTS); + // 解决去重 + deduplicateGcRoots(snapshot); + listener.onProgressUpdate(FINDING_LEAKING_REF); + // reference key UUID key?UUID.randomUUID() 作用 提供的一个自动生成主键的方法 + // 在一台机器上生成的数字保证对在同一时空中的所有机器都是唯一的 + Instance leakingRef = findLeakingReference(referenceKey, snapshot); + + // False alarm, weak reference was cleared in between key check and heap dump. + if (leakingRef == null) { + return noLeak(since(analysisStartNanoTime)); + } + // 此对象存在也不能确认它内存泄漏了,还要检测此对象的 gc root + 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) { + // 获取 snapshot 中所有的 KeyedWeakReference + 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; + } + // 找到 KeyedWeakReference 里面的 Key 值,UUID + 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 AnalysisResult findLeakTrace(long analysisStartNanoTime, Snapshot snapshot, + Instance leakingRef, boolean computeRetainedSize) { + + listener.onProgressUpdate(FINDING_SHORTEST_PATH); + //这两行代码是判断内存泄露的关键,我们在上篇中分析hprof文件,判断内存泄漏 + //判断的依据是展开调用到gc root,所谓gc root,就是不能被gc回收的对象, + //gc root有很多类型,我们只要关注两种类型1.此对象是静态 2.此对象被其他线程使用,并且其他线程正在运行,没有结束 + ShortestPathFinder pathFinder = new ShortestPathFinder(excludedRefs); + ShortestPathFinder.Result result = pathFinder.findPath(snapshot, leakingRef); + + // False alarm, no strong reference path to GC Roots. + if (result.leakingNode == null) { + return noLeak(since(analysisStartNanoTime)); + } + + listener.onProgressUpdate(BUILDING_LEAK_TRACE); + // 生成泄漏的调用栈 + LeakTrace leakTrace = buildLeakTrace(result.leakingNode); + + String className = leakingRef.getClassObj().getClassName(); + + 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/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/HeapDump.java b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/HeapDump.java new file mode 100644 index 0000000..7401c97 --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/HeapDump.java @@ -0,0 +1,195 @@ +/* + * 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.squareup.leakcanary; + +import java.io.File; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import static com.squareup.leakcanary.Preconditions.checkNotNull; +import static java.util.Collections.unmodifiableList; + +/** Data structure holding information about a heap dump. */ +public final class HeapDump implements Serializable { + + public static Builder builder() { + return new Builder(); + } + + /** Receives a heap dump to analyze. */ + public interface Listener { + Listener NONE = new Listener() { + @Override public void analyze(HeapDump heapDump) { + } + }; + + void analyze(HeapDump heapDump); + } + + /** The heap dump file, which you might want to upload somewhere. */ + public final File heapDumpFile; + + /** + * Key associated to the {@link KeyedWeakReference} used to detect the memory leak. + * When analyzing a heap dump, search for all {@link KeyedWeakReference} instances, then open + * the one that has its "key" field set to this value. Its "referent" field contains the + * leaking object. Computing the shortest path to GC roots on that leaking object should enable + * you to figure out the cause of the leak. + */ + public final String referenceKey; + + /** + * User defined name to help identify the leaking instance. + */ + public final String referenceName; + + /** References that should be ignored when analyzing this heap dump. */ + public final ExcludedRefs excludedRefs; + + /** Time from the request to watch the reference until the GC was triggered. */ + public final long watchDurationMs; + public final long gcDurationMs; + public final long heapDumpDurationMs; + public final boolean computeRetainedHeapSize; + public final List> reachabilityInspectorClasses; + + /** + * Calls {@link #HeapDump(Builder)} with computeRetainedHeapSize set to true. + * + * @deprecated Use {@link #HeapDump(Builder)} instead. + */ + @Deprecated + public HeapDump(File heapDumpFile, String referenceKey, String referenceName, + ExcludedRefs excludedRefs, long watchDurationMs, long gcDurationMs, long heapDumpDurationMs) { + this(new Builder().heapDumpFile(heapDumpFile) + .referenceKey(referenceKey) + .referenceName(referenceName) + .excludedRefs(excludedRefs) + .computeRetainedHeapSize(true) + .watchDurationMs(watchDurationMs) + .gcDurationMs(gcDurationMs) + .heapDumpDurationMs(heapDumpDurationMs)); + } + + HeapDump(Builder builder) { + this.heapDumpFile = builder.heapDumpFile; + this.referenceKey = builder.referenceKey; + this.referenceName = builder.referenceName; + this.excludedRefs = builder.excludedRefs; + this.computeRetainedHeapSize = builder.computeRetainedHeapSize; + this.watchDurationMs = builder.watchDurationMs; + this.gcDurationMs = builder.gcDurationMs; + this.heapDumpDurationMs = builder.heapDumpDurationMs; + this.reachabilityInspectorClasses = builder.reachabilityInspectorClasses; + } + + public Builder buildUpon() { + return new Builder(this); + } + + public static final class Builder { + File heapDumpFile; + String referenceKey; + String referenceName; + ExcludedRefs excludedRefs; + long watchDurationMs; + long gcDurationMs; + long heapDumpDurationMs; + boolean computeRetainedHeapSize; + List> reachabilityInspectorClasses; + + Builder() { + this.heapDumpFile = null; + this.referenceKey = null; + referenceName = ""; + excludedRefs = null; + watchDurationMs = 0; + gcDurationMs = 0; + heapDumpDurationMs = 0; + computeRetainedHeapSize = false; + reachabilityInspectorClasses = null; + } + + Builder(HeapDump heapDump) { + this.heapDumpFile = heapDump.heapDumpFile; + this.referenceKey = heapDump.referenceKey; + this.referenceName = heapDump.referenceName; + this.excludedRefs = heapDump.excludedRefs; + this.computeRetainedHeapSize = heapDump.computeRetainedHeapSize; + this.watchDurationMs = heapDump.watchDurationMs; + this.gcDurationMs = heapDump.gcDurationMs; + this.heapDumpDurationMs = heapDump.heapDumpDurationMs; + this.reachabilityInspectorClasses = heapDump.reachabilityInspectorClasses; + } + + public Builder heapDumpFile(File heapDumpFile) { + this.heapDumpFile = checkNotNull(heapDumpFile, "heapDumpFile"); + return this; + } + + public Builder referenceKey(String referenceKey) { + this.referenceKey = checkNotNull(referenceKey, "referenceKey"); + return this; + } + + public Builder referenceName(String referenceName) { + this.referenceName = checkNotNull(referenceName, "referenceName"); + return this; + } + + public Builder excludedRefs(ExcludedRefs excludedRefs) { + this.excludedRefs = checkNotNull(excludedRefs, "excludedRefs"); + return this; + } + + public Builder watchDurationMs(long watchDurationMs) { + this.watchDurationMs = watchDurationMs; + return this; + } + + public Builder gcDurationMs(long gcDurationMs) { + this.gcDurationMs = gcDurationMs; + return this; + } + + public Builder heapDumpDurationMs(long heapDumpDurationMs) { + this.heapDumpDurationMs = heapDumpDurationMs; + return this; + } + + public Builder computeRetainedHeapSize(boolean computeRetainedHeapSize) { + this.computeRetainedHeapSize = computeRetainedHeapSize; + return this; + } + + public Builder reachabilityInspectorClasses( + List> reachabilityInspectorClasses) { + checkNotNull(reachabilityInspectorClasses, "reachabilityInspectorClasses"); + this.reachabilityInspectorClasses = + unmodifiableList(new ArrayList<>(reachabilityInspectorClasses)); + return this; + } + + public HeapDump build() { + checkNotNull(excludedRefs, "excludedRefs"); + checkNotNull(heapDumpFile, "heapDumpFile"); + checkNotNull(referenceKey, "referenceKey"); + checkNotNull(reachabilityInspectorClasses, "reachabilityInspectorClasses"); + return new HeapDump(this); + } + } +} diff --git a/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/HeapDumper.java b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/HeapDumper.java new file mode 100644 index 0000000..43a9499 --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/HeapDumper.java @@ -0,0 +1,35 @@ +/* + * 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.squareup.leakcanary; + +import java.io.File; + +/** Dumps the heap into a file. */ +public interface HeapDumper { + HeapDumper NONE = new HeapDumper() { + @Override public File dumpHeap() { + return RETRY_LATER; + } + }; + + File RETRY_LATER = null; + + /** + * @return a {@link File} referencing the dumped heap, or {@link #RETRY_LATER} if the heap could + * not be dumped. + */ + File dumpHeap(); +} \ No newline at end of file diff --git a/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/KeyedWeakReference.java b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/KeyedWeakReference.java new file mode 100644 index 0000000..9932308 --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/KeyedWeakReference.java @@ -0,0 +1,34 @@ +/* + * 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.squareup.leakcanary; + +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; + +import static com.squareup.leakcanary.Preconditions.checkNotNull; + +/** @see {@link HeapDump#referenceKey}. */ +final class KeyedWeakReference extends WeakReference { + public final String key; + public final String name; + + KeyedWeakReference(Object referent, String key, String name, + ReferenceQueue referenceQueue) { + super(checkNotNull(referent, "referent"), checkNotNull(referenceQueue, "referenceQueue")); + this.key = checkNotNull(key, "key"); + this.name = checkNotNull(name, "name"); + } +} \ No newline at end of file diff --git a/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/LeakNode.java b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/LeakNode.java new file mode 100644 index 0000000..94a7725 --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/LeakNode.java @@ -0,0 +1,33 @@ +/* + * 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.squareup.leakcanary; + +import com.squareup.haha.perflib.Instance; + +final class LeakNode { + /** May be null. */ + 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/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/LeakReference.java b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/LeakReference.java new file mode 100644 index 0000000..824e329 --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/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.squareup.leakcanary; + +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/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/LeakTrace.java b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/LeakTrace.java new file mode 100644 index 0000000..b497f49 --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/LeakTrace.java @@ -0,0 +1,70 @@ +/* + * 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.squareup.leakcanary; + + +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 { + + public final List elements; + 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 String toDetailedString() { + String string = ""; + for (LeakTraceElement element : elements) { + string += element.toDetailedString(); + } + return string; + } +} diff --git a/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/LeakTraceElement.java b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/LeakTraceElement.java new file mode 100644 index 0000000..586901e --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/LeakTraceElement.java @@ -0,0 +1,200 @@ +/* + * 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.squareup.leakcanary; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static com.squareup.leakcanary.LeakTraceElement.Holder.ARRAY; +import static com.squareup.leakcanary.LeakTraceElement.Holder.CLASS; +import static com.squareup.leakcanary.LeakTraceElement.Holder.THREAD; +import static com.squareup.leakcanary.LeakTraceElement.Type.STATIC_FIELD; +import static java.util.Collections.unmodifiableList; +import static java.util.Locale.US; + +/** 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; + } + + public String getClassName(){ + return className; + } + /** + * 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 += getClassName(); + + 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/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/Preconditions.java b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/Preconditions.java new file mode 100644 index 0000000..f7521e9 --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/Preconditions.java @@ -0,0 +1,35 @@ +/* + * 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.squareup.leakcanary; + +final 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/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/Reachability.java b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/Reachability.java new file mode 100644 index 0000000..ea46c4d --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/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.squareup.leakcanary; + +/** 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/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/RefWatcher.java b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/RefWatcher.java new file mode 100644 index 0000000..4a4fcfb --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/RefWatcher.java @@ -0,0 +1,173 @@ +/* + * 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.squareup.leakcanary; + +import java.io.File; +import java.lang.ref.ReferenceQueue; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CopyOnWriteArraySet; + +import static com.squareup.leakcanary.HeapDumper.RETRY_LATER; +import static com.squareup.leakcanary.Preconditions.checkNotNull; +import static com.squareup.leakcanary.Retryable.Result.DONE; +import static com.squareup.leakcanary.Retryable.Result.RETRY; +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +/** + * Watches references that should become weakly reachable. When the {@link RefWatcher} detects that + * a reference might not be weakly reachable when it should, it triggers the {@link HeapDumper}. + * + *

This class is thread-safe: you can call {@link #watch(Object)} from any thread. + */ +public final class RefWatcher { + + public static final RefWatcher DISABLED = new RefWatcherBuilder<>().build(); + + private final WatchExecutor watchExecutor; + private final DebuggerControl debuggerControl; + private final GcTrigger gcTrigger; + private final HeapDumper heapDumper; + private final HeapDump.Listener heapdumpListener; + private final HeapDump.Builder heapDumpBuilder; + private final Set retainedKeys; + private final ReferenceQueue queue; + + RefWatcher(WatchExecutor watchExecutor, DebuggerControl debuggerControl, GcTrigger gcTrigger, + HeapDumper heapDumper, HeapDump.Listener heapdumpListener, HeapDump.Builder heapDumpBuilder) { + this.watchExecutor = checkNotNull(watchExecutor, "watchExecutor"); + this.debuggerControl = checkNotNull(debuggerControl, "debuggerControl"); + this.gcTrigger = checkNotNull(gcTrigger, "gcTrigger"); + this.heapDumper = checkNotNull(heapDumper, "heapDumper"); + this.heapdumpListener = checkNotNull(heapdumpListener, "heapdumpListener"); + this.heapDumpBuilder = heapDumpBuilder; + retainedKeys = new CopyOnWriteArraySet<>(); + queue = new ReferenceQueue<>(); + } + + /** + * Identical to {@link #watch(Object, String)} with an empty string reference name. + * + * @see #watch(Object, String) + */ + public void watch(Object watchedReference) { + watch(watchedReference, ""); + } + + /** + * Watches the provided references and checks if it can be GCed. This method is non blocking, + * the check is done on the {@link WatchExecutor} this {@link RefWatcher} has been constructed + * with. + * + * @param referenceName An logical identifier for the watched object. + */ + public void watch(Object watchedReference, String referenceName) { + if (this == DISABLED) { + return; + } + checkNotNull(watchedReference, "watchedReference"); + checkNotNull(referenceName, "referenceName"); + final long watchStartNanoTime = System.nanoTime(); + String key = UUID.randomUUID().toString(); + retainedKeys.add(key); + final KeyedWeakReference reference = + new KeyedWeakReference(watchedReference, key, referenceName, queue); + + ensureGoneAsync(watchStartNanoTime, reference); + } + + /** + * LeakCanary will stop watching any references that were passed to {@link #watch(Object, String)} + * so far. + */ + public void clearWatchedReferences() { + retainedKeys.clear(); + } + + boolean isEmpty() { + removeWeaklyReachableReferences(); + return retainedKeys.isEmpty(); + } + + HeapDump.Builder getHeapDumpBuilder() { + return heapDumpBuilder; + } + + Set getRetainedKeys() { + return new HashSet<>(retainedKeys); + } + + private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) { + watchExecutor.execute(new Retryable() { + @Override public Retryable.Result run() { + return ensureGone(reference, watchStartNanoTime); + } + }); + } + + @SuppressWarnings("ReferenceEquality") // Explicitly checking for named null. + Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) { + long gcStartNanoTime = System.nanoTime(); + long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime); + // 从回收的队列 + removeWeaklyReachableReferences(); + + if (debuggerControl.isDebuggerAttached()) { + // The debugger can create false leaks. 排除因为 Debug 造成的泄漏 + return RETRY; + } + if (gone(reference)) { + return DONE; + } + gcTrigger.runGc(); + removeWeaklyReachableReferences(); + if (!gone(reference)) { + long startDumpHeap = System.nanoTime(); + long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime); + // dump Hprof 文件 + File heapDumpFile = heapDumper.dumpHeap(); + if (heapDumpFile == RETRY_LATER) { + // Could not dump the heap. + return RETRY; + } + long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap); + // 分析 Dump 文件 ,内部使用 haha 库分析 + HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key) + .referenceName(reference.name) + .watchDurationMs(watchDurationMs) + .gcDurationMs(gcDurationMs) + .heapDumpDurationMs(heapDumpDurationMs) + .build(); + + heapdumpListener.analyze(heapDump); + } + return DONE; + } + + private boolean gone(KeyedWeakReference reference) { + return !retainedKeys.contains(reference.key); + } + + private void removeWeaklyReachableReferences() { + // WeakReferences are enqueued as soon as the object to which they point to becomes weakly + // reachable. This is before finalization or garbage collection has actually happened. + KeyedWeakReference ref; + while ((ref = (KeyedWeakReference) queue.poll()) != null) { + retainedKeys.remove(ref.key); + } + } +} diff --git a/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/RefWatcherBuilder.java b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/RefWatcherBuilder.java new file mode 100644 index 0000000..866e35c --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/RefWatcherBuilder.java @@ -0,0 +1,154 @@ +package com.squareup.leakcanary; + +import java.util.Collections; +import java.util.List; + +/** + * Responsible for building {@link RefWatcher} instances. Subclasses should provide sane defaults + * for the platform they support. + */ +public class RefWatcherBuilder> { + + private HeapDump.Listener heapDumpListener; + private DebuggerControl debuggerControl; + private HeapDumper heapDumper; + private WatchExecutor watchExecutor; + private GcTrigger gcTrigger; + private final HeapDump.Builder heapDumpBuilder; + + public RefWatcherBuilder() { + heapDumpBuilder = new HeapDump.Builder(); + } + + /** @see HeapDump.Listener */ + public final T heapDumpListener(HeapDump.Listener heapDumpListener) { + this.heapDumpListener = heapDumpListener; + return self(); + } + + /** @see ExcludedRefs */ + public final T excludedRefs(ExcludedRefs excludedRefs) { + heapDumpBuilder.excludedRefs(excludedRefs); + return self(); + } + + /** @see HeapDumper */ + public final T heapDumper(HeapDumper heapDumper) { + this.heapDumper = heapDumper; + return self(); + } + + /** @see DebuggerControl */ + public final T debuggerControl(DebuggerControl debuggerControl) { + this.debuggerControl = debuggerControl; + return self(); + } + + /** @see WatchExecutor */ + public final T watchExecutor(WatchExecutor watchExecutor) { + this.watchExecutor = watchExecutor; + return self(); + } + + /** @see GcTrigger */ + public final T gcTrigger(GcTrigger gcTrigger) { + this.gcTrigger = gcTrigger; + return self(); + } + + /** @see Reachability.Inspector */ + public final T stethoscopeClasses( + List> stethoscopeClasses) { + heapDumpBuilder.reachabilityInspectorClasses(stethoscopeClasses); + return self(); + } + + /** + * Whether LeakCanary should compute the retained heap size when a leak is detected. False by + * default, because computing the retained heap size takes a long time. + */ + public final T computeRetainedHeapSize(boolean computeRetainedHeapSize) { + heapDumpBuilder.computeRetainedHeapSize(computeRetainedHeapSize); + return self(); + } + + /** Creates a {@link RefWatcher}. */ + public final RefWatcher build() { + if (isDisabled()) { + return RefWatcher.DISABLED; + } + + if (heapDumpBuilder.excludedRefs == null) { + heapDumpBuilder.excludedRefs(defaultExcludedRefs()); + } + + HeapDump.Listener heapDumpListener = this.heapDumpListener; + if (heapDumpListener == null) { + heapDumpListener = defaultHeapDumpListener(); + } + + DebuggerControl debuggerControl = this.debuggerControl; + if (debuggerControl == null) { + debuggerControl = defaultDebuggerControl(); + } + + HeapDumper heapDumper = this.heapDumper; + if (heapDumper == null) { + heapDumper = defaultHeapDumper(); + } + + WatchExecutor watchExecutor = this.watchExecutor; + if (watchExecutor == null) { + watchExecutor = defaultWatchExecutor(); + } + + GcTrigger gcTrigger = this.gcTrigger; + if (gcTrigger == null) { + gcTrigger = defaultGcTrigger(); + } + + if (heapDumpBuilder.reachabilityInspectorClasses == null) { + heapDumpBuilder.reachabilityInspectorClasses(defaultReachabilityInspectorClasses()); + } + + return new RefWatcher(watchExecutor, debuggerControl, gcTrigger, heapDumper, heapDumpListener, + heapDumpBuilder); + } + + protected boolean isDisabled() { + return false; + } + + protected GcTrigger defaultGcTrigger() { + return GcTrigger.DEFAULT; + } + + protected DebuggerControl defaultDebuggerControl() { + return DebuggerControl.NONE; + } + + protected ExcludedRefs defaultExcludedRefs() { + return ExcludedRefs.builder().build(); + } + + protected HeapDumper defaultHeapDumper() { + return HeapDumper.NONE; + } + + protected HeapDump.Listener defaultHeapDumpListener() { + return HeapDump.Listener.NONE; + } + + protected WatchExecutor defaultWatchExecutor() { + return WatchExecutor.NONE; + } + + protected List> defaultReachabilityInspectorClasses() { + return Collections.emptyList(); + } + + @SuppressWarnings("unchecked") + protected final T self() { + return (T) this; + } +} diff --git a/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/Retryable.java b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/Retryable.java new file mode 100644 index 0000000..d9d19d4 --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/Retryable.java @@ -0,0 +1,11 @@ +package com.squareup.leakcanary; + +/** A unit of work that can be retried later. */ +public interface Retryable { + + enum Result { + DONE, RETRY + } + + Result run(); +} diff --git a/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/ShortestPathFinder.java b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/ShortestPathFinder.java new file mode 100644 index 0000000..289c843 --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/ShortestPathFinder.java @@ -0,0 +1,325 @@ +/* + * 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.squareup.leakcanary; + +import com.squareuo.haha.perflib.HahaSpy; +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.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 java.util.ArrayDeque; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; + +import static com.squareup.leakcanary.HahaHelper.isPrimitiveOrWrapperArray; +import static com.squareup.leakcanary.HahaHelper.isPrimitiveWrapper; +import static com.squareup.leakcanary.HahaHelper.threadName; +import static com.squareup.leakcanary.LeakTraceElement.Type.ARRAY_ENTRY; +import static com.squareup.leakcanary.LeakTraceElement.Type.INSTANCE_FIELD; +import static com.squareup.leakcanary.LeakTraceElement.Type.LOCAL; +import static com.squareup.leakcanary.LeakTraceElement.Type.STATIC_FIELD; + +/** + * Not thread safe. + * + * Finds the shortest path from a leaking reference to a gc root, ignoring excluded + * refs first and then including the ones that are not "always ignorable" as needed if no path is + * found. + */ +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; + toVisitQueue = new ArrayDeque<>(); + toVisitIfNoPathQueue = new ArrayDeque<>(); + toVisitSet = new LinkedHashSet<>(); + toVisitIfNoPathSet = new LinkedHashSet<>(); + visitedSet = new LinkedHashSet<>(); + } + + static final class Result { + final LeakNode leakingNode; + final boolean excludingKnownLeaks; + + Result(LeakNode leakingNode, boolean excludingKnownLeaks) { + this.leakingNode = leakingNode; + this.excludingKnownLeaks = excludingKnownLeaks; + } + } + + Result findPath(Snapshot snapshot, Instance leakingRef) { + clearState(); + canIgnoreStrings = !isString(leakingRef); + + enqueueGcRoots(snapshot); + + boolean excludingKnownLeaks = false; + LeakNode leakingNode = null; + while (!toVisitQueue.isEmpty() || !toVisitIfNoPathQueue.isEmpty()) { + LeakNode node; + if (!toVisitQueue.isEmpty()) { + node = toVisitQueue.poll(); + } else { + node = toVisitIfNoPathQueue.poll(); + if (node.exclusion == null) { + throw new IllegalStateException("Expected node to have an exclusion " + node); + } + excludingKnownLeaks = true; + } + + // Termination + if (node.instance == leakingRef) { + leakingNode = node; + break; + } + + if (checkSeen(node)) { + continue; + } + + if (node.instance instanceof RootObj) { + visitRootObj(node); + } else if (node.instance instanceof ClassObj) { + visitClassObj(node); + } else if (node.instance instanceof ClassInstance) { + visitClassInstance(node); + } else if (node.instance instanceof ArrayInstance) { + visitArrayInstance(node); + } else { + throw new IllegalStateException("Unexpected type for " + node.instance); + } + } + return new Result(leakingNode, excludingKnownLeaks); + } + + private void clearState() { + toVisitQueue.clear(); + toVisitIfNoPathQueue.clear(); + toVisitSet.clear(); + toVisitIfNoPathSet.clear(); + visitedSet.clear(); + } + + private void enqueueGcRoots(Snapshot snapshot) { + for (RootObj rootObj : snapshot.getGCRoots()) { + switch (rootObj.getRootType()) { + case JAVA_LOCAL: + Instance thread = HahaSpy.allocatingThread(rootObj); + String threadName = threadName(thread); + Exclusion params = excludedRefs.threadNames.get(threadName); + if (params == null || !params.alwaysExclude) { + enqueue(params, null, rootObj, null); + } + break; + case INTERNED_STRING: + case DEBUGGER: + case INVALID_TYPE: + // An object that is unreachable from any other root, but not a root itself. + case UNREACHABLE: + case UNKNOWN: + // An object that is in a queue, waiting for a finalizer to run. + case FINALIZING: + break; + case SYSTEM_CLASS: + case VM_INTERNAL: + // A local variable in native code. + case NATIVE_LOCAL: + // A global variable in native code. + case NATIVE_STATIC: + // An object that was referenced from an active thread block. + case THREAD_BLOCK: + // Everything that called the wait() or notify() methods, or that is synchronized. + case BUSY_MONITOR: + case NATIVE_MONITOR: + case REFERENCE_CLEANUP: + // Input or output parameters in native code. + case NATIVE_STACK: + case JAVA_STATIC: + enqueue(null, null, rootObj, null); + break; + default: + throw new UnsupportedOperationException("Unknown root type:" + rootObj.getRootType()); + } + } + } + + private boolean checkSeen(LeakNode node) { + return !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); + // We switch the parent node with the thread instance that holds + // the local reference. + Exclusion exclusion = null; + if (node.exclusion != null) { + exclusion = node.exclusion; + } + LeakNode parent = new LeakNode(null, holder, null, null); + enqueue(exclusion, parent, child, new LeakReference(LOCAL, null, null)); + } else { + enqueue(null, node, child, null); + } + } + + private void visitClassObj(LeakNode node) { + ClassObj classObj = (ClassObj) node.instance; + Map ignoredStaticFields = + excludedRefs.staticFieldNameByClassName.get(classObj.getClassName()); + for (Map.Entry entry : classObj.getStaticFieldValues().entrySet()) { + Field field = entry.getKey(); + if (field.getType() != Type.OBJECT) { + continue; + } + String fieldName = field.getName(); + if (fieldName.equals("$staticOverhead")) { + continue; + } + Instance child = (Instance) entry.getValue(); + boolean visit = true; + String fieldValue = entry.getValue() == null ? "null" : entry.getValue().toString(); + LeakReference leakReference = new LeakReference(STATIC_FIELD, fieldName, fieldValue); + if (ignoredStaticFields != null) { + Exclusion params = ignoredStaticFields.get(fieldName); + if (params != null) { + visit = false; + if (!params.alwaysExclude) { + enqueue(params, node, child, leakReference); + } + } + } + if (visit) { + enqueue(null, node, child, leakReference); + } + } + } + + private void visitClassInstance(LeakNode node) { + ClassInstance classInstance = (ClassInstance) node.instance; + Map ignoredFields = new LinkedHashMap<>(); + ClassObj superClassObj = classInstance.getClassObj(); + Exclusion classExclusion = null; + while (superClassObj != null) { + Exclusion params = excludedRefs.classNames.get(superClassObj.getClassName()); + if (params != null) { + // true overrides null or false. + if (classExclusion == null || !classExclusion.alwaysExclude) { + classExclusion = params; + } + } + Map classIgnoredFields = + excludedRefs.fieldNameByClassName.get(superClassObj.getClassName()); + if (classIgnoredFields != null) { + ignoredFields.putAll(classIgnoredFields); + } + superClassObj = superClassObj.getSuperClassObj(); + } + + if (classExclusion != null && classExclusion.alwaysExclude) { + return; + } + + for (ClassInstance.FieldValue fieldValue : classInstance.getValues()) { + Exclusion fieldExclusion = classExclusion; + Field field = fieldValue.getField(); + if (field.getType() != Type.OBJECT) { + continue; + } + Instance child = (Instance) fieldValue.getValue(); + String fieldName = field.getName(); + Exclusion params = ignoredFields.get(fieldName); + // If we found a field exclusion and it's stronger than a class exclusion + if (params != null && (fieldExclusion == null || (params.alwaysExclude + && !fieldExclusion.alwaysExclude))) { + fieldExclusion = params; + } + String value = fieldValue.getValue() == null ? "null" : fieldValue.getValue().toString(); + enqueue(fieldExclusion, node, child, new LeakReference(INSTANCE_FIELD, fieldName, value)); + } + } + + private void visitArrayInstance(LeakNode node) { + ArrayInstance arrayInstance = (ArrayInstance) node.instance; + Type arrayType = arrayInstance.getArrayType(); + if (arrayType == 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(); + enqueue(null, node, child, new LeakReference(ARRAY_ENTRY, name, value)); + } + } + } + + private void enqueue(Exclusion exclusion, LeakNode parent, Instance child, + LeakReference leakReference) { + if (child == null) { + return; + } + if (isPrimitiveOrWrapperArray(child) || isPrimitiveWrapper(child)) { + return; + } + // Whether we want to visit now or later, we should skip if this is already to visit. + if (toVisitSet.contains(child)) { + return; + } + boolean visitNow = exclusion == null; + if (!visitNow && toVisitIfNoPathSet.contains(child)) { + return; + } + if (canIgnoreStrings && isString(child)) { + return; + } + if (visitedSet.contains(child)) { + return; + } + LeakNode childNode = new LeakNode(exclusion, child, parent, leakReference); + if (visitNow) { + toVisitSet.add(child); + toVisitQueue.add(childNode); + } else { + toVisitIfNoPathSet.add(child); + toVisitIfNoPathQueue.add(childNode); + } + } + + private boolean isString(Instance instance) { + return instance.getClassObj() != null && instance.getClassObj() + .getClassName() + .equals(String.class.getName()); + } +} diff --git a/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/TrackedReference.java b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/TrackedReference.java new file mode 100644 index 0000000..a80615f --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/TrackedReference.java @@ -0,0 +1,33 @@ +package com.squareup.leakcanary; + + +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}. */ + public final String key; + + /** Corresponds to {@link KeyedWeakReference#name}. */ + public final String name; + + /** Class of the tracked instance. */ + public final String className; + + /** List of all fields (member and static) for that instance. */ + public final List fields; + + public TrackedReference( String key, String name, String className, + List fields) { + this.key = key; + this.name = name; + this.className = className; + this.fields = Collections.unmodifiableList(new ArrayList<>(fields)); + } +} diff --git a/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/WatchExecutor.java b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/WatchExecutor.java new file mode 100644 index 0000000..9446877 --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/squareup/leakcanary/WatchExecutor.java @@ -0,0 +1,14 @@ +package com.squareup.leakcanary; + +/** + * A {@link WatchExecutor} is in charge of executing a {@link Retryable} in the future, and retry + * later if needed. + */ +public interface WatchExecutor { + WatchExecutor NONE = new WatchExecutor() { + @Override public void execute(Retryable retryable) { + } + }; + + void execute(Retryable retryable); +} diff --git a/MyApp.hprof b/MyApp.hprof new file mode 100644 index 0000000..785f0c6 Binary files /dev/null and b/MyApp.hprof differ diff --git a/myhprof.hprof b/myhprof.hprof new file mode 100644 index 0000000..8293272 Binary files /dev/null and b/myhprof.hprof differ