From 14f1948a9b96241d32e445600ef52622e6370ccf Mon Sep 17 00:00:00 2001 From: freshxu Date: Fri, 29 Jul 2022 20:03:47 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E4=BD=9C=E4=B8=9A=E5=8F=8ADe?= =?UTF-8?q?mo=EF=BC=8C=E5=AE=9E=E7=8E=B0=E5=86=85=E5=AD=98=E4=B8=AD?= =?UTF-8?q?=E9=87=8D=E5=A4=8DBitmap=E7=9A=84=E6=A3=80=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- hprof/.gitignore | 1 + hprof/build.gradle | 39 ++++ hprof/proguard-rules.pro | 21 ++ .../hprof/ExampleInstrumentedTest.java | 25 +++ hprof/src/main/AndroidManifest.xml | 23 +++ .../java/com/example/hprof/MainActivity.java | 53 ++++++ .../com/example/hprof/util/HahaHelper.java | 180 ++++++++++++++++++ .../com/example/hprof/util/HprofAnalysis.java | 110 +++++++++++ .../com/example/hprof/util/Preconditions.java | 20 ++ .../drawable-v24/ic_launcher_foreground.xml | 30 +++ hprof/src/main/res/drawable/down_faile.png | Bin 0 -> 2311 bytes hprof/src/main/res/drawable/down_focus.png | Bin 0 -> 2318 bytes .../res/drawable/ic_launcher_background.xml | 170 +++++++++++++++++ hprof/src/main/res/layout/activity_main.xml | 39 ++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes hprof/src/main/res/values-night/themes.xml | 10 + hprof/src/main/res/values/colors.xml | 10 + hprof/src/main/res/values/strings.xml | 3 + hprof/src/main/res/values/themes.xml | 10 + .../com/example/hprof/ExampleUnitTest.java | 17 ++ settings.gradle | 1 + 34 files changed, 774 insertions(+), 2 deletions(-) create mode 100644 hprof/.gitignore create mode 100644 hprof/build.gradle create mode 100644 hprof/proguard-rules.pro create mode 100644 hprof/src/androidTest/java/com/example/hprof/ExampleInstrumentedTest.java create mode 100644 hprof/src/main/AndroidManifest.xml create mode 100644 hprof/src/main/java/com/example/hprof/MainActivity.java create mode 100644 hprof/src/main/java/com/example/hprof/util/HahaHelper.java create mode 100644 hprof/src/main/java/com/example/hprof/util/HprofAnalysis.java create mode 100644 hprof/src/main/java/com/example/hprof/util/Preconditions.java create mode 100644 hprof/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 hprof/src/main/res/drawable/down_faile.png create mode 100644 hprof/src/main/res/drawable/down_focus.png create mode 100644 hprof/src/main/res/drawable/ic_launcher_background.xml create mode 100644 hprof/src/main/res/layout/activity_main.xml create mode 100644 hprof/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 hprof/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 hprof/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 hprof/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 hprof/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 hprof/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 hprof/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 hprof/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 hprof/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 hprof/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 hprof/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 hprof/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 hprof/src/main/res/values-night/themes.xml create mode 100644 hprof/src/main/res/values/colors.xml create mode 100644 hprof/src/main/res/values/strings.xml create mode 100644 hprof/src/main/res/values/themes.xml create mode 100644 hprof/src/test/java/com/example/hprof/ExampleUnitTest.java 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 0000000000000000000000000000000000000000..38d935780daeda3515905a7fcb089fc3f6086385 GIT binary patch literal 2311 zcmaJ@dpwi-A78hVOZ-|SB+cWRvWx3BMq|THrre@J?edHr+a9(VsW_o?Hjy$FDLE*e zu4hiUMIs})q?C?Y2&Fl`Qwpn@%GSA(^rGR zU>X!plCRQYW?z*blwZL~qJh%r3CTf1KZqraq47Z&kqOa31cgfr1ARdnb9YoD=mdi; zvS9lM34^F!cm~8p(q?0jvD`={8U}N6iH)Q&B0wR64u-LL1YqQPHGp6<2|%D76-|wF z1H;*#aeUA(&fA|67s0?W0T*Y4Q!HLdzy*agL@bxX6X0VBz#K1Ld7gEn0K{B~FoFPl z78OL@f^dWQAi@r5XT?BUV-OBFB*xCp!NGb1!WxaSMWHb$jExlrhqtxGqtS>j51`D( zXR`3VB=;}5ls5tpE)+)MQK*=h7-WnM65@xUFgP3zg|uY#1t*7Ky?j(X%DZ2U4m3Ka|V; z8Z8j|g8%CMCb7VOcO;1N1qDzvpP?Ka%Xl_bB;Jh=(u5G-AA&gZUEC552_Zo^6p3(i zo9i0Ff=Xksd9%)Sb0{hmPvHrKG#&$_kO+X12gznL@irJ6Yj+F=M@HKy4cV6HiYAeW zSbHoQO|nCy-RH3+h!M>NdBS-t^It6SyV%)<;6^GllR!RO1Tx+E5En65IG+6tE$$?o zy*-vR%SZmUmd{w`_qAZZs|BSLgPI-h|BQRyrQD&}hEKX9$Oaqz|U_{RW(yNx^P9Wqvgu&C$!heXH=`x92`oATNT3x zq?5(3x=v*aM6A}6f)X6{%&jEd{$)N!Je@$v`nv!2a-t4lH{j(EJ^e3J`-7}e$RO#{ zkFE+!L#+>D*Eeh3ef<6iG0@sQ8Goc~lUHA-=BO;l%3hqy(W7wGd_U6!*Q- zELQD!YahVrE!nl}Oj^`=-5B+_SiLB#Ci9&Nv#m=yj|QaXq#Fia*Qa$cs<4Mvu{FKZ zY;A758qF@lo`)yZEj93e(H^t7XsamCLpGr@_0g_3x+9OBU*5 zub&As;=JE)dztXX=md_8bf{?$SvBmtTET!|1H}ORF`RTyR|S$VuZi z{2{$M$fTln{g#i(|=uk>gGU6oCW%5^30&FYl@Dd_`;CgREt<$zS+>c z=)mg;243=L%=+M(C)@rmtV`9c<;5IPe{12$um*<0%6>g=xh!PJC;c!WHmcTqzV@=) znkSvla9Co9MYn9Un{qS-Yce#D$8cP4A~cCrKX525;W^&g%(yl_eiU2&q(yvMu96`rcoDP;7LGaC&^A>$=v&$Na90x6aHnSRm9i z`NNT5V8B?rDY!7x?}jPyn$@y~kp9iG!^uVU?P7fv-)O4bGb^8GYh3aS+SgH9RV7L+ zcQ9bp)44Qz#Y*k_pzi1G&`gI;D<>c5KvZ z7fVk_%Yh3&qFrsmV|!i;J=gZo_RSO5L0~fu3Dwm z4|$ibR-Ue;tNhs(@wAcWbnD5v;evvtOK<;-zW4e<>du4lf{f#Pf6{xmNtS3DkbPE4 z^y=G4;jMim!Y^@lWLA4l8-)-%gS%t2%Pf84b4K7{<#$Yq>;12kF04B5x7s95a*x|6 z%!oR0v=aUxv)IV4W~O`4y})C2Tr?R|%UtVBNXH+kl9&=tK#LZc=rj--%5vKJTJw(I zZJOG*5IN9h+1t{;xKrwVdAPDI)aCv7hiCWnyTm)1&MsJBJ{DR(@-8AEbMsA|yfg;m z)a{G~=DLtU-_mS-I8U^`b91V!_Fm3Vty_zCsl)~YWSYIE+IX7xWp4dg^-GJvwiSj+ zos@F@M`4(K#!*!xFSv!LeZ~hT_nAl&Ru=aR3T_(*dQT-3EgVY|Ez_9kZoG4>J0nmC d;t$NIqG5Vdgz2z}b94VD6tXwzvg@wIe*oCg)u;df literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7170b0d7b04f75531ed406c92909e0982eab043b GIT binary patch literal 2318 zcmaJ@dpy&7AK!z@BOK%sNjoD{cKL0_Hb%3HsZC<1h&VGpEW6nj+gy4iC!$E2I!KBx z5Aoz0$1g7=IMB?* z!2|+eb z6jMY76S;JS7z|Scvp9+*4n7X%?+5jj5p)E6P|AkN_&kAxAS1#)@e*`<{WTH>{S+ci zBEtR>70wKSl7(UribY`I926RWdgBoQ7VGVe_JE>MfF}|KAOQ>I5s=8#)Ko+&1|bwDAOSock3^x7Xf#|G0hh=HQnm~(kT}mV zP(TSs%oRzwLIG6I$c`0ml@eh(rGJ&c7k!}>NItho*D$1vEkXhal)j|7KqmA5hw}Mf zq9xK$@Nd2UlUTx%i$G*3C=qTIb994?ch;v85y)bYEftDcLLqOiiy?_ZsZf$A6hX=4 zPhEq$Gua%jK!3U76NP#~Sd#{G@Od=;xV1Ye}fOaaB*6flk^7V@E=3MX*Ct_Af~ zzR%dWuWJFmibd+gAob(@uW`>^={%(0elf4^@WuY1K<9a}&fE*oR?`s3y!0Rni6#5F z`^O|6i)yVFHa~FSRapm(Znil!kC$!~X>F}quzu0*hQ#VMW{cA;LarM57v(OyvZ?j} z^azkuys|Rg2>ZQt)!q%;1GdA@=F-yx=T+tGSo4pMZXX0u5BX^cZw1d>+B`;{9K;Xx zbmJ3xJQHNhK#S~hmz64PY{_|g`M1F~*{aOskQtAZ+zpK+&-^y@QeQViXDbb1?C^P} z_4sV2^=x$b&`zpTTV;M$wyY_v1*yzGr#YcrGQpKp9$0mgQaZJ|&URSUW1JBMb2e{hK=2> zc6$elTbaNtvu0b_?d1ha@-3J1t^}$~)~|dbtr$1H!)&W5z*Ho!MPrXdc{bYDK2oJ+ zkKByzI2k|SIx&jYpq1+`pO5xf_TYHd{yM_qv0((Gcz2|D2|z> z*ygp_l2z@qgE{FcC9ACOI!zl~k+;JABZD!&+;!y`YX6MC`8yvwVr^K$wF0i)g{kw7au_G`nuC~-Mbq(!LI*oS%@5$F6%6-z;D5AtGlxLAttA0vdzN8iC#4P z6yZ04x)MLgHw|~G-&2&~HC|4Pp}wwojuW_D?VaesIMXHHI4T)u-|Ta4UAsWm;hd6!}{vV{Z4$Qw#mrv;)&yA8FKt04jK*3dABzTJ!ELz z6;-{%y$wz}2WO}^zwibMp%KGe3LJwk{1D~hd!!!Y?Db5T@u0c2)x7vZ>C$-Bq1vO5 z1wUWBxx0Jt*sbvyhT`egXc;(xEX+X*=aL>Bkw zU#eha^==<=>-4W)Y+453D&@y|trNoO6zaaJO&xXt z^RjsXcg=7_krwwyzntwt%|e*YJpGNguGGV4>{t1{yb-3yh?}9o_<1+`RH9VEAKHY + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + +