Android客戶端內置內存工具進行崩潰定位的實踐經驗
來自: 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特征,通過服務器下發特征數據,減少用于驗證問題的版本迭代.