diff --git a/build.gradle b/build.gradle index cdf1d5d..d91fd7e 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.2.0' + classpath 'com.android.tools.build:gradle:7.0.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b9cc257..5e73eb6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip diff --git a/hprof/.gitignore b/hprof/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/hprof/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/hprof/build.gradle b/hprof/build.gradle new file mode 100644 index 0000000..319791c --- /dev/null +++ b/hprof/build.gradle @@ -0,0 +1,39 @@ +plugins { + id 'com.android.application' +} + +android { + compileSdk 32 + + defaultConfig { + applicationId "com.example.hprof" + minSdk 21 + targetSdk 32 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + + implementation 'com.android.support:appcompat-v7:28.0.0' + implementation 'com.squareup.haha:haha:2.0.4' + implementation group: 'com.squareup.leakcanary', name: 'leakcanary-watcher', version: '1.6.2' + implementation 'com.android.support.constraint:constraint-layout:2.0.4' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' +} \ No newline at end of file diff --git a/hprof/proguard-rules.pro b/hprof/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/hprof/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/hprof/src/androidTest/java/com/example/hprof/ExampleInstrumentedTest.java b/hprof/src/androidTest/java/com/example/hprof/ExampleInstrumentedTest.java new file mode 100644 index 0000000..449b4a4 --- /dev/null +++ b/hprof/src/androidTest/java/com/example/hprof/ExampleInstrumentedTest.java @@ -0,0 +1,25 @@ +package com.example.hprof; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.example.hprof", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/hprof/src/main/AndroidManifest.xml b/hprof/src/main/AndroidManifest.xml new file mode 100644 index 0000000..14108f1 --- /dev/null +++ b/hprof/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/hprof/src/main/java/com/example/hprof/MainActivity.java b/hprof/src/main/java/com/example/hprof/MainActivity.java new file mode 100644 index 0000000..e5fa065 --- /dev/null +++ b/hprof/src/main/java/com/example/hprof/MainActivity.java @@ -0,0 +1,53 @@ +package com.example.hprof; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Debug; +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; +import android.view.View; +import android.widget.ImageView; + +import com.example.hprof.util.HprofAnalysis; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadPoolExecutor; + +public class MainActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.down_faile); + Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(), R.drawable.down_focus); + Bitmap bitmap3 = BitmapFactory.decodeResource(getResources(), R.drawable.down_faile); + Bitmap bitmap4 = BitmapFactory.decodeResource(getResources(), R.drawable.down_focus); + + ((ImageView) findViewById(R.id.image1)).setImageBitmap(bitmap1); + ((ImageView) findViewById(R.id.image3)).setImageBitmap(bitmap2); + ((ImageView) findViewById(R.id.image2)).setImageBitmap(bitmap3); + ((ImageView) findViewById(R.id.image4)).setImageBitmap(bitmap4); + + findViewById(R.id.dump).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + File file = new File(getExternalCacheDir(), "dump.hprof"); + Executors.newSingleThreadExecutor().execute(new Runnable() { + @Override + public void run() { + try { + Debug.dumpHprofData(file.getAbsolutePath()); + HprofAnalysis.analysis(file.getAbsolutePath()); + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + } + }); + } +} \ No newline at end of file diff --git a/hprof/src/main/java/com/example/hprof/util/HahaHelper.java b/hprof/src/main/java/com/example/hprof/util/HahaHelper.java new file mode 100644 index 0000000..31c68ad --- /dev/null +++ b/hprof/src/main/java/com/example/hprof/util/HahaHelper.java @@ -0,0 +1,180 @@ +package com.example.hprof.util; + +import static com.example.hprof.util.Preconditions.checkNotNull; + +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 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/hprof/src/main/java/com/example/hprof/util/HprofAnalysis.java b/hprof/src/main/java/com/example/hprof/util/HprofAnalysis.java new file mode 100644 index 0000000..061bd45 --- /dev/null +++ b/hprof/src/main/java/com/example/hprof/util/HprofAnalysis.java @@ -0,0 +1,110 @@ +package com.example.hprof.util; + +import android.text.TextUtils; + +import com.squareup.haha.perflib.ArrayInstance; +import com.squareup.haha.perflib.ClassInstance; +import com.squareup.haha.perflib.ClassObj; +import com.squareup.haha.perflib.Heap; +import com.squareup.haha.perflib.HprofParser; +import com.squareup.haha.perflib.Instance; +import com.squareup.haha.perflib.Snapshot; +import com.squareup.haha.perflib.io.HprofBuffer; +import com.squareup.haha.perflib.io.MemoryMappedFileBuffer; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class HprofAnalysis { + + public static void analysis(String hprofPath) throws IOException { + if (TextUtils.isEmpty(hprofPath)) { + return; + } + File hprofFile = new File(hprofPath); + if (!hprofFile.exists()) { + return; + } + HprofBuffer dataBuffer = new MemoryMappedFileBuffer(hprofFile); + HprofParser parser = new HprofParser(dataBuffer); + final Snapshot snapshot = parser.parse(); + snapshot.computeDominators(); + final ClassObj bitmapClass = snapshot.findClass("android.graphics.Bitmap"); + Heap appHeap = snapshot.getHeap("app"); + // 拿到所有的 Bitmap 实例 + final List bitmapInstances = bitmapClass.getHeapInstances(appHeap.getId()); + if (bitmapInstances == null || bitmapInstances.size() <= 1) { + return; + } + + int[] buffers = new int[bitmapInstances.size()]; + for (int i = 0; i < bitmapInstances.size(); i++) { + Instance bitmapInstance = bitmapInstances.get(i); + // mBuffer 是一个 byte[] + ArrayInstance arrayInstance = HahaHelper.fieldValue(((ClassInstance) bitmapInstance).getValues(), "mBuffer"); + buffers[i] = Arrays.hashCode(arrayInstance.getValues()); + } + HashMap> map = new HashMap<>(); + for (int i = 0; i < buffers.length; i++) { + if (!map.containsKey(buffers[i])) { + List list = new ArrayList<>(); + list.add(bitmapInstances.get(i)); + map.put(buffers[i], list); + } else { + map.get(buffers[i]).add(bitmapInstances.get(i)); + } + } + + StringBuilder sb = new StringBuilder(); + for (Map.Entry> entry : map.entrySet()) { + if (entry.getValue().size() > 1) { + sb.append("\"duplcateCount\":" + entry.getValue().size()); + sb.append("\n"); + sb.append("\"stacks\": \n"); + List instanceList = entry.getValue(); + for (int i = 0; i < instanceList.size(); i++) { + sb.append("===================================================== \n"); + sb.append(getTraceString(getTraceFromInstance(instanceList.get(i)))); + sb.append("===================================================== \n"); + } + + sb.append("\"bufferHashcode\":").append("\"").append(entry.getKey().toString()).append("\"\n"); + int width = HahaHelper.fieldValue(((ClassInstance) entry.getValue().get(0)).getValues(), "mWidth"); + int height = HahaHelper.fieldValue(((ClassInstance) entry.getValue().get(0)).getValues(), "mHeight"); + sb.append("\"width\":" + width + "\n"); + sb.append("\"height\":" + height + "\n"); + sb.append("\"bufferSize\":" + entry.getValue().get(0).getSize() + "\n"); + sb.append("----------------------------------------------------- \n"); + } + } + if (!sb.toString().isEmpty()) { + System.out.println(sb); + } + } + + public static ArrayList getTraceFromInstance(Instance instance) { + ArrayList arrayList = new ArrayList<>(); + //Instance nextInstance = null; + while(instance != null && instance.getDistanceToGcRoot() != 0 && instance.getDistanceToGcRoot() != Integer.MAX_VALUE) { + arrayList.add(instance); + instance = instance.getNextInstanceToGcRoot(); + } + return arrayList; + } + + public static String getTraceString(List instances) { + StringBuilder sb = new StringBuilder(); + if (instances.size() > 0) { + for (Instance instance : instances) { + sb.append(instance.getClassObj().getClassName()); + sb.append("\n"); + } + } + return sb.toString(); + } +} diff --git a/hprof/src/main/java/com/example/hprof/util/Preconditions.java b/hprof/src/main/java/com/example/hprof/util/Preconditions.java new file mode 100644 index 0000000..797cb60 --- /dev/null +++ b/hprof/src/main/java/com/example/hprof/util/Preconditions.java @@ -0,0 +1,20 @@ +package com.example.hprof.util; + +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/hprof/src/main/res/drawable-v24/ic_launcher_foreground.xml b/hprof/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/hprof/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/hprof/src/main/res/drawable/down_faile.png b/hprof/src/main/res/drawable/down_faile.png new file mode 100644 index 0000000..38d9357 Binary files /dev/null and b/hprof/src/main/res/drawable/down_faile.png differ diff --git a/hprof/src/main/res/drawable/down_focus.png b/hprof/src/main/res/drawable/down_focus.png new file mode 100644 index 0000000..7170b0d Binary files /dev/null and b/hprof/src/main/res/drawable/down_focus.png differ diff --git a/hprof/src/main/res/drawable/ic_launcher_background.xml b/hprof/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/hprof/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hprof/src/main/res/layout/activity_main.xml b/hprof/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..e757328 --- /dev/null +++ b/hprof/src/main/res/layout/activity_main.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + +