Android客户端内置内存工具进行崩溃定位的实践经验

ee3374 8年前

来自: http://blog.kifile.com/android/2016/01/27/android_client_memory_analysis.html


前言

本宝宝苦啊,辛辛苦苦上线一个版本,上线之后,看到崩溃日志,感觉整个人都不好了.

别人家的崩溃日志是这样子的:

 1 Fatal Exception: java.lang.NullPointerException   2        at com.*.*.*.*$4.run(*.java:537)   3        at android.os.Handler.handleCallback(Handler.java:733)   4        at android.os.Handler.dispatchMessage(Handler.java:95)   5        at android.os.Looper.loop(Looper.java:136)   6        at android.app.ActivityThread.main(ActivityThread.java:5314)   7        at java.lang.reflect.Method.invokeNative(Method.java)   8        at java.lang.reflect.Method.invoke(Method.java:515)   9        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:862)  10        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:678)  11        at dalvik.system.NativeStart.main(NativeStart.java)

我们家的崩溃日志是这样子的:

 1 Fatal Exception: java.lang.RuntimeException: Canvas: trying to use a recycled bitmap android.graphics.Bitmap@5e844ef   2        at android.graphics.Canvas.throwIfCannotDraw(Canvas.java:1282)   3        at android.view.GLES20Canvas.drawBitmap(GLES20Canvas.java:599)   4        at android.graphics.drawable.BitmapDrawable.draw(BitmapDrawable.java:538)   5        at android.view.View.getDrawableRenderNode(View.java:15766)   6        at android.view.View.drawBackground(View.java:15712)   7        at android.view.View.draw(View.java:15479)   8        at android.widget.FrameLayout.draw(FrameLayout.java:658)   9        at android.view.View.updateDisplayListIfDirty(View.java:14384)  10        at android.view.View.getDisplayList(View.java:14413)  11        ...  12        at android.os.Handler.dispatchMessage(Handler.java:104)  13        at android.os.Looper.loop(Looper.java:194)  14        at android.app.ActivityThread.main(ActivityThread.java:5631)  15        at java.lang.reflect.Method.invoke(Method.java)  16        at java.lang.reflect.Method.invoke(Method.java:372)  17        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:959)  18        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:754)

全部是Android Framework里的崩溃啊,表示不服.

再瞅瞅啊,虽然能够看出来是由于某个Bitmap被回收导致的Crash,但是你妹的,竟然崩溃栈完全没有看到自己的代码,让我怎么定位,我不想一个一个界面去 排除啊.崩溃栈你不能直接显示出是哪一个View崩溃了吗?把View类的类名暴露给我看看也好啊.

面对这种情况,让我一个一个排除肯定是不干的,让我想想怎么办呢?

提出方案

1.第一个方案,也就是想想有没有办法拿到真实的调用类名.

想法是好的,但是发现没法进行注入啊,Java的UncaughtExceptionHandler中虽然传入了Thread对象和Throwable对象,但是这两个对象通过查看引用关系,发现没法找到真实类,所以放弃.

2.第二个方案,看看能不能通过method hook的形式去进行注入

上网查了一下,目前网上流行的java方法注入其实基本上都是在Dalvik虚拟机下通过类似Dexposed的形式进行注入,而对于ART虚拟机貌似现在还没有一个很好的方案,所以暂时列为备选吧,如果没有别的更好的方案,就使用版本控制进行使用了.

3.最后实在是没办法了,不如我们来针对崩溃时的内存堆栈进行分析吧

反正我们知道现在的问题是由于Bitmap被recycle掉了,那么如果我们通过堆栈分析找到那个被recycle掉的bitmap,然后在去分析它的引用关系不就好了吗?

选择内存分析库

内存分析啊,得有工具啊.虽然Eclipse和AndroidStudio下都有内存分析工具,但是我不想在把用户的内存信息整个传过来,一个内存堆栈至少20m,太恐怖了,我要在客户端进行分析啊,怎么办,怎么办?

话说最近那啥LeakCanary不是很火吗?他不是也对对象的引用关系做了内存分析吗?

就看看它是怎么分析内存泄露的呗,查了一下,发现他是移植了mat的内存分析工具,然后建了一个叫做haha的 一个开源库.

再定睛一看,咦,原来他还有一个2.0版本,是移植于AndroidStudio的perflib, 恩,就是他了,就用它来分析了.

开始内存分析

确定内存分析点

我们不可能随时随地去做内存dump,毕竟dump内存的时候,会导致当前进程所有的线程冻结,因此我们只能选择在应用Crash的时候做内存dump.

为了尽可能降低内存dump的几率,我们只能够在自定义的UncaughtExceptionHandler对传入的Throwable对象进行判断,对指定的异常进行内存分析.

例如这里:我会判断Throwable的message中,是否以Canvas: trying to use a recycled bitmap android.graphics.Bitmap开头,以确保这是我想要进行内存分析的崩溃点.

新开进程进行分析

由于分析内存耗时挺久的,在我的机器上大概耗时30s,那么在用户的机器上只可能耗时更久,因此我在这里单开一个内存分析进程进行内存分析.

首先获取内存快照:

1 HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);  2 HprofParser parser = new HprofParser(buffer);  3 Snapshot snapshot = parser.parse();

然后对内存快照中的对象建立他们的链接关系,是的你没有看错,通过上面的代码获得的快照对象中还没有对对象间的引用关系做关联,需要我们手动调一个方法:

1 snapshot.computeDominators();

计算之后,我们来找一下Bitmap类对象

1 Collection<ClassObj> classes = snapshot.findClasses(Bitmap.class.getName());  2 Collection<Heap> heaps = snapshot.getHeaps();  3 for (Heap heap : heaps) {  4     for (ClassObj clazz : classes) {  5         List<Instance> instances = clazz.getHeapInstances(heap.getId());  6     }  7 }

这里有一个坑,其实snapshot也是从每个heap上获取他的ClassObj列表的,但是可能出现这个heap上的ClassObj对象出现在了另一个heap中的情况,因此我们不能直接获取heap的ClassObj列表,需要直接从snapshot总获取ClassObj列表.

通过以上的代码我们能够获取到当前快照中所有的Bitmap对象,之后,我们通过对对象中的字段进行过滤,可以判断它是否被Recycle掉.

 1 instance.accept(new Visitor(){   2     ...   3     public void visitClassInstance(ClassInstance instance) {   4         List<FieldValue> values = instance.getFields();   5         for (FieldValue value : values) {   6             if ("mRecycled".equals(value.getField().getName()) {   7                 if (value.getValue() == true) {   8                     ...   9                 }  10                 break;  11             }  12         }  13     }  14     ...  15 })

注意 value.getValue()只对基本类型会生成他的对象实例,对于其他类型则会生成一个ClassInstance对象.

通过上面的代码我们就能够找到我们需要找到的那个被recycle掉的bitmap,然后再去找他的引用关系.

刚才我们说过了,需要调用snapshot.computeDominators();去计算引用关系,计算完成之后,我们可以使用instance.getHardReferences()来获取关于他的强引用对象.

之后再通过一系列的类路径匹配,我们终于能够找到究竟是哪个类出问题了.这个路径匹配问题就不再这里说了,大家自己解决吧

总结

通过上面的做法,我们现在已经能够在用户Crash的时候获取到他的内存信息,并让用户的机器自己分析内存问题.

但这样有个局限,我们只能够针对特定的Crash进行验证分析,也就是说我们发现这种问题后,需要先迭代一个版本去获取用户崩溃的原因,然后才能够发一个fix版本.

所以我这边在考虑如何通过特定语法或配置文件去指明他的crash特征,通过服务器下发特征数据,减少用于验证问题的版本迭代.