经过 dump 虚拟机线程办法栈和堆内存来剖析 Android 卡顿和 OOM 问题
Android
中的功用问题无非便是卡顿和 OOM
,虽然总体就这两种,可是形成这两种功用问题的原因却是十分多,需求具体的原因具体剖析,并且这是十分复杂的。本篇文章仅仅简略介绍如何找到形成这些问题的直接原因的东西(也算是入门剖析),更深层次的问题你或许还需求其他知识。
卡顿(ANR
)通常是主线程堵塞导致,主线程堵塞也或许有许多其他原因,比如在竞争其他线程的锁,在做耗时的运算,等候 UI
制作的 Buffer
等等,咱们要剖析这些问题就需求 dump
一切的线程办法栈,经过这些办法栈再一步一步剖析具体问题。
OOM
通常是堆内存增长到最大的约束,导致程序无法持续运行而导致的溃散,本篇咱们主要剖析的是 Dalvik
虚拟机栈的内存,而没有 Native
栈的内存(而这部分内存也十分重要,后续考虑独自写文章剖析)。
dump 办法栈
假设想要愈加直观的展现卡顿问题其实 Systrace
是一个愈加好用的东西,它可以愈加直观地显示主线程每一帧的耗时,哪一帧对应办法栈(它的办法栈很简略并且没有 native
信息,假设要显示自己栈的信息需求手动在代码添加 trace
。)的耗时信息,还有各种的锁信息,Binder
唤醒信息:
我上面的这个图便是形成了掉帧,主线程有大段时刻是 sleep
状况,在等候 RenderThread
,然后 RenderThread
比较忙在制作前两次的 VSYNC
数据,然后还在等候 IjkPlayer
线程,IjkPlayer
自身有较多的时刻是 Runnable
状况(表明线程等候 CPU
执行,说明先当时时刻 CPU
很忙)。具体想要看 systrace
怎样运用,咱们可以找找其他文章。
systrace
相比照较直观,它是截取了程序运行一个时刻段内的状况,可以清晰发现是哪一帧发生了卡顿,可是相对程序的办法栈信息就比较少,并且这个办法只可以用于调试,无法获取线上用户的数据。
相对于线上用户咱们就不能运用 systrace
的办法,而需求在探测到线程卡顿的时分 dump
出一切线程的办法栈信息相对所以比较好的一个办法。
那么怎样探测卡顿呢?咱们可以经过设置主线程 Looper
中的 Printer
来监听每个主线程的 Message
的执行时刻来判别卡顿,比如这个时分某个 Message
执行超过了 2s 咱们就认为卡顿了。假设不了解 Handler
的同学可以参考我的这篇文章:Android Handler 工作原理
那么怎样 dump
一切线程的栈信息呢?在 Java
和 Android
的环境十分简略直接可以经过以下办法获取到一切的栈信息。
val allStacks = Thread.getAllStackTraces()
for ((t, stack) in allStacks) {
println("Stack in ${t.name}")
for (s in stack) {
println(s)
}
}
以上办法确实简略高效,可是只能获取到简略的 Java
栈,没有 native
信息,没有线程状况,也没有锁状况。
那咱们要怎样才能 dump
愈加具体的栈信息呢?在 Java
环境中可以运用 jdk
中的东西 jstack
来 dump
线程栈信息,可是它在 Android
中并不能用。那在 Android
中要怎样获取具体的栈信息呢?
在 Android
发生 ANR
时会向方针运用进程发送 SIGQUIT
的 Linux
信号,运用进程默许的信号处线程 Signal Catcher
会 dump
一切的线程栈信息和虚拟机的 GC
信息到本地文件,这些文件在 /data/anr/
目录下,他们是文本文件,直接读取就好了。可是你或许会说咱们仅仅想要在卡顿时获取,可是卡顿并不一定会触发 ANR
。确实是这样,可是我不是说过吗,ANR
通知到运用进程是经过 Linux
信号,如何发送一个 Linux
的 SIGQUIT
信号并不难,可以上网搜索一下。当收到 SIGQUIT
信号后,Signal Catcher
看护线程就会把栈信息写入到本地文件。
进入 Android
手机的 shell
指令行终端,可以经过 kill -SIGQUIT [pid]
指令来发送 (推荐运用虚拟机更便利获取 root
权限,运用 adb root
获取权限,然后经过 adb shell
进入终端)。
以下是我测试获取的部分堆信息(我没有列出 GC
信息,感兴趣的自己去看看):
// ...
DALVIK THREADS (25):
"Signal Catcher" daemon prio=10 tid=6 Runnable
| group="system" sCount=0 dsCount=0 flags=0 obj=0x134c0228 self=0xb40000749aad67b0
| sysTid=8055 nice=-20 cgrp=top-app sched=0/0 handle=0x731950fcc0
| state=R schedstat=( 94147207 2576958 55 ) utm=5 stm=4 core=3 HZ=100
| stack=0x7319418000-0x731941a000 stackSize=995KB
| held mutexes= "mutator lock"(shared held)
native: #00 pc 000000000049ee50 /apex/com.android.art/lib64/libart.so (art::DumpNativeStack(std::__1::basic_ostream<char, std::__1::char_traits<char> >&, int, BacktraceMap*, char const*, art::ArtMethod*, void*, bool)+140)
native: #01 pc 00000000005abfa8 /apex/com.android.art/lib64/libart.so (art::Thread::DumpStack(std::__1::basic_ostream<char, std::__1::char_traits<char> >&, bool, BacktraceMap*, bool) const+376)
native: #02 pc 00000000005c90e0 /apex/com.android.art/lib64/libart.so (art::DumpCheckpoint::Run(art::Thread*)+924)
native: #03 pc 00000000005c3020 /apex/com.android.art/lib64/libart.so (art::ThreadList::RunCheckpoint(art::Closure*, art::Closure*)+528)
native: #04 pc 00000000005c21ec /apex/com.android.art/lib64/libart.so (art::ThreadList::Dump(std::__1::basic_ostream<char, std::__1::char_traits<char> >&, bool)+1920)
native: #05 pc 00000000005c168c /apex/com.android.art/lib64/libart.so (art::ThreadList::DumpForSigQuit(std::__1::basic_ostream<char, std::__1::char_traits<char> >&)+776)
native: #06 pc 000000000056d64c /apex/com.android.art/lib64/libart.so (art::Runtime::DumpForSigQuit(std::__1::basic_ostream<char, std::__1::char_traits<char> >&)+196)
native: #07 pc 0000000000582be0 /apex/com.android.art/lib64/libart.so (art::SignalCatcher::HandleSigQuit()+1396)
native: #08 pc 0000000000581bac /apex/com.android.art/lib64/libart.so (art::SignalCatcher::Run(void*)+348)
native: #09 pc 00000000000af8c8 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+64)
native: #10 pc 000000000004fe08 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64)
(no managed stack frames)
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 flags=1 obj=0x725b86a8 self=0xb40000749aad8380
| sysTid=8045 nice=-10 cgrp=top-app sched=0/0 handle=0x75c13d14f8
| state=S schedstat=( 3274180825 2052143817 7347 ) utm=263 stm=63 core=0 HZ=100
| stack=0x7fee25a000-0x7fee25c000 stackSize=8192KB
| held mutexes=
native: #00 pc 000000000009bab8 /apex/com.android.runtime/lib64/bionic/libc.so (__epoll_pwait+8)
native: #01 pc 0000000000019ad0 /system/lib64/libutils.so (android::Looper::pollInner(int)+184)
native: #02 pc 00000000000199b0 /system/lib64/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+112)
native: #03 pc 0000000000110f74 /system/lib64/libandroid_runtime.so (android::android_os_MessageQueue_nativePollOnce(_JNIEnv*, _jobject*, long, int)+44)
at android.os.MessageQueue.nativePollOnce(Native method)
at android.os.MessageQueue.next(MessageQueue.java:335)
at android.os.Looper.loop(Looper.java:183)
at android.app.ActivityThread.main(ActivityThread.java:7656)
at java.lang.reflect.Method.invoke(Native method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
"RenderThread" daemon prio=7 tid=18 Native
| group="main" sCount=1 dsCount=0 flags=1 obj=0x134c06d8 self=0xb40000749aaf7820
| sysTid=8067 nice=-10 cgrp=top-app sched=0/0 handle=0x72c27dccc0
| state=S schedstat=( 5484600361 852151802 11227 ) utm=89 stm=458 core=1 HZ=100
| stack=0x72c26e5000-0x72c26e7000 stackSize=995KB
| held mutexes=
native: #00 pc 000000000009bab8 /apex/com.android.runtime/lib64/bionic/libc.so (__epoll_pwait+8)
native: #01 pc 0000000000019ad0 /system/lib64/libutils.so (android::Looper::pollInner(int)+184)
native: #02 pc 00000000000199b0 /system/lib64/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+112)
native: #03 pc 000000000020ee3c /system/lib64/libhwui.so (android::uirenderer::ThreadBase::waitForWork()+132)
native: #04 pc 0000000000230370 /system/lib64/libhwui.so (android::uirenderer::renderthread::RenderThread::threadLoop()+80)
native: #05 pc 00000000000154d0 /system/lib64/libutils.so (android::Thread::_threadLoop(void*)+260)
native: #06 pc 0000000000014d94 /system/lib64/libutils.so (thread_data_t::trampoline(thread_data_t const*)+412)
native: #07 pc 00000000000af8c8 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+64)
native: #08 pc 000000000004fe08 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64)
(no managed stack frames)
"GLThread 266" prio=5 tid=19 Native
| group="main" sCount=1 dsCount=0 flags=1 obj=0x134c0750 self=0xb40000749aaf93f0
| sysTid=8070 nice=0 cgrp=top-app sched=0/0 handle=0x72c15e0cc0
| state=S schedstat=( 37459048343 3167531629 19183 ) utm=257 stm=3488 core=1 HZ=100
| stack=0x72c14dd000-0x72c14df000 stackSize=1043KB
| held mutexes=
native: #00 pc 000000000004aecc /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28)
native: #01 pc 00000000001af92c /apex/com.android.art/lib64/libart.so (art::ConditionVariable::WaitHoldingLocks(art::Thread*)+148)
native: #02 pc 000000000037e750 /apex/com.android.art/lib64/libart.so (art::(anonymous namespace)::CheckJNI::ReleaseStringCharsInternal(char const*, _JNIEnv*, _jstring*, void const*, bool, bool)+508)
native: #03 pc 00000000000c9388 /system/lib64/libandroid_runtime.so (android_glGetUniformLocation__ILjava_lang_String_2(_JNIEnv*, _jobject*, int, _jstring*)+100)
at android.opengl.GLES20.glGetUniformLocation(Native method)
at com.tans.tmediaplayer.render.texconverter.Yuv420pImageTextureConverter$convertImageToTexture$1.invoke(Yuv420pImageTextureConverter.kt:43)
at com.tans.tmediaplayer.render.texconverter.Yuv420pImageTextureConverter$convertImageToTexture$1.invoke(Yuv420pImageTextureConverter.kt:32)
at com.tans.tmediaplayer.render.GLUtilKt.offScreenRender(GLUtil.kt:191)
at com.tans.tmediaplayer.render.texconverter.Yuv420pImageTextureConverter.convertImageToTexture(Yuv420pImageTextureConverter.kt:32)
at com.tans.tmediaplayer.render.tMediaPlayerView$FrameRenderer.onDrawFrame(tMediaPlayerView.kt:196)
at android.opengl.GLSurfaceView$GLThread.guardedRun(GLSurfaceView.java:1573)
at android.opengl.GLSurfaceView$GLThread.run(GLSurfaceView.java:1272)
"tMediaPlayerDecoderThread" prio=5 tid=25 Native
| group="main" sCount=1 dsCount=0 flags=1 obj=0x134c0b20 self=0xb40000749ab056a0
| sysTid=8083 nice=10 cgrp=top-app sched=0/0 handle=0x72bd3fecc0
| state=S schedstat=( 50362882932 5311662131 15661 ) utm=4917 stm=119 core=2 HZ=100
| stack=0x72bd2fb000-0x72bd2fd000 stackSize=1043KB
| held mutexes=
native: #00 pc 000000000004aecc /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28)
native: #01 pc 00000000001af92c /apex/com.android.art/lib64/libart.so (art::ConditionVariable::WaitHoldingLocks(art::Thread*)+148)
native: #02 pc 0000000000669abc /apex/com.android.art/lib64/libart.so (art::GoToRunnable(art::Thread*)+460)
native: #03 pc 00000000006698ac /apex/com.android.art/lib64/libart.so (art::JniMethodEnd(unsigned int, art::Thread*)+28)
native: #04 pc 00000000020952a0 /memfd:jit-cache (deleted) (offset 2000000) (art_jni_trampoline+160)
native: #05 pc 0000000002017940 /memfd:jit-cache (deleted) (offset 2000000) (com.tans.tmediaplayer.tMediaPlayer.decodeNativeInternal$tmediaplayer_debug+48)
native: #06 pc 0000000002012cc0 /memfd:jit-cache (deleted) (offset 2000000) (com.tans.tmediaplayer.tMediaPlayerDecoder$decoderHandler$2$1.dispatchMessage+2848)
native: #07 pc 0000000002022830 /memfd:jit-cache (deleted) (offset 2000000) (android.os.Looper.loop+1328)
native: #08 pc 000000000013387c /apex/com.android.art/lib64/libart.so (art_quick_osr_stub+60)
native: #09 pc 000000000033d108 /apex/com.android.art/lib64/libart.so (art::jit::Jit::MaybeDoOnStackReplacement(art::Thread*, art::ArtMethod*, unsigned int, int, art::JValue*)+344)
native: #10 pc 000000000068ae84 /apex/com.android.art/lib64/libart.so (MterpMaybeDoOnStackReplacement+208)
native: #11 pc 0000000000132350 /apex/com.android.art/lib64/libart.so (MterpHelpers+240)
native: #12 pc 0000000000397020 /system/framework/framework.jar (offset 92b000) (android.os.Looper.loop+1084)
native: #13 pc 000000000067f6f0 /apex/com.android.art/lib64/libart.so (MterpInvokeStatic+1224)
native: #14 pc 000000000012d994 /apex/com.android.art/lib64/libart.so (mterp_op_invoke_static+20)
native: #15 pc 000000000036eca4 /system/framework/framework.jar (offset 92b000) (android.os.HandlerThread.run+56)
native: #16 pc 0000000000305c58 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute(art::Thread*, art::CodeItemDataAccessor const&, art::ShadowFrame&, art::JValue, bool, bool) (.llvm.8100235316906539105)+268)
native: #17 pc 000000000066b1fc /apex/com.android.art/lib64/libart.so (artQuickToInterpreterBridge+780)
native: #18 pc 000000000013cff8 /apex/com.android.art/lib64/libart.so (art_quick_to_interpreter_bridge+88)
native: #19 pc 0000000000133564 /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+548)
native: #20 pc 00000000001a8a78 /apex/com.android.art/lib64/libart.so (art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)+200)
native: #21 pc 0000000000554c6c /apex/com.android.art/lib64/libart.so (art::JValue art::InvokeVirtualOrInterfaceWithJValues<art::ArtMethod*>(art::ScopedObjectAccessAlreadyRunnable const&, _jobject*, art::ArtMethod*, jvalue const*)+460)
native: #22 pc 00000000005a4008 /apex/com.android.art/lib64/libart.so (art::Thread::CreateCallback(void*)+1308)
native: #23 pc 00000000000af8c8 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+64)
native: #24 pc 000000000004fe08 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64)
at com.tans.tmediaplayer.tMediaPlayer.decodeNative(Native method)
at com.tans.tmediaplayer.tMediaPlayer.decodeNativeInternal$tmediaplayer_debug(tMediaPlayer.kt:496)
at com.tans.tmediaplayer.tMediaPlayerDecoder$decoderHandler$2$1.dispatchMessage(tMediaPlayerDecoder.kt:51)
- locked <0x07f95ec5> (a com.tans.tmediaplayer.tMediaPlayerBufferManager$Companion$MediaBuffer)
at android.os.Looper.loop(Looper.java:223)
at android.os.HandlerThread.run(HandlerThread.java:67)
// ...
除了基础的 Java
栈,还包括 native
的栈信息,每个线程所归于的分组,他们所持有的 mutex
锁信息和栈占用内存巨细等等信息。
dump 堆内存
dump
堆内存可不像线程栈那么简略,因为规范的 hprof
文件通常比较大,假设想要获取线上用户的内存信息快照信息,要考虑到上传文件的巨细,要考虑上传,需求了解 hprof
文件结构,然后裁剪掉不重要的一些信息,减小需求上传的数据;dump
堆内存还有一个比较重要的问题,便是功用问题,因为本来用户的内存都不够用,手机特别卡,假设再 dump
内存会导致手机愈加卡,会比较严重影响用户体会。
所以我认为 dump
线上用户的堆内存,就需求解决上述两个问题,文件过大上传到服务器问题;dump
进程过于消耗设备功用。
我这儿直接经过 Android Studio
中的 Profiler
来 dump
堆内存信息,应该还有其他东西可以经过编程的办法来 dump
内存信息,咱们可以去网上找找。
运用 Profiler
选中 Memory
, 选中 Capture heap dump
表明 dump
堆内存,在你需求的时刻点点击 Record
,就可以完结 dump
。
当 dump
完结后,就可以看到以下内存的信息:
首先可以经过左边的保存按钮将堆内存快照保存到文件,这是规范的 JVM
hprof
快照文件,可以供其他的东西打开。看到右上方有一些重要数据 Classes
(类数量),Leaks
(内存泄漏的 Activity
,Fragment
和 Dialog
等等),Count
(目标数量,和 Allocations
是一样的),Shallow Size
,Retained Size
。
其他参数都很好理解,可是 Shallow Size
和 Retained Size
咱们或许混杂不清楚,他们仅仅不同的核算目标内存巨细的办法。
-
Shallow Size
这儿举一个比如,假设一个目标包括 1 个int
(4 Bytes) 类型的成员变量;1 个long
(8 Bytes) 类型的成员变量;包括 1 个引证的其他的目标(引证自身占用 4 Bytes,其他目标在堆中占用 10 Bytes),咱们不考虑目标头号其他的占用的内存,咱们只考虑咱们的咱们列出来的数据。经过Shallow Size
核算,该目标占用的内存巨细为4 Bytes + 8 Bytes + 4 Bytes = 20 Bytes
,这儿就不需求核算引证的目标自身在堆内存中占用的巨细。 -
Retained Size
同样是上面的比如经过Retained Size
来核算便是4 Bytes + 8 Bytes + 4 Bytes + 10 Bytes = 30 Bytes
,Retained Size
核算的巨细要包括所引证目标在堆中占用的内存巨细。不过所引证的目标GC Roots
也有引证时就不核算巨细,不知道GC Roots
的同学去找找其他的材料。参考如下图(obj1
核算巨细时,就不需求考虑obj3
和obj5
在内存中的巨细):
经过对 Shallow Size
和 Retained Size
的比照剖析,可以得出结论 Shallow Size
<= Retained Size
, 可是咱们看到 Profiler
展现的 Shallow Size
是远远大于 Retained Size
,哈哈,我也不知道原因,我也找其他材料了,没有找到相关信息,知道的同学可以在下面留言。可是当我选中某个目标检查具体实例的时分就可以满足 Shallow Size
<= Retained Size
。
咱们可以选中某个目标检查它的对应的一切实例,挑选对应的实例可以看到实例的一些信息:
咱们可以看到实例对应的成员变量他们的一些信息,这儿又多了一个 Depth
参数,它是表明当时目标到 GC Roots
的最小跳数。选中成员变量右击可以跳转到对应的实例中。
挑选 References
还可以看到一切的其他目标对它的引证:
经过 Android Studio
中 Profiler
咱们可以很简单剖析出虚拟机中的内存泄漏和形成 OOM
的代码。
Profiler
可不是只能简略地 dump
堆内存哦,它还可以捕获一段时刻内目标的分配和毁掉,这个功用也能帮咱们剖析许多问题,比如说 Android
动画进程中形成的内存颤动,咱们可以快速定位到由于分配哪个目标导致的内存颤动(优异的程序应该做到随时刻的流逝内存的占用是一条滑润的曲线,而不是变化剧烈的折线)。
咱们选中 Record Java/Kotlin allocations
后然后点击 Record
就可以开始记录当时的目标分配和毁掉,当需求停止时记住点击大红按钮就好了(不过这个功用很消耗功用,无论是电脑仍是调试的手机)。
挑选的这段时刻是触发 GC
前后的时刻,其中包括这段时刻内的一些重要信息:Allocation
(这段时刻内新建目标个数),Deallocation
(这段时刻内收回目标个数),Total Count
(当时剩下目标个数),Shallow Size
和 Shallow Size Change
。
咱们可以挑选某一个目标,然后会列出这段时刻内这个类一切的创建和收回的实例:
有的目标标识了“三条杠”,这些目标可以检查到它的分配的办法栈。
假设挑选 Visualization
还可以看到你选的目标创建实例的一切办法栈信息:
到这儿咱们剖析 Android Dalvik
虚拟机堆内存的办法就介绍完了,不过这仅仅虚拟机中的堆内存,许多时分咱们出现问题的是 native
的堆内存,后续考虑再独自出文章来介绍。
总结
除了线程办法栈的 dump
,其他的办法都不合适线上剖析。
不要认为学会了办法栈的 dump
和虚拟机堆内存的 dump
就可以彻底解决这些问题,他们仅仅帮你发现这些问题,要彻底解决这些问题还需求许多其他方面的知识。