经过 dump 虚拟机线程办法栈和堆内存来剖析 Android 卡顿和 OOM 问题

Android 中的功用问题无非便是卡顿和 OOM,虽然总体就这两种,可是形成这两种功用问题的原因却是十分多,需求具体的原因具体剖析,并且这是十分复杂的。本篇文章仅仅简略介绍如何找到形成这些问题的直接原因的东西(也算是入门剖析),更深层次的问题你或许还需求其他知识。

卡顿(ANR)通常是主线程堵塞导致,主线程堵塞也或许有许多其他原因,比如在竞争其他线程的锁,在做耗时的运算,等候 UI 制作的 Buffer 等等,咱们要剖析这些问题就需求 dump 一切的线程办法栈,经过这些办法栈再一步一步剖析具体问题。

OOM 通常是堆内存增长到最大的约束,导致程序无法持续运行而导致的溃散,本篇咱们主要剖析的是 Dalvik 虚拟机栈的内存,而没有 Native 栈的内存(而这部分内存也十分重要,后续考虑独自写文章剖析)。

dump 办法栈

假设想要愈加直观的展现卡顿问题其实 Systrace 是一个愈加好用的东西,它可以愈加直观地显示主线程每一帧的耗时,哪一帧对应办法栈(它的办法栈很简略并且没有 native 信息,假设要显示自己栈的信息需求手动在代码添加 trace。)的耗时信息,还有各种的锁信息,Binder 唤醒信息:

经过 dump 虚拟机线程办法栈和堆内存来剖析 Android 卡顿和 OOM 问题
我上面的这个图便是形成了掉帧,主线程有大段时刻是 sleep 状况,在等候 RenderThread,然后 RenderThread 比较忙在制作前两次的 VSYNC 数据,然后还在等候 IjkPlayer 线程,IjkPlayer 自身有较多的时刻是 Runnable 状况(表明线程等候 CPU 执行,说明先当时时刻 CPU 很忙)。具体想要看 systrace 怎样运用,咱们可以找找其他文章。

systrace 相比照较直观,它是截取了程序运行一个时刻段内的状况,可以清晰发现是哪一帧发生了卡顿,可是相对程序的办法栈信息就比较少,并且这个办法只可以用于调试,无法获取线上用户的数据。
相对于线上用户咱们就不能运用 systrace 的办法,而需求在探测到线程卡顿的时分 dump 出一切线程的办法栈信息相对所以比较好的一个办法。
那么怎样探测卡顿呢?咱们可以经过设置主线程 Looper 中的 Printer 来监听每个主线程的 Message 的执行时刻来判别卡顿,比如这个时分某个 Message 执行超过了 2s 咱们就认为卡顿了。假设不了解 Handler 的同学可以参考我的这篇文章:Android Handler 工作原理

那么怎样 dump 一切线程的栈信息呢?在 JavaAndroid 的环境十分简略直接可以经过以下办法获取到一切的栈信息。

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 中的东西 jstackdump 线程栈信息,可是它在 Android 中并不能用。那在 Android 中要怎样获取具体的栈信息呢?
Android 发生 ANR 时会向方针运用进程发送 SIGQUITLinux 信号,运用进程默许的信号处线程 Signal Catcherdump 一切的线程栈信息和虚拟机的 GC 信息到本地文件,这些文件在 /data/anr/ 目录下,他们是文本文件,直接读取就好了。可是你或许会说咱们仅仅想要在卡顿时获取,可是卡顿并不一定会触发 ANR。确实是这样,可是我不是说过吗,ANR 通知到运用进程是经过 Linux 信号,如何发送一个 LinuxSIGQUIT 信号并不难,可以上网搜索一下。当收到 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 中的 Profilerdump 堆内存信息,应该还有其他东西可以经过编程的办法来 dump 内存信息,咱们可以去网上找找。

运用 Profiler 选中 Memory, 选中 Capture heap dump 表明 dump 堆内存,在你需求的时刻点点击 Record,就可以完结 dump

经过 dump 虚拟机线程办法栈和堆内存来剖析 Android 卡顿和 OOM 问题

dump 完结后,就可以看到以下内存的信息:

经过 dump 虚拟机线程办法栈和堆内存来剖析 Android 卡顿和 OOM 问题

首先可以经过左边的保存按钮将堆内存快照保存到文件,这是规范的 JVM hprof 快照文件,可以供其他的东西打开。看到右上方有一些重要数据 Classes (类数量),Leaks (内存泄漏的 ActivityFragmentDialog 等等),Count (目标数量,和 Allocations 是一样的),Shallow SizeRetained Size
其他参数都很好理解,可是 Shallow SizeRetained 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 BytesRetained Size 核算的巨细要包括所引证目标在堆中占用的内存巨细。不过所引证的目标 GC Roots 也有引证时就不核算巨细,不知道 GC Roots 的同学去找找其他的材料。参考如下图( obj1 核算巨细时,就不需求考虑 obj3obj5 在内存中的巨细):

经过 dump 虚拟机线程办法栈和堆内存来剖析 Android 卡顿和 OOM 问题

经过对 Shallow SizeRetained Size 的比照剖析,可以得出结论 Shallow Size <= Retained Size, 可是咱们看到 Profiler 展现的 Shallow Size 是远远大于 Retained Size,哈哈,我也不知道原因,我也找其他材料了,没有找到相关信息,知道的同学可以在下面留言。可是当我选中某个目标检查具体实例的时分就可以满足 Shallow Size <= Retained Size

咱们可以选中某个目标检查它的对应的一切实例,挑选对应的实例可以看到实例的一些信息:

经过 dump 虚拟机线程办法栈和堆内存来剖析 Android 卡顿和 OOM 问题

咱们可以看到实例对应的成员变量他们的一些信息,这儿又多了一个 Depth 参数,它是表明当时目标到 GC Roots 的最小跳数。选中成员变量右击可以跳转到对应的实例中。

挑选 References 还可以看到一切的其他目标对它的引证:

经过 dump 虚拟机线程办法栈和堆内存来剖析 Android 卡顿和 OOM 问题

经过 Android StudioProfiler 咱们可以很简单剖析出虚拟机中的内存泄漏和形成 OOM 的代码。

Profiler 可不是只能简略地 dump 堆内存哦,它还可以捕获一段时刻内目标的分配和毁掉,这个功用也能帮咱们剖析许多问题,比如说 Android 动画进程中形成的内存颤动,咱们可以快速定位到由于分配哪个目标导致的内存颤动(优异的程序应该做到随时刻的流逝内存的占用是一条滑润的曲线,而不是变化剧烈的折线)。

经过 dump 虚拟机线程办法栈和堆内存来剖析 Android 卡顿和 OOM 问题

咱们选中 Record Java/Kotlin allocations 后然后点击 Record 就可以开始记录当时的目标分配和毁掉,当需求停止时记住点击大红按钮就好了(不过这个功用很消耗功用,无论是电脑仍是调试的手机)。

经过 dump 虚拟机线程办法栈和堆内存来剖析 Android 卡顿和 OOM 问题
挑选的这段时刻是触发 GC 前后的时刻,其中包括这段时刻内的一些重要信息:Allocation (这段时刻内新建目标个数),Deallocation (这段时刻内收回目标个数),Total Count (当时剩下目标个数),Shallow SizeShallow Size Change

咱们可以挑选某一个目标,然后会列出这段时刻内这个类一切的创建和收回的实例:

经过 dump 虚拟机线程办法栈和堆内存来剖析 Android 卡顿和 OOM 问题
有的目标标识了“三条杠”,这些目标可以检查到它的分配的办法栈。

假设挑选 Visualization 还可以看到你选的目标创建实例的一切办法栈信息:

经过 dump 虚拟机线程办法栈和堆内存来剖析 Android 卡顿和 OOM 问题

到这儿咱们剖析 Android Dalvik 虚拟机堆内存的办法就介绍完了,不过这仅仅虚拟机中的堆内存,许多时分咱们出现问题的是 native 的堆内存,后续考虑再独自出文章来介绍。

总结

除了线程办法栈的 dump,其他的办法都不合适线上剖析。
不要认为学会了办法栈的 dump 和虚拟机堆内存的 dump 就可以彻底解决这些问题,他们仅仅帮你发现这些问题,要彻底解决这些问题还需求许多其他方面的知识。