Loading

移动游戏崩溃优化知识点总结

近期游戏项目上线了,上线到现在的崩溃率,Android为0.25%,IOS为0.54%,虽然还有优化的空间,但已经让人项目和运营满意了。我们没有专门的崩溃解决团队,都是出崩溃之后安排能力强的人迅速跟进解决。笔者也对解决崩溃并不专业,只是在解决崩溃的时候现学现用。由于知识比较零碎,所以在写这篇文章的时候大量引用的他人的文章内容,用以汇总知识。这篇文章是对自己对崩溃处理工作的一个总结,想要系统的学习崩溃知识可以参考引用的文章。

工具

工欲善其事必先利其器,处理崩溃,解析崩溃DMP文件只是第一步,更重要的是需要有分析崩溃需要用到的现场信息和报表。所以要么自己研发一套崩溃上报和处理后台,要么用第三方的。第三方的用过友盟和字节GPM,都还不错。

根据崩溃的影响建立响应机制

对线程的崩溃要建立评判和预警措施,保证将线上出的崩溃问题的影响降低到影响。

我们依据崩溃率和崩溃时长崩溃分为四个等级:严重、显著、轻微、正常。并根据崩溃严重等级,需要不同的负责人相应。

崩溃级别整体崩溃率单种类崩溃率(崩溃平均时长小于5min)相应措施
严重大于等于2%大于等于1%通知主程序和负责崩溃的同学,以及PM,并由PM向上级汇报情况
显著大于等于1%且小于2%大于等于0.5%且小于1%通知主程序和负责崩溃的同学,以及PM
轻微大于等于0.5%且小于1%大于等于0.25%且小于0.5%通知主程序和负责崩溃的同学
正常小于0.5%小于0.25%通知负责崩溃的同学

在移动设备中,IOS的崩溃相对Android的崩溃更好分析和解决,所以这篇文章中就只写Android的崩溃处理。

Android 崩溃

android发生的崩溃最常见有Java崩溃、Native崩溃、ANR导致的退出。

Java崩溃在Java代码中出现了未捕获的异常,导致程序异常退出。

Native崩溃一般都是C++代码中访问了非法地址,导致程序崩溃。

在低端手机上经常会遇到ANR,不同的系统对它的处理有差别。有的会弹出提示窗口(Application Not Responding,应用无响应),可能过一小会就恢复过来了,也有可能弹出ANR后,过一会被系统杀死;甚至个别机器会在出现ANR后,不会给用户任何提示,直接就把应用杀了。

应用退出的其他情况

除了上面场景的崩溃,游戏还会因为其他情况发生退出。

  1. 主动自杀。Process.killProcess()、exit() 等
  2. 系统重启。系统出现异常、断电、用户主动重启等,我们可以通过比较应用开机运行时间是否比之前记录的值更小
  3. 被系统杀死。被 low memory killer 杀掉、从系统的任务管理器中划掉等

根据应用的前后台状态,我们可以把异常退出分为前台异常退出和后台异常退出。“被系统杀死” 是后台异常退出的主要原因,当然我们会更关注前台的异常退出的情况,这会跟 ANR、OOM 等异常情况有更大的关联。

崩溃现场

解决问题都需要先分析问题的基本情况,找到问题的原因,再去解决。解决崩溃也有固定的套路。

  1. 根据崩溃现场找到各种线索
  2. 分析崩溃,猜想可能的原因
  3. 复现崩溃
  4. 解决崩溃

崩溃现场保留着很多有价值的线索。通过仔细搜索甄别这些信息,能为我们分析崩溃指明方向。崩溃现场的信息主要由:崩溃日志、崩溃栈、内存、资源占用等信息。

崩溃信息

从崩溃的基本信息,我们可以对崩溃有初步的判断。

首先是进程名、线程名。崩溃的进程是前台进程还是后台进程,崩溃是不是发生在 UI 线程。

其次是崩溃堆栈和崩溃类型。崩溃是属于 Java 崩溃、Native 崩溃,还是 ANR,对于不同类型的崩溃关注的点也不太一样。

详细的崩溃信息解读参考附录

LogCat

系统的信息有时候会带有一些关键的线索,对我们解决问题有非常大的帮助。

//system logcat:
10-25 17:13:47.788 21430 21430 D dalvikvm: Trying to load lib ...

//event logcat:
10-25 17:13:47.788 21430 21430 I am_on_resume_called: 生命周期
10-25 17:13:47.788 21430 21430 I am_low_memory: 系统内存不足
10-25 17:13:47.788 21430 21430 I am_destroy_activity: 销毁 Activty
10-25 17:13:47.888 21430 21430 I am_anr: ANR 以及原因
10-25 17:13:47.888 21430 21430 I am_kill: APP 被杀以及原因

内存信息

OOM、ANR、虚拟内存耗尽等,很多崩溃都跟内存有直接关系。低内存手机的崩溃率要比高内存手机的崩溃率高很多。我们要专注以下三个内存数据:

  1. 系统可用内存。可以直接读取文件 /proc/meminfo获取系统内存状态。当系统可用内存很小(低于 MemTotal 的 10%)时,OOM、大量 GC、系统频繁自杀拉起等问题都非常容易出现。
  2. 应用使用内存。包括 Java 内存、RSS(Resident Set Size)、PSS(Proportional Set Size),我们可以得出应用本身内存的占用大小和分布。PSS 和 RSS 通过 /proc/self/smap 计算,可以进一步得到例如 apk、dex、so 等更加详细的分类统计。
  3. 虚拟内存。虚拟内存可以通过 /proc/self/status 得到,通过 /proc/self/maps 文件可以得到具体的分布情况。有时候我们一般不太重视虚拟内存,但很多类似 OOM、tgkill 等问题都是虚拟内存不足导致的。
Name: com.xxx.name // 进程名
FDSize: 800 // 当前进程申请的文件句柄个数
VmPeak: 3004628 kB // 当前进程的虚拟内存峰值大小
VmSize: 2997032 kB // 当前进程的虚拟内存大小
Threads: 600 // 当前进程包含的线程个数

资源信息

有的时候会发现应用堆内存和设备内存都非常充足,还是会出现内存分配失败的情况,这跟资源泄漏可能有比较大的关系。

  1. 文件句柄 fd。文件句柄的限制可以通过 /proc/self/limits 获得,一般单个进程允许打开的最大文件句柄个数为 1024。但是如果文件句柄超过 800 个就比较危险,需要将所有的 fd 以及对应的文件名输出到日志中,进一步排查是否出现了有文件或者线程的泄漏。
opened files count 812:
0 -> /dev/null
1 -> /dev/log/main4
2 -> /dev/binder
3 -> /data/data/com.xmamiga.sample/files/test.conf
...
  1. 线程数。当前线程数大小可以通过上面的 status 文件得到,一个线程可能就占 2MB 的虚拟内存,过多的线程会对虚拟内存和文件句柄带来压力。根据我的经验来说,如果线程数超过 400 个就比较危险。需要将所有的线程 id 以及对应的线程名输出到日志中,进一步排查是否出现了线程相关的问题
threads count 412:
1820 com.xxx.crashsdk
1844 ReferenceQueueD
1869 FinalizerDaemon
...

打点信息

线上用户发生崩溃后,想要获取用户的操作步骤,要费很大的功夫,要等用户提供操作路径再复现崩溃黄花菜都凉了。所以我们依靠自己,在用户的关键操作步骤里打点,比如跳转地图,打开UI,充值等。崩溃时上报的这些信息,会有很大的帮助。

解决崩溃的过程

拿到崩溃现场信息,要根据这些信息分析,找到一些怀疑点,然后去验证,同时也要根据对崩溃现场分析后的猜想为QA提供复现建议。

确定崩溃的影响

线上会有很多种崩溃类型,想要全部解决不现实。所以我们要判断崩溃对用户影响的严重程度,决定以何种优先级处理崩溃。

当然在线上的时候,我们首要的任务是将线上崩溃的影响降低到最低。有的崩溃可能需要换包才能解决,对于这种情况需要我们提供规避方法。规避也是一种非常重要的解决崩溃的方式。

快速划分崩溃类别

Java类型的崩溃比较明显,一般就是NullPointerException和OUtOfMemory,这个时候要去查看现场中的内存信息。

Native崩溃,要分析崩溃栈的信号。

ANR要看主线程的堆栈,看是否是因为锁等待导致的。可以看ANR日志中的iowait、CPU、GC等信息,进一步确认是IO问题,还是CPU竞争,还是GC导致的卡死。

查找共性

一般来说我们要解决的崩溃都是大量发生的,如果上面的方法不能有效定位问题。我们可以尝试查找这些问题的共性,有了共性我们可以缩小我们的猜想范围。

我们通常用到的信息有机型、系统、厂商、国家地区,打点信息,崩溃市场等。找到同种崩溃类型的共性,能提高我们复现问题的概率。

尝试复现

解决崩溃的一个关键步骤是要复现崩溃。就算我们已经知道了崩溃的原因,能解决或规避,也需要我们复现崩溃去验证方案的可行性。更多的情况是,我们并没有办法直接定位崩溃的原因,需要复现崩溃,才能做进一步的分析。

我们这一步是通常是根据崩溃的共性信息,找到类型的设备,依据崩溃上报的打点或客服从用户那获取的操作路径,尝试去复现。只要能复现问题,程序就没有解决不了的崩溃。

奇葩崩溃

很多崩溃并不是我们自己代码的问题,而是android系统、硬件厂商、或三方库的漏洞导致的。

对这种问题,我们还是需要复现崩溃,依据现场信息去找资料去解决或规避问题。

与崩溃的斗争是一个长期的过程,我们应该可能地预防崩溃的发生。每发生并解决一种崩溃,要想一下怎么避免同种类型崩溃的再次发生,加一些预警措施,让我们能够让开发早期就能将问题暴露出来,或者留一些有用的线索帮助我们分析分析线上发生的崩溃。

附录

崩溃解析

pid: 31336, tid: 31741, name: Thread-19  >>> com.xxx.xxxa.and <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
    x0  0000000000000000  x1  0000000000000000  x2  fefefefefefefeff  x3  00000000ffffffff
    x4  0000000000000000  x5  0000000000000000  x6  00000000ebad6076  x7  00000000ebad6077
    x8  cac6be3247bdad08  x9  cac6be3247bdad08  x10 0000000000430000  x11 0000000000000001
    x12 0000000000000060  x13 00000000ebad607d  x14 00000000ebad607e  x15 00000000ebad607f
    x16 0000007487e5f190  x17 000000758bc18bec  x18 412316e2c3dbe747  x19 0000000000000000
    x20 0000007487e5f608  x21 0000000000000400  x22 0000000000000000  x23 0000000000000000
    x24 0000007487e5f608  x25 00000000fff90000  x26 ffffffffffffffff  x27 00000074e9961d48
    x28 00000074e9961cb0  x29 0000007487e5f570
    sp  0000007487e5f570  lr  000000747a6659e4  pc  000000747add6384
backtrace:

    #00 pc 00000000011d6384  /data/app/~~tEook-EjVgd-7Ns83grTaQ==/com.xxx.xxxa.and-OXrAoIGTwyPY0WYk48KNgw==/split_config.arm64_v8a.apk!/libFancy3D.so (offset 0x14000) 
           strlen at mangled-strlen.c:100
    #01 pc 0000000000a659e0  /data/app/~~tEook-EjVgd-7Ns83grTaQ==/com.xxx.xxxa.and-OXrAoIGTwyPY0WYk48KNgw==/split_config.arm64_v8a.apk!/libFancy3D.so (offset 0x14000) 
           Fancy::StringEncoding::UTF8ToUCS2(wchar_t*, unsigned int, char const*, unsigned int, unsigned int*, bool*) at StringEncoding.cpp:108
    #02 pc 00000000009cfdd4  /data/app/~~tEook-EjVgd-7Ns83grTaQ==/com.xxx.xxxa.and-OXrAoIGTwyPY0WYk48KNgw==/split_config.arm64_v8a.apk!/libFancy3D.so (offset 0x14000) 
           Fancy::StringEncoding::UTF8ToUCS2(wchar_t*, unsigned int, char const*, unsigned int*, bool*) at StringEncoding.h:32
           (inlined by) Fancy::System::GetCPUIDString() at System.cpp:221
    #03 pc 00000000006bb644  /data/app/~~tEook-EjVgd-7Ns83grTaQ==/com.xxx.xxxa.and-OXrAoIGTwyPY0WYk48KNgw==/split_config.arm64_v8a.apk!/libFancy3D.so (offset 0x14000) 
           FancySystem::_cpuIDString_get() at FancySystem.cpp:1447
    #04 pc 00000000006c6898  /data/app/~~tEook-EjVgd-7Ns83grTaQ==/com.xxx.xxxa.and-OXrAoIGTwyPY0WYk48KNgw==/split_config.arm64_v8a.apk!/libFancy3D.so (offset 0x14000) 
           int Fancy::ScriptClass<FancySystem>::Call<Fancy::String>(FancySystem&, Fancy::String (FancySystem::*)()) at IScriptClass.h:281 (discriminator 5)
           (inlined by) Fancy::LuaClass<FancySystem>::FuncWrapper<Fancy::String (FancySystem::*)()>::Dispatch(void*) at IScriptClass.h:949 (discriminator 5)
    #05 pc 0000000000d306f4  /data/app/~~tEook-EjVgd-7Ns83grTaQ==/com.xxx.xxxa.and-OXrAoIGTwyPY0WYk48KNgw==/split_config.arm64_v8a.apk!/libFancy3D.so (offset 0x14000)
           lj_BC_FUNCC at :?
    #06 pc 0000000000995608  /data/app/~~tEook-EjVgd-7Ns83grTaQ==/com.xxx.xxxa.and-OXrAoIGTwyPY0WYk48KNgw==/split_config.arm64_v8a.apk!/libFancy3D.so (offset 0x14000) 
           Fancy::LuaScriptManager::ObjectIndex(lua_State*) at LuaScriptManager.cpp:327 (discriminator 4)
    #07 pc 0000000000d306f4  /data/app/~~tEook-EjVgd-7Ns83grTaQ==/com.xxx.xxxa.and-OXrAoIGTwyPY0WYk48KNgw==/split_config.arm64_v8a.apk!/libFancy3D.so (offset 0x14000)
           lj_BC_FUNCC at :?
    #08 pc 0000000000d0fd40  /data/app/~~tEook-EjVgd-7Ns83grTaQ==/com.xxx.xxxa.and-OXrAoIGTwyPY0WYk48KNgw==/split_config.arm64_v8a.apk!/libFancy3D.so (offset 0x14000) 
           lua_pcall at :?

信息解读:

第一行是进程线程的基本信息。

  • pid:进程号
  • tid:线程号
  • name:线程名

第二行是终止信息和故障地址信息。

  • signal 11 (SIGSEGV)指中断信号为SIGSEGV
  • code 1 (SEGV_MAPERR)表示错误类型为SEGV_MAPERR
  • fault addr 0x0表示错误地址为0x0

下图是错误场景到中断信号类型的说明。

紧随其后的x0 ~ x29表示崩溃时各个寄存器的值。

backtrace是调用栈信息。00,#01,#02 等表示的都是函数调用栈中栈帧的编号,其中编号越小的栈帧表示着当前最近调用的函数信息,所以栈帧标号#00表示的就是当前正在执行并导致程序崩溃函数的信息。

在栈帧的每一行中,pc后面的16进制数值表示当前函数正在执行语句的在共享链接库或者可执行文件中的位置,然后/lib/xxx/xxx.so则表示的是当前执行的代码所在的库,最后面表示调用的函数。

ANR解析

ANR产生的原因可能是应用原因,也可能是系统原因:

  1. 应用层

    1. 函数阻塞如主线程IO、处理大量计算
    2. 主线程等待子线程的锁
    3. 内存紧张,系统频繁交换内存,导致应用操作超时
  2. 系统层

    1. CPU被强占,前台在玩游戏,后台广播可能会抢占CPU
    2. 系统服务无法及时响应
    3. 其他应用占用大量内存

发生ANR的时候系统会产生trace文件,内部有一些重要的信息:

  • CPU负载
  • 内存信息
  • 堆栈信息

CPU信息

Load: 1.8 / 1.29 / 1.61
CPU usage from 0ms to 8049ms later (2022-03-30 20:58:28.285 to 2022-03-30 20:58:36.333):
  133% 11759/com.xxx: 109% user + 23% kernel / faults: 72125 minor 170 major
  65% 944/system_server: 42% user + 23% kernel / faults: 60786 minor 565 major
  47% 19123/android.hardware.audio.service: 44% user + 2.9% kernel / faults: 67 minor 19 major
  25% 1411/com.android.systemui: 17% user + 7.1% kernel / faults: 16025 minor 252 major
  21% 105/kswapd0: 0% user + 21% kernel
  ...
 37% TOTAL: 23% user + 12% kernel + 0.3% iowait + 0.6% irq + 0.3% softirq

信息解读:

  • 第一行:1、5、15分钟内正在使用和等待使用CPU的活动进程的平均数
  • 第二行:表明信息抓取自ANR产生后的0~1987ms,也指出了ANR的时间点
  • 第三行:各进程占用CPU的详细情况
  • 第四行:合计信息

详细信息解释:

  • user:用户态;kernel:内核态
  • faults:内存缺页, minor轻微,major重度,需要从磁盘拿数据
  • iowait:IO等待占比
  • irq:硬中断;softirq:软中断

如果iowait占比很高,有很大的可能性是IO导致ANR的,要进一步查看进程faults major的情况。

单进程CPU的负载上限并不是100%,而是有多少核上限就是百分之几百。

内存信息

Total number of allocations 476778  进程创建到现在一共创建了多少对象
Total bytes allocated 52MB 进程创建到现在一共申请了多少内存
Total bytes freed 52MB   进程创建到现在一共释放了多少内存
Free memory 777KB    不扩展堆的情况下可用的内存
Free memory until GC 777KB  GC前的可用内存
Free memory until OOME 383MB  OOM之前的可用内存
Total memory 当前总内存(已用+可用)
Max memory 384MB  进程最多能申请的内存

Free memory until OOME的值如果很小,说明内存很简章了。

系统log里也有内存相关的信息。

04-02 22:00:08.195  1531  1544 I am_meminfo: [350937088,41086976,492830720,427937792,291887104]

以上四个值分别指Cached、Free、Zram、Kernel和Native

Cached+Free的内存代表着当前整个手机的可用内存,如果值很小,意味着处于内存紧张状态。一般低内存的判定阈值为:4G以下内存手机的阀值:350MB,以上则为:450MB。

如果ANR前后日志里有onTrimMemory,也可以作为内存紧张的一个依据。

堆栈信息

"main" prio=5 tid=1 Native
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x713c4f60 self=0xb400007a8a248010
  | sysTid=11759 nice=0 cgrp=default sched=0/0 handle=0x7bb0cee4f8
  | state=S schedstat=( 1620391198 261239585 2878 ) utm=118 stm=43 core=0 HZ=100
  | stack=0x7fc5886000-0x7fc5888000 stackSize=8192KB
  | held mutexes=
  native: #00 pc 000000000004b20c  /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28)
  native: #01 pc 000000000004edec  /apex/com.android.runtime/lib64/bionic/libc.so (__futex_wait_ex(void volatile*, bool, int, bool, timespec const*)+144)
  native: #02 pc 00000000000aed9c  /apex/com.android.runtime/lib64/bionic/libc.so (pthread_cond_wait+60)
  native: #03 pc 0000000000006498  /data/app/~~GIAwQ0tGOhG1J9WTKqAOCQ==/com.xxx.xxx-PnEAsV4dVxMhWHhtpQOcDQ==/split_config.arm64_v8a.apk!libNativeActivity.so (offset 1f25000) (???)
  at android.app.NativeActivity.onPauseNative(Native method)
  at android.app.NativeActivity.onPause(NativeActivity.java:205)
  at com.Fancy.Application.GameActivity.onPause(GameActivity.java:211)
  at android.app.Activity.performPause(Activity.java:8253)
  at android.app.Instrumentation.callActivityOnPause(Instrumentation.java:1510)
  at android.app.ActivityThread.performPauseActivityIfNeeded(ActivityThread.java:4884)
  at android.app.ActivityThread.performPauseActivity(ActivityThread.java:4845)
  at android.app.ActivityThread.handlePauseActivity(ActivityThread.java:4796)
  at android.app.servertransaction.PauseActivityItem.execute(PauseActivityItem.java:46)
  at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)
  at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
  at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2144)
  at android.os.Handler.dispatchMessage(Handler.java:106)
  at android.os.Looper.loop(Looper.java:240)
  at android.app.ActivityThread.main(ActivityThread.java:8000)
  at java.lang.reflect.Method.invoke(Native method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:603)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

信息解读:

  • main:main表示主线程,子线程的名字为thread-xx
  • prio:线程优先级,默认为5
  • tid:线程的唯一标识ID
  • group:线程组名称
  • sCount:线程被挂起(Suspend)的次数
  • dsCount:线程被调试器挂起的次数
  • obj:对象地址
  • self:该线程的地址
  • sysTid:线程号(主线程与进程号相同)
  • nice:线程调度优先级
  • sched:表示线程的调度策略和优先级
  • cgrp:调度归属组
  • handle:线程处理函数的地址
  • state:调度状态
  • schedstate:从 /proc/[pid]/task/[tid]/schedstat读出,三个值分别表示线程在cpu上执行的时间、线程的等待时间和线程执行的时间片长度,不支持这项信息的三个值都是0
  • utm:线程用户态下使用的时间
  • stm:内核态下的调用时间
  • core:最后执行这个线程的CPU序号
  • 最下面的就是调用栈

第一行最后需要特别说明的信息,native是线程的一种状态。

我们要通过线程的状态判断线程可能在做什么,如果线程处理MONITOR、WAIT、TIMED_WAIT状态,基本上就是线程阻塞导致ANR了。

引用

  1. 28.崩溃优化 · 知识总结
  2. 干货:ANR日志分析全面解析(内含经典堆栈举例)
  3. Android ANR日志分析指南 - 掘金
  4. Android 平台 Native 代码的崩溃捕获机制及实现
posted @ 2022-04-06 23:54  silence394  阅读(0)  评论(0)    收藏  举报  来源