diff --git a/DuplicatedBitmapAnalyzer/build.gradle b/DuplicatedBitmapAnalyzer/build.gradle index 912e192..3c1c7d7 100644 --- a/DuplicatedBitmapAnalyzer/build.gradle +++ b/DuplicatedBitmapAnalyzer/build.gradle @@ -3,9 +3,11 @@ apply plugin: 'java' version 1.0 +sourceCompatibility = 1.8 dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation 'com.squareup.haha:haha:2.0.4' } diff --git a/DuplicatedBitmapAnalyzer/libs/java-json.jar b/DuplicatedBitmapAnalyzer/libs/java-json.jar new file mode 100644 index 0000000..2f211e3 Binary files /dev/null and b/DuplicatedBitmapAnalyzer/libs/java-json.jar differ diff --git a/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/HahaHelper.java b/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/HahaHelper.java new file mode 100644 index 0000000..580d4b6 --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/HahaHelper.java @@ -0,0 +1,214 @@ +/* + * 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; + +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.Field; +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) { + Instance instance = (Instance) stringObject; + List values = classInstanceValues(instance); + + Integer count = fieldValue(values, "count"); + if (count == 0) { + return ""; + } + + Object value = fieldValue(values, "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"); + } + + 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; + } + + public 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(); + } + + 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/Main.java b/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/Main.java index 202764e..2ddf3d5 100644 --- a/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/Main.java +++ b/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/Main.java @@ -1,13 +1,141 @@ package com.hprof.bitmap; -import java.io.FileInputStream; +import com.squareup.haha.perflib.*; +import com.squareup.haha.perflib.io.HprofBuffer; +import com.squareup.haha.perflib.io.MemoryMappedFileBuffer; +import org.json.JSONArray; +import org.json.JSONObject; +import sun.security.provider.MD5; + +import java.io.File; import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.util.ArrayList; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.*; public class Main { public static void main(String[] args) throws IOException { + String HPROF_PATH = "F:/convert.hprof"; + if(args.length > 0){ + HPROF_PATH = args[0]; + } + File hprofFile = new File(HPROF_PATH); + if(!hprofFile.exists()){ + System.out.printf("file: " + HPROF_PATH + " not exist, please check it"); + return; + } + Map> objMap = new HashMap>(); + HprofBuffer buffer = new MemoryMappedFileBuffer(hprofFile); + HprofParser parser = new HprofParser(buffer); + Snapshot snapshot = parser.parse(); + Heap defaultHeap = null, appHeap = null; + Collection heaps = snapshot.getHeaps(); + //找到 default 和 app heap + for (Heap heap : heaps) { + if (heap.getName().equals("default")) { + defaultHeap = heap; + } else if (heap.getName().equals("app")) { + appHeap = heap; + } else { + //ignore + } + } + ClassObj bitmapClass = snapshot.findClass("android.graphics.Bitmap"); + if(defaultHeap != null){ + analyzeHeapForSameBuffer(objMap, defaultHeap, bitmapClass); + } + if(appHeap != null){ + analyzeHeapForSameBuffer(objMap,appHeap , bitmapClass); + } + dumpSameBufferBitmapInfo(objMap); + } + + /** + * 处理 heap,找出有相同 buffer 的 bitmap 的 instance,存放在 map 中。 + * @param objMap 存放相同 buffer 的 instance 的map + * @param heap 待处理的 heap + * @param classObj 这里是 bitmap 的封装对象 + */ + private static void analyzeHeapForSameBuffer(Map> objMap, + Heap heap, ClassObj classObj){ + List instances = classObj.getHeapInstances(heap.getId()); + for (Instance instance : instances){ + ArrayInstance buffer = HahaHelper.fieldValue(HahaHelper.classInstanceValues(instance), "mBuffer"); + byte[] bytes = HahaHelper.getByteArray(buffer); + try { + String md5String = Md5Helper.getMd5(bytes); + if(objMap.containsKey(md5String)){ + objMap.get(md5String).add(getObjNode(instance)); + }else { + ArrayList objNodes = new ArrayList<>(); + objNodes.add(getObjNode(instance)); + objMap.put(md5String, objNodes); + } + }catch (Exception e){ + e.printStackTrace(); + } + } + + } + + /** + * 封装 Instance 对象,包含 Instance 对象和其 trace + * @param instance + * @return + */ + private static ObjNode getObjNode(Instance instance){ + ObjNode objNode = new ObjNode(); + objNode.setInstance(instance); + objNode.setTrace(getTraceFromInstance(instance)); + return objNode; + } + + /** + * 获取 trace + * @param instance + * @return + */ + private static ArrayList getTraceFromInstance(Instance instance){ + ArrayList arrayList = new ArrayList<>(); + Instance nextInstance = null; + while ((nextInstance = instance.getNextInstanceToGcRoot()) != null){ + arrayList.add(nextInstance); + instance = nextInstance; + } + return arrayList; + } + + /** + * 根据 map 的内容,生成 json 格式的输出 + * @param map + */ + private static void dumpSameBufferBitmapInfo(Map> map){ + JSONArray jsonResult = new JSONArray(); + Iterator iterator = map.entrySet().iterator(); + while (iterator.hasNext()){ + Map.Entry entry = (Map.Entry)iterator.next(); + if(((ArrayList)entry.getValue()).size() > 1){ + try { + ArrayList objNodeArrayList = (ArrayList)entry.getValue(); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("duplcateCount", objNodeArrayList.size()); + jsonObject.put("bufferHash", entry.getKey()); + jsonObject.put("width", + (int)HahaHelper.fieldValue(((ClassInstance)objNodeArrayList.get(0).getInstance()).getValues(), "mWidth")); + jsonObject.put("height", + (int)HahaHelper.fieldValue(((ClassInstance)objNodeArrayList.get(0).getInstance()).getValues(), "mHeight")); + JSONArray traceJsonArray = new JSONArray(); + for (ObjNode objNode : objNodeArrayList){ + traceJsonArray.put(objNode.getTraceString()); + } + jsonObject.put("stacks", traceJsonArray); + jsonResult.put(jsonObject); + }catch (Exception e){ + e.printStackTrace(); + } + } + } + System.out.printf("result: " + jsonResult.toString()); } } diff --git a/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/Md5Helper.java b/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/Md5Helper.java new file mode 100644 index 0000000..deaa493 --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/Md5Helper.java @@ -0,0 +1,28 @@ +package com.hprof.bitmap; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Created by shengmingxu on 2018/12/11. + */ +public class Md5Helper { + // 计算字节流的md5值 + public static String getMd5(byte[] bytes) throws Exception { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + md5.update(bytes, 0, bytes.length); + return byteArrayToHex(md5.digest()).toLowerCase(); + } + + private static String byteArrayToHex(byte[] byteArray) { + char[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + char[] resultCharArray = new char[byteArray.length * 2]; + int index = 0; + for (byte b : byteArray) { + resultCharArray[index++] = hexDigits[b >>> 4 & 0xf]; + resultCharArray[index++] = hexDigits[b & 0xf]; + } + return new String(resultCharArray); + } + +} diff --git a/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/ObjNode.java b/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/ObjNode.java new file mode 100644 index 0000000..9a8da3e --- /dev/null +++ b/DuplicatedBitmapAnalyzer/src/com/hprof/bitmap/ObjNode.java @@ -0,0 +1,40 @@ +package com.hprof.bitmap; + +import com.squareup.haha.perflib.Instance; + +import java.util.ArrayList; + +/** + * Created by shengmingxu on 2018/12/11. + */ +public class ObjNode { + private Instance instance; + private ArrayList trace; + + public Instance getInstance() { + return instance; + } + + public void setInstance(Instance instance) { + this.instance = instance; + } + + public ArrayList getTrace() { + return trace; + } + + public void setTrace(ArrayList trace) { + this.trace = trace; + } + + public String getTraceString(){ + StringBuilder stringBuilder = new StringBuilder(); + if(trace != null && trace.size() > 0){ + for (Instance instance : trace){ + stringBuilder.append(instance.getClassObj().getClassName()); + stringBuilder.append("\n"); + } + } + return stringBuilder.toString(); + } +}