【Android】如何分析 ANR 日志

前言

前两天,我已经写了一篇文章对 ANR 的产生原理进行了简单的探讨,链接如下:

【Android】ANR 原理解析

感兴趣的大佬可以前去观摩观摩,今天这篇文章,将写写如何进行 ANR 日志的分析。

导致 ANR 的原因

应用层导致的ANR(耗时操作)

  • 主线程耗时长
  • 主线程方法执行了死循环
  • 主线程等待子线程释放锁时间过长
  • 应用内存紧张,当一个应用长期处于内存紧张状态,会导致频繁内存交换,进而导致应用的一些操作超时

系统层导致的ANR

  • CPU被抢占:一般来说,前台在玩游戏,可能会导致你的后台广播被抢占CPU
  • 系统服务无法及时响应:比如获取系统联系人等,系统的服务都是Binder机制,服务能力也是有限的,有可能系统服务长时间不响应导致ANR
  • 其他应用占用的大量内存

导出并查看 ANR 日志

方法一

当系统出现 ANR 时,设备会自动将 ANR 日志输出到 /data/anr/ 目录下,如下所示:

在这里插入图片描述

对于这些文件,我们直接双击在 Android Studio 上打开即可。

方法二

执行命令:

adb bugreport D:\mybug\bugrep.zip

可以导出设备所有 bug 日志,执行命令后,在指定文件夹内得到一个 zip 文件,将文件解压后打开,文件目录如下所示:

在这里插入图片描述

其中,设备的 anr 日志会保存在该路径下:D:\mybug\bugrep\FS\data\anr,如图:

在这里插入图片描述

另外,该文件 D:\mybug\bugrep\bugreport-device.200216.002-2022-06-16-15-30-10.txt 内也有有 anr 日志的打印,我们可以通过以下关键词搜索该文件的一些异常信息,如:

“main” prio=:搜索 anr 相关信息

beginning of crash:搜索 crash 相关信息

CPU usage from:搜索 cpu 使用信息

如何分析 ANR 日志

一个 ANR 日志,会包含当前设备中所有进程的使用情况,每个进程开头都会

----- pid 16808 at date ----- 来开头,

----- end 16808 ----- 来结尾,

如下所示:

----- pid 16808 at 2022-06-16 16:56:04 -----
Cmd line: com.example.demoproject
...
...
...
----- end 16808 -----

另外,每个进程日志中都会有一些进程内存相关的信息,如:

----- pid 16808 at 2022-06-16 16:56:04 -----
Cmd line: com.example.demoproject
...
...
Total number of allocations 59378 // 进程创建到现在一共创建了多少对象
Total bytes allocated 8815KB // 进程创建到现在一共申请了多少内存
Total bytes freed 6847KB // 进程创建到现在一共释放了多少内存
Free memory 23MB // 空闲内存(可用内存)
Free memory until GC 23MB // GC前的空闲内存
Free memory until OOME 190MB // OOM之前的可用内存,当这个值很小的时候,已经处于内存紧张状态,应用可能占用了过多的内存
Total memory 25MB // 当前总内存(已用+可用)
Max memory 192MB // 进程最多能申请的内存 
...
----- end 16808 -----

另外,每个进程日志中都会有进程堆栈信息,堆栈信息非常重要,它展示了发生 ANR 的进程当前的所有线程状态。

----- pid 16808 at 2022-06-16 16:56:04 -----
... 
suspend all histogram:  Sum: 114us 99% C.I. 2us-27us Avg: 12.666us Max: 27us
DALVIK THREADS (14):
"Signal Catcher" daemon prio=5 tid=7 Runnable
  | group="system" sCount=0 dsCount=0 flags=0 obj=0x182c0298 self=0x7914b9c000
  | sysTid=16819 nice=0 cgrp=default sched=0/0 handle=0x791a98fd50
  | state=R schedstat=( 28572293 3522448 11 ) utm=1 stm=1 core=0 HZ=100
  | stack=0x791a899000-0x791a89b000 stackSize=991KB
  | held mutexes= "mutator lock"(shared held)
  native: #00 pc 00000000004108e8  /apex/com.android.runtime/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 00000000004f8040  /apex/com.android.runtime/lib64/libart.so (art::Thread::DumpStack(std::__1::basic_ostream<char, std::__1::char_traits<char>>&, bool, BacktraceMap*, bool) const+512)
  native: #02 pc 000000000051297c  /apex/com.android.runtime/lib64/libart.so (art::DumpCheckpoint::Run(art::Thread*)+828)
  native: #03 pc 000000000050b7a0  /apex/com.android.runtime/lib64/libart.so (art::ThreadList::RunCheckpoint(art::Closure*, art::Closure*)+456)
  native: #04 pc 000000000050ac84  /apex/com.android.runtime/lib64/libart.so (art::ThreadList::Dump(std::__1::basic_ostream<char, std::__1::char_traits<char>>&, bool)+1964)
  native: #05 pc 000000000050a364  /apex/com.android.runtime/lib64/libart.so (art::ThreadList::DumpForSigQuit(std::__1::basic_ostream<char, std::__1::char_traits<char>>&)+844)
  native: #06 pc 00000000004c5778  /apex/com.android.runtime/lib64/libart.so (art::Runtime::DumpForSigQuit(std::__1::basic_ostream<char, std::__1::char_traits<char>>&)+200)
  native: #07 pc 00000000004d9bb0  /apex/com.android.runtime/lib64/libart.so (art::SignalCatcher::HandleSigQuit()+1352)
  native: #08 pc 00000000004d8c5c  /apex/com.android.runtime/lib64/libart.so (art::SignalCatcher::Run(void*)+252)
  native: #09 pc 00000000000e68a0  /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+36)
  native: #10 pc 0000000000084b6c  /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64)
  (no managed stack frames)

"main" prio=5 tid=1 Blocked
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x72e0ee78 self=0x79aafbcc00
  | sysTid=16808 nice=-10 cgrp=default sched=0/0 handle=0x79ac524ed0
  | state=S schedstat=( 1140726262 41301458 368 ) utm=94 stm=20 core=0 HZ=100
  | stack=0x7fe35ed000-0x7fe35ef000 stackSize=8192KB
  | held mutexes=
  at com.example.demoproject.view.MainActivity.doSomething(MainActivity.kt:51)
  - waiting to lock <0x02250ad8> (a com.example.demoproject.view.MainActivity) held by thread 18
  at com.example.demoproject.view.MainActivity.click2(MainActivity.kt:36)
  at java.lang.reflect.Method.invoke(Native method)
  at androidx.appcompat.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:441)
  at android.view.View.performClick(View.java:7259)
  at com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:1194)
  at android.view.View.performClickInternal(View.java:7236)
  at android.view.View.access$3600(View.java:801)
  at android.view.View$PerformClick.run(View.java:27896)
  at android.os.Handler.handleCallback(Handler.java:883)
  at android.os.Handler.dispatchMessage(Handler.java:100)
  at android.os.Looper.loop(Looper.java:214)
  at android.app.ActivityThread.main(ActivityThread.java:7397)
  at java.lang.reflect.Method.invoke(Native method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:935)   
      
"Jit thread pool worker thread 0" daemon prio=5 tid=2 Native
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x182c0220 self=0x7919600000
  | sysTid=16814 nice=0 cgrp=default sched=0/0 handle=0x791aa94d40
  | state=S schedstat=( 44810570 11604064 76 ) utm=4 stm=0 core=0 HZ=100
  | stack=0x791a996000-0x791a998000 stackSize=1023KB
  | held mutexes=
  kernel: (couldn't read /proc/self/task/16814/stack)
  native: #00 pc 000000000008033c  /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28)
  native: #01 pc 000000000014b1f4  /apex/com.android.runtime/lib64/libart.so (art::ConditionVariable::WaitHoldingLocks(art::Thread*)+148)
  native: #02 pc 00000000005143dc  /apex/com.android.runtime/lib64/libart.so (art::ThreadPool::GetTask(art::Thread*)+256)
  native: #03 pc 0000000000513768  /apex/com.android.runtime/lib64/libart.so (art::ThreadPoolWorker::Run()+144)
  native: #04 pc 0000000000513228  /apex/com.android.runtime/lib64/libart.so (art::ThreadPoolWorker::Callback(void*)+148)
  native: #05 pc 00000000000e68a0  /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+36)
  native: #06 pc 0000000000084b6c  /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64)
  (no managed stack frames)
...
----- end 16808 -----

如上截图中,有三个线程,一个是 Signal Catcher 线程,一个是 main 线程,一个是 Jit thread pool worker thread 0,他们的线程状态分别是 Runnableblockednative 状态。

而在 Java 中,线程状态有6种(具体可参考我之前写的文章,了解更详细的多线程和并发知识:一文搞懂多线程和并发编程
),如下所示:

  • NEW - 创建状态
  • RUNNABLE - 就绪或运行状态
  • BLOCKED - 阻塞状态
  • WATING - 等待状态
  • TIMED_WAITING - 定时等待状态
  • TERMINATED - 终止状态

那么,上述日志中的 native 状态是什么呢?

其实该状态是 cpp 代码中定义的线程状态,他跟 java 定义的线程状态关系如下:

在这里插入图片描述

由上可知,native 状态对应的 java 线程状态是 runnable 状态。

堆栈信息是我们分析ANR的第一个重要的信息,一般来说:

  • 主线程处于 BLOCK / WAITING / TIMEWAITING 状态,基本上是函数阻塞导致的 anr
  • 若主线程无异常,则应该排查 CPU 负载和内存环境等其他因素

另外,在 anr 日志中,还有一些常见参数,他们的含义如下:

  • group:线程所处的线程组
  • sCount:线程被正常挂起的次数
  • dsCount:线程因调试而挂起次数
  • nice:线程的调度优先级
  • utm:线程在用户态中调度时间值
  • stm:线程在内核态中的调度时间值
  • core:最后执行这个线程的CPU核序号

ANR 案例分析

案例一 - Slepping anr

MainActivity 代码如下:

class MainActivity : AppCompatActivity() {
    
    
    
    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    /**
     * 点击睡眠 10s
     */
    fun clickToSleep(view: View) {
    
    
        Thread.sleep(10_000)
    }
}

当点击按钮睡眠 10s 后,我们左滑或者点击返回键退出 MainActivity(由于主线程正在睡眠,所以此时是无法退出成功的),等待一段时间,系统将会弹出 ANR 弹窗。

我们导出 ANR 日志并打开:

"main" prio=5 tid=1 Sleeping
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x72e0ee78 self=0x79aafbcc00
  | sysTid=16356 nice=-10 cgrp=default sched=0/0 handle=0x79ac524ed0
  | state=S schedstat=( 1075827038 33414740 291 ) utm=93 stm=14 core=2 HZ=100
  | stack=0x7fe35ed000-0x7fe35ef000 stackSize=8192KB
  | held mutexes=
  at java.lang.Thread.sleep(Native method)
  - sleeping on <0x04fbafa5> (a java.lang.Object)
  at java.lang.Thread.sleep(Thread.java:440)
  - locked <0x04fbafa5> (a java.lang.Object)
  at java.lang.Thread.sleep(Thread.java:356)
  at com.example.demoproject.view.MainActivity.clickToSleep(MainActivity.kt:57)
  at java.lang.reflect.Method.invoke(Native method)
  at androidx.appcompat.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:441)
  at android.view.View.performClick(View.java:7259)
  at com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:1194)
  at android.view.View.performClickInternal(View.java:7236)
  at android.view.View.access$3600(View.java:801)
  at android.view.View$PerformClick.run(View.java:27896)
  at android.os.Handler.handleCallback(Handler.java:883)
  at android.os.Handler.dispatchMessage(Handler.java:100)
  at android.os.Looper.loop(Looper.java:214)
  at android.app.ActivityThread.main(ActivityThread.java:7397)
  at java.lang.reflect.Method.invoke(Native method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:935)

从堆栈信息中也很容易发现,在执行到 MainActivity.clickToSleep 方法时,线程进行了睡眠,最终导致 anr。

案例二 - Blocked anr

MainActivity 代码如下:

class MainActivity : AppCompatActivity() {
    
    

    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    /**
     * 点击按钮1 - 创建一个子线程并持有当前 Activity 对象锁然后开始睡眠 10s
     */
    fun click1(view: View) {
    
    
        testBlockThread()
    }

    /**
     * 点击按钮2 - 在主线程中尝试获取 Activity 对象锁并打印 log
     */
    fun click2(view: View) {
    
    
        doSomething()
    }

    private fun testBlockThread() {
    
    
        val thread = Thread {
    
    
            synchronized(this) {
    
    
                Log.i("testLog", "开始睡眠 10s.. 当前线程名称=${
      
      Thread.currentThread().name} 线程id=${
      
      Thread.currentThread().id}")
                Thread.sleep(10_000)
            }
        }
        thread.name = "MyTestBlockThread"
        thread.start()
    }

    private fun doSomething() {
    
    
        synchronized(this) {
    
    
            Log.i("testLog", "doSomething.. 当前线程名称=${
      
      Thread.currentThread().name} 线程id=${
      
      Thread.currentThread().id}")
        }
    }

当首先点击按钮1时,会创建一个子线程 MyTestBlockThread 并持有当前 Activity 对象锁然后开始睡眠 10s,然后继续点击按钮2,此时主线程将会尝试获取 Activity 对象锁并执行,由于锁正在被子线程 MyTestBlockThread 持有,因此,主线程将会一直被 block 直到子线程释放锁。

在主线程被 block 期间,我们左滑或者点击返回键退出 MainActivity(由于主线程正在被 block,所以此时是无法退出成功的),等待一段时间,系统将会弹出 ANR 弹窗。

下面我们导出 ANR 日志并打开:

在这里插入图片描述

可以发现,这里有一行:

- waiting to lock <0x02250ad8> (a com.example.demoproject.view.MainActivity) held by thread 18

其含义就是,主线程正在被 block, 其正在等待 线程18 释放锁,最终因此导致出现了 ANR。

那么,这个 线程18 是谁呢?我们继续看 anr 日志:

在这里插入图片描述

在这里,发现这一行日志:

"MyTestBlockThread" prio=5 tid=18 Sleeping

其含义就是:线程 MyTestBlockThread 正在 Sleeping,它的 prio=5 tid=18

案例三 - 耗时或死循环方法

MainActivity 代码如下:

class MainActivity : AppCompatActivity() {
    
    

    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
    
    fun click(view: View) {
    
    
        doSomething()
    }

    private fun doSomething() {
    
    
        while (true) {
    
    
            Log.i("testLog", "doSomething.. 当前线程名称=${
      
      Thread.currentThread().name} 线程id=${
      
      Thread.currentThread().id}")
        }
    }
}

当我们点击按钮时,会在主线程通过一个死循环不断打印 log,因此 doSomething() 方法我们可以认为是一个耗时方法,点击按钮后,我们左滑或者点击返回键退出 MainActivity(由于主线程正在忙碌中,所以此时是无法退出成功的),等待一段时间,系统将会弹出 ANR 弹窗。

下面我们导出 ANR 日志并打开:

----- pid 13231 at 2022-06-17 14:23:41 -----
Cmd line: com.example.demoproject
...
"main" prio=5 tid=1 Runnable
  | group="main" sCount=0 dsCount=0 flags=0 obj=0x72b20e78 self=0x77fe5a6c00
  | sysTid=13231 nice=-10 cgrp=default sched=0/0 handle=0x77ffb0eed0
  | state=R schedstat=( 31694533124 58819622 723 ) utm=1310 stm=1859 core=5 HZ=100
  | stack=0x7fdc2b7000-0x7fdc2b9000 stackSize=8192KB
  | held mutexes= "mutator lock"(shared held)
  native: #00 pc 00000000004108e8  /apex/com.android.runtime/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 00000000004f8040  /apex/com.android.runtime/lib64/libart.so (art::Thread::DumpStack(std::__1::basic_ostream<char, std::__1::char_traits<char>>&, bool, BacktraceMap*, bool) const+512)
  native: #02 pc 000000000051297c  /apex/com.android.runtime/lib64/libart.so (art::DumpCheckpoint::Run(art::Thread*)+828)
  native: #03 pc 00000000004f8d4c  /apex/com.android.runtime/lib64/libart.so (art::Thread::RunCheckpointFunction()+176)
  native: #04 pc 00000000003713fc  /apex/com.android.runtime/lib64/libart.so (art::(anonymous namespace)::CheckJNI::ReleaseStringCharsInternal(char const*, _JNIEnv*, _jstring*, void const*, bool, bool)+1356)
  native: #05 pc 00000000001507cc  /system/lib64/libandroid_runtime.so (android::android_util_Log_println_native(_JNIEnv*, _jobject*, int, int, _jstring*, _jstring*)+232)
  at android.util.Log.println_native(Native method)
  at android.util.Log.i(Log.java:176)
  at com.example.demoproject.view.MainActivity.doSomething(MainActivity.kt:52)
  at com.example.demoproject.view.MainActivity.click2(MainActivity.kt:36)
  at java.lang.reflect.Method.invoke(Native method)
  at androidx.appcompat.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:441)
  at android.view.View.performClick(View.java:7259)
  at com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:1194)
  at android.view.View.performClickInternal(View.java:7236)
  at android.view.View.access$3600(View.java:801)
  at android.view.View$PerformClick.run(View.java:27896)
  at android.os.Handler.handleCallback(Handler.java:883)
  at android.os.Handler.dispatchMessage(Handler.java:100)
  at android.os.Looper.loop(Looper.java:214)
  at android.app.ActivityThread.main(ActivityThread.java:7397)
  at java.lang.reflect.Method.invoke(Native method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:935)
...
----- end 16808 -----

发现主线程正处于 Runnable 状态,并不是处于空闲状态,堆栈信息中有一行关键日志:

at com.example.demoproject.view.MainActivity.doSomething(MainActivity.kt:52)

这就说明是是主线程中正在执行 doSomething 这个方法导致的 anr。

案例四 - 正常情况

"main" prio=5 tid=1 Native
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x72e0ee78 self=0x79aafbcc00
  | sysTid=1496 nice=-2 cgrp=default sched=0/0 handle=0x79ac524ed0
  | state=S schedstat=( 50585681414 30364690662 64096 ) utm=3092 stm=1966 core=2 HZ=100
  | stack=0x7fe35ed000-0x7fe35ef000 stackSize=8192KB
  | held mutexes=
  kernel: (couldn't read /proc/self/task/1496/stack)
  native: #00 pc 00000000000d0f58  /apex/com.android.runtime/lib64/bionic/libc.so (__epoll_pwait+8)
  native: #01 pc 00000000000180bc  /system/lib64/libutils.so (android::Looper::pollInner(int)+144)
  native: #02 pc 0000000000017f8c  /system/lib64/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+56)
  native: #03 pc 000000000013b8f4  /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:336)
  at android.os.Looper.loop(Looper.java:174)
  at com.android.server.SystemServer.run(SystemServer.java:546)
  at com.android.server.SystemServer.main(SystemServer.java:354)
  at java.lang.reflect.Method.invoke(Native method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:913)

上述主线程堆栈就是一个很正常的空闲堆栈,表明主线程正在等待新的消息。如果ANR日志里主线程是这样一个状态,那可能有两个原因导致 anr:

  • 该 anr 是 cpu 资源抢占或内存紧张等其他因素引起
  • 这份 anr 日志抓取的时候,主线程已经恢复正常

案例五 - CPU 被其他应用抢占

当 anr 日志中找不到有效的信息,这种情况我们就得看 cpu 信息了,

这个日志一般在 bugreport.txt 文件中可以查看,具体怎么导出 bugreport.txt 文件可见本文的《导出并查看 ANR 日志》那里的步骤。

取出 cpu 信息日志如下:

-------------------------------------------------------------------------------
DUMP OF SERVICE CRITICAL cpuinfo:
Load: 5.28 / 5.71 / 5.58
CPU usage from 275243ms to 190078ms ago (2022-06-16 15:25:36.158 to 2022-06-16 15:27:01.323):
  51% 695/audioserver: 35% user + 15% kernel / faults: 3071 minor
  17% 1496/system_server: 9% user + 8% kernel / faults: 43598 minor
  14% 2077/VosRXThread: 0% user + 14% kernel
  3.1% 666/[email protected]: 0.8% user + 2.3% kernel / faults: 170 minor
  3.1% 642/[email protected]: 0.2% user + 2.8% kernel / faults: 2 minor
  ...................................省略 N 行...........................................
21% TOTAL: 6.4% user + 10% kernel + 0.1% iowait + 2.4% irq + 1.4% softirq
--------- 0.007s was the duration of dumpsys cpuinfo, ending at: 2022-06-16 15:30:11
-------------------------------------------------------------------------------	

这个日志怎么分析呢?它的这个部分的含义如下:

1、Load: 5.28 / 5.71 / 5.58 // 代表了设备在 1、5、15 分钟内 正在使用和等待使用CPU 的活动进程的平均数
2、CPU usage from 275243ms to 190078ms ago (2022-06-16 15:25:36.158 to 2022-06-16 15:27:01.323) // 表明负载信息抓取是在 275243ms ~ 190078ms之间的,且时间点是从 2022-06-16 15:25:36.158 开始
3、中间打印百分比的部分 // 各个进程占用的CPU的详细情况
4、最后一行 // 各个进程合计占用的CPU信息。

还有一些名词和含义如下:

1、user: 用户态
2、kernel: 内核态
3、faults: 内存缺页,minor —— 轻微的,major —— 重度,需要从磁盘拿数据
4、iowait: IO 等待占比,如果占比很高,意味着有很大可能是io耗时导致ANR
5、irq: 硬中断,
6、softirq: 软中断

注意以下两点:

  • 如果 owait 占比很高,意味着有很大可能是 io 耗时导致的 ANR,具体进一步查看有没有进程 faults major 比较多
  • 单进程 CPU 的负载并不是以100%为上限,而是有几个核就有百分之几百,如 4 核,那么上限为400%

了解了如何看 cpu 日志后,我们再回到具体的日志中查看,

发现如上排在第一位的进程 audioserver 即为系统中占用 cpu 最高的进程,达到 51%,这属于正常的 cpu 占用范围,因此,此日志中并没有发现导致 anr 的有效日志。

如果发现排在第一位的进程占用了极高的 cpu 资源,那么就极有可能是这个进程导致的 anr 。

案例六 - 系统服务超时

如果系统服务超时,日志中一般会包含 BinderProxy.transactNative 关键字,如下:

在这里插入图片描述

从堆栈可以看出 getActiveNetworkInfo 方法发生了 ANR。我们知道,系统的服务都是 Binder 机制,Binder 一共有16个线程,服务能力也是有限的,所以有可能系统服务长时间不响应导致ANR。

如果其他应用占用了 Binder 线程,那么当前应用只能等待,可进一步搜索 blockUntilThreadAvailable 关键字

at android.os.Binder.blockUntilThreadAvailable(Native method)

如果有发现某个线程的堆栈包含此字样,可进一步看其堆栈确定其调用了什么系统服务。

此类 ANR 属于系统环境的问题,如果某类型机器上频繁发生此问题,应用层可以考虑规避策略。

案例七 - 内存紧张

如果日志中 CPU 和堆栈都正常,仍旧发生了 ANR,那么可以进一步考虑是否是内存紧张导致的 anr。

bugreport.txt 文件中,我们还可以通过如下关键字 am_meminfo 搜索日志:

06-16 02:14:42.014  1000  1496  1575 I am_meminfo: [1163550720,172752896,7536640,272494592,512559104]

数组中的五个值分别指的是

  • Cached
  • Free
  • Zram
  • Kernel
  • Native

其中 Cached + Free 代表当前整个手机的 可用内存如果值很小,那就意味着处于内存紧张状态

一般低内存的判定阈值为:4G 内存手机以下阀值:350MB,以上阀值则为:450MB

另外,如果通过关键字 am_meminfo 搜索不出任何日志,那么我们可以通过 onTrimMemory 搜索,如:

10-31 22:37:33.458 20733 20733 E Runtime : onTrimMemory level:80,pid:com.xxx.xxx:Launcher0

它也可以作为内存紧张的一个参考判断,可见,这里的 level 为 80 ,我们看看 Android 中是怎么定义这个 level 的:

/**
 * 进程接近后台 LRU 列表的末尾,如果没有很快找到更多内存,进程将被杀死
 */
static final int TRIM_MEMORY_COMPLETE = 80;

/**
 * 进程在后台 LRU 列表的中间;释放内存可以帮助系统保持列表中稍后运行的其他进程,以获得更好的整体性能
 */
static final int TRIM_MEMORY_MODERATE = 60;

/**
 * 进程已进入 LRU 列表。这是一个清理资源的好机会,如果用户返回应用程序,这些资源可以高效快速地重新构建
 */
static final int TRIM_MEMORY_BACKGROUND = 40;

/**
 * 进程已显示用户界面,现在不再显示。此时应释放 UI 的大量分配,以便更好地管理内存
 */
static final int TRIM_MEMORY_UI_HIDDEN = 20;

/**
 * 该进程不是可消耗的后台进程,但设备运行的内存极低,即将无法保持任何后台进程运行。您正在运行的进程应尽可能多地释放非关键资源,以允许该内存在其他地方使用。在此之后将发生的下一件事是调用 onLowMemory() 以报告在后台根本无法保留任何内容,这种情况可能会开始显着影响用户
 */
static final int TRIM_MEMORY_RUNNING_CRITICAL = 15;

/**
 * 进程不是可消耗的后台进程,但设备内存不足。您正在运行的进程应该释放不需要的资源,以允许在其他地方使用该内存
 */
static final int TRIM_MEMORY_RUNNING_LOW = 10;

/**
 * 进程不是可消耗的后台进程,但设备运行的内存适中。您正在运行的进程可能希望释放一些不需要的资源以供其他地方使用
 */
static final int TRIM_MEMORY_RUNNING_MODERATE = 5;

由此可知,80 是一个很严重的级别,如果没有很快找到更多内存,进程将被杀死,而且从日志中可以看到,这个进程是 launcher 进程,连桌面都快要被杀死,那普通应用肯定也不会好到哪去。

一般来说,设备内存紧张,会导致多个应用发生 ANR。

参考文章

干货:ANR日志分析全面解析(内含经典堆栈举例)

猜你喜欢

转载自blog.csdn.net/yang553566463/article/details/125335624