赞
踩
稳定性问题比较杂,且很多是概率性问题,没有统一处理方式,需要针对具体的问题,具体分析,
必现的问题较易解决,针对当前代码添加各种调试log,一步步debug去定位,过程虽然可能慢点,但一般都会解决。
但针对偶发性的概率问题,则较为麻烦,依赖于大量的测试复现,然后统计 分析当前抓取到的 events、system 等log中,找到复现的步骤,然后去定位。
且针对与这种概率问题,最好能够拿到当时的现场,所以有时候需要将 tombstone 或者anr、crash 转为anr 去处理。
系统中发生的ANR 大概分为三类:
1. 输入事件响应超时(主要类型):主线程 (“事件处理线程” / “UI线程”) 在5秒内没有响应输入事件
2. 广播接收处理超时:BroadcastReceiver 没有在10秒内完成返回
备注:
前台广播为10s, 后台广播为 60s, 默认发送的为后台广播
发送广播时,携带Intent.FLAG_RECEIVER_FOREGROUND将广播设置为前台广播
3. Service服务处理超时(小概率事件):Service在20秒(前台5s, 后台service 为200 秒,默认为后台服务)内没有执行完成应用发起的service请求
备注:
- 前台服务:超时时间为5s, 通过startForegroundService 将指定服务指定为前台服务,会在notification中存在提示信息(像音乐类应用)
- 被前台进程binder的服务: service 服务被前台进程binder, 那么此服务的超时时间为 20 秒
- 被后台进程binder的服务: service 服务被后台进程binder,那么此服务的超时时间为 200 秒
以上三类ANR产生的原因可能是各种各样的,但常见的原因可以分为:
1. 程序自身主线程有问题引起的anr,此类问题往往可以直接通过 ANR的 backtrace 定位,此类异常常见的为:
a、主进程进行IO文件操作
b、主进程进行大量频繁的数据库操作
c、主进程进入死循环
d、主进程进行联网操作(应该已经限制不允许了)
但也有一些诡异的无法通过堆栈定位的异常,需要针对具体情况,具体分析。例如:
证卷类的 大富翁apk,更新时在某些低端机上会产生anr,主线程会卡在
at android.os.MessageQueue.nativePollOnce(Native Method)
这个方法是表示主线程处于空闲状态,主线程 handler 等待消息过来处理,那应该不会发生问题啊?
之后多次怀疑,多次定位,才定位出来原因是:
大富翁在更新时, 会在主线程里面有一个显示更新的进度条,每下载一个字节会使用handler 通知主进程 更新一下进度条,会毫秒级的 发送handler 消息。
在低端机上,性能较差,主进程的 handler 消息处理不过来,正常事件没有机会的到处理因而产生anr
高端机旗舰机,性能好,不会产生此问题。
2. 调用到对端,对端异常造成的ANR,此类常见的为 通过am、pm等相关manager 调用到了 system_server 进程内的 service 实现,system_server 因为某些异常没有及时返回,导致主进程black 产生的ANR, 究其根本是system_server 异常引起的问题,常见原因为:
a、system_server 对应的service 正在等待某个锁,没有及时处理请求。
b、system_server 正在频繁的进行io操作(例如:system_server 正在抓取bugreport)
所以定位处理此类的问题的关键为:
问题产生时 dump 到的堆栈必须齐全
3. iowait 过高,此类异常是后台有进程在进行大量io磁盘操作,导致cpu空转,严重影响系统性能,使发生ANR的主线程不能够获得cpu执行,因而产生的anr,例如:
迅雷app 后台以1~2M/s的速度下载数据,会造成整体较为卡钝,会有很大几率产生anr
这种异常从backtrace并不能看出异常,但问题发生的时间,往往伴随着大量anr, 此类问题,找出大量进行io操作的 进程即可,往往不需要具体定位。
ps:iowait 达到20% 就已经算非常高了
4. cpu占用率过高,查看backtrace 中cpu的信息,看那个进程占用cpu较高,具体分析占用cpu较高的进程是否正常。
5. 内存过低, 手机在内存过低的情况下,系统会不断的LowMemeoryKiller优先级低进程,进行整体GC释放内存,对性能影响较大,也是无法直接从backtrace 中看出有效信息的,需要解决当时情况,去分析是否属于正常。
6. 还有一种是 很麻烦的情况,进程卡钝产生了ANR, 但在抓取堆栈的时候,进程恢复过来了,因此 无法从堆栈中看出有效信息,且也是概率性的产生ANR,这类情况,如果发生概率较高,需要综合分析log,没有很好的处理方式。
watchdog 每过30s 检测一次, 如果 要监控的线程30s 后没有相应,系统会dump出此进程堆栈,如果超过60s 没有相应,会触发watchdog,并重启系统。
系统原生的watchdog 分为两种 monitorCheck 与handlerChecker 这两种类型, 在MTK平台上结合现有的流程,添加了一种用于检测watchdog线程的机制:hang_detect, 所以说watchdog的检测类型有三种:
1. handlerChecker 检测
用于检测固定的某些线程,通过向线程的handler post msg,如果检测的 handlerChecker 没有添加 monitorCheck,那么此 msg 只是用来检测一下对应线程能否正常处理事件,handlerChecker 固定检测的线程为:
a、 foreground thread 主要处理的事物为:
- AccountManagerService
- BatteryStatsService
- DreamManagerService
- MountService
- NetworkManagementService
- PackageManagerService
- usb相关(debug, device, portmanager)
- WindowManagerService(screenshotApplicationsInner)
b、 android.bg 主要处理的事物为:
- mBgHandler.handleMessage()的两个消息:CHECK_INTERNET_PERMISSION_MSG、COLLECT_PSS_BG_MSG
c、 main thread 主要处理的事物为:
- system_server 的主线程
d、 ui thread 主要处理的事物为:
- AMS UiHandler里show各种msg
- DisplayManagerService里的overlay相关msg
- PointerEventDispatcher inputevent相关
- VoiceInteractionManagerService Voice交互
- WindowManagerPolicy init操作
e、 i/o thread 主要处理的事物为:
- BluetoothManagerService 相关操作
- MountService里的obb操作
- Tethering 网络共享(usb /wifi/mobile?)
- TvInputManagerService tv里channel session相关
f、 display thread 主要处理的事物为:
- DisplayManagerService(display adapter,viewport ,event…)
- InputManagerService (keyboard , input device …)
- WindowManagerService 实例的创建
ps:正常情况下只有foreground thread 线程,才会存在 monitorCheck。
2. monitorCheck检测
monitorCheck主要用来检测系统常见的各个服务,在android系统里面,monitorCheck 是基于foreground thread 的 handlerChecker的,而monitorCheck检测的动作是由要检查的服务自己继承Monitor接口 去实现,目前各个service的实现是去检测各自对应的锁。
3. hang_detect检测
hang_detect 是 mtk 设计的用来检测 watchdog 自身是否正常的一套机制,它的实现方式为 在kernel 中专门注册了一个设备( /dev/RT_Monitor ),用于和上层通信 监控 watchdog是否正常。
大概流程为:watchdog 向/dev/RT_Monitor 设置一个值,然后hang_detect 驱动根据此值,计算出一个时间,在此时间内,watchdog,必须再次通知一下hang_detect, 来表示并未超时,如果超时也去会dump 相关堆栈,重启手机。
正常情况下 watchdog 的解决办法,就是从watchdog 文件定位查找死锁原因,但也有一些复杂的情况,例如:
1、 死锁:watchdog 中最简单的情况,直接根据watchdog 一步步查找出来死锁即可
2、 native层 与java 层互掉造成的死锁, 这种情况比较少见,但也遇到过,例如:
乐视的三指截屏功能,java 层在system_server 里面,申请锁后 会调用到底层去实现 三指截屏的功能,而在底层,也会去申请一个native的方法锁,去进行三指截屏,截屏操作完,会回调java 层 发送广播,截屏完毕,而发送广播依赖java 锁, 然后native的锁就与 java 层的锁,死锁了。
因为native的锁,并不会在 backtrace 体现,所以碰到这种情况的死锁,要堆栈结合代码,一步步定位,3、 system_server的16 个 binder 全部堵塞,这种情况也比较少见,一般是因为system_server的某些异常引起的,我也是只遇到一次:
这个bug也是在乐视碰到的,某段时间,一些手机概率报出 同一类的watchdog,查看堆栈是:system_server 的16 个binder 线程全部处于等待状态,从堆栈看不出特异的情况,
结合 发生问题时的log, 才确定问题:进程Crash, 会出来一个 fc 弹窗,此弹窗是交互式的,会堵塞binder,等用户相应后,会释放binder, 而问题就发生在这里,一个进程如果同一时间发生两次 异常,那么android 会直接kill 此进程,并将 弹窗dismiss 了, 而乐视 覆写了 弹窗的dismiss 方法,没有在弹窗dismiss 时释放binder, 因而就造成一个binder堵塞, 多发生几次,system_server 的 16 个binder 就全部堵塞了。
ps:我写的有关watchdog 的博客,多多光顾,多多光顾
http://blog.csdn.net/xiaolli/article/details/62039795
http://blog.csdn.net/xiaolli/article/details/54906528
http://blog.csdn.net/xiaolli/article/details/72150384
crash 类型的问题,是非常常见的,问题原因也比较杂,但每人应该都有自己的定位方式,在此就介绍一些定位的工具与小方法:
1、jdb
jdb是 jdk 中 自带的一个调试命令,使用ddms 知道进程的端口号后,可以方便的attach到对应进程进行调试,也可以attach到指定进程后关联源码,结合源码进行调试,因为非图形界面,所以打断点一类的较为麻烦,但调试功能还算齐全。
ps: 因为jdb是在运行时进行调试的, 所以针对加固应用,也可以达到调试目的。
2、framework 调试
之前是eclipse关联到源码后,对framework 进行调试, 现在android studio 应该也支持此功能,但没有使用过,使用eclipse对framework进行调试,调试的过层类似调试apk
3、strace、ltrace
来源于linux的命令,用于获取指定进程的函数调用、底层库调用,这两个命令适用性较为广泛,tombstone、性能问题 也可以使用这两个命令区排查定位
- strace: 此命令常用来跟踪进程执行时的系统调用和所接收的信号。 在Android、Linux世界,进程不能直接访问硬件设备,当进程需要访问硬件设备(比如读取磁盘文件,接收网络数据等等)时,必须由用户态模式切换至内核态模式,通过系统调用访问硬件设备。strace可以跟踪到一个进程产生的系统调用,包括参数,返回值,执行消耗的时间。
系统调用:可以通过对比分析异常情况与正常情况下的 系统调用流程与参数、返回值,来定位crash、tombstone的问题。
执行耗时:strace 这个命令可以统计系统调用消耗的时间,通过这个时间可以用来定位系统的性能问题。
- ltrace: 此命令同strace,但它比的优势在于它可以用来跟踪进程调用库函数的情况,比strace 的系统调用,更为直观。
这两个命令网上,教程比较多,有兴趣可以从网上搜索查看
4、java异常的捕获
这是有两种情况:
- 设置了异常捕获器:
这种类型常见的场景是,某些应用它自己设置了异常捕获器,设置的异常捕获器uncaught的优先级高于default。之后在应用发生crash时, 自己捕获到了当时的异常,自行内部处理消化了异常,不交由系统处理,然后退出了。从log里面看,进程就是正常退出的,找不到异常的原因。
针对这种情况的处理方式,就是在系统侧将设置异常捕获器的方法内部实现给注销了, 以便在应用发生crash时,异常信息走系统流程,获取更多异常发生时的信息,便于定位。需要处理的方法为:
Thread类中的:
setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
- 进行了try catch处理:
这种情况类似异常捕获器的情况,常见为:应用一段代码发生了异常,但它进行try catch 处理,使应用进入了异常处理流程。进程依然在前台运行,并没有实际退出,但应用此时的表现区别于正常。处理这种问题,也往往无法直接log中看出异常,也需要在framework中进行特殊处理:
一般异常对象(error object),都是在异常发生的时候创建的。 而所有的异常对象,最后都会继承Throwable类,因而可知 在异常发生时,一定会调用Throwable 的构造方法,所以我们可以在 Throwable 中构造方法中,将当时的异常堆栈打印到log。之后可以从log 进行排查异常(这种方式log里面的打印的 异常堆栈会比较多,需要根据准确时间点去查找定位)
滴滴打车,之前版本有一个bug,打完车,给的代金卷,概率 无法分享到微信(点击分享,闪一下,又会回到代金卷页面), 就是此类问题
5、crash2anr
crash 是瞬时的,系统获取到异常信息就会把进程kill(java 层产生的异常),但有可能获取到的异常信息不全,需要异常发生时的现场才能定位, 这就需要保留当时crash的瞬时现场,为了达到这个目的可以将crash 转换为anr进行处理, 可以在RuntimeInit.java 里默认的异常捕获器进行处理, 也可以在 ActivityManagerService.java 的handleApplicationCrash 方法里面进行处理。
6、double crash
如果一个进程在同一时间点,同时发生两次异常,那么系统不会去捕获进程的异常信息,会直接退出。针对这种情况,也需要在framework中做处理:
因为java世界的进程fork自zygote,所以只要给zygote 设置了 异常捕获器,那么之后所有的进程都自动会存在异常捕获器,而zygote 的异常捕获器,在开机时是在RuntimeInit.java 设置:
- protected static final void commonInit() {
- ......
- Thread.setUncaughtExceptionPreHandler(new LoggingHandler());
- Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler());
- ......
- }
在DefaultUncaughtExceptionHandler KillApplicationHandler 对象里面发生异常时的处理方式为:
- private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
- public void uncaughtException(Thread t, Throwable e) {
- try {
- if (mCrashing) return;
- mCrashing = true;
- ......
- ActivityManager.getService().handleApplicationCrash(
- mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
- } catch (Throwable t2) {
- ......
- } finally {
- Process.killProcess(Process.myPid());
- System.exit(10);
- }
- }
- }
由代码可以看出,进程第一次发生异常,会通过handleApplicationCrash方法 调用ams方法 去获取异常信息,当同一时间第二次发生异常时,会直接kill进程,造成第一次的异常信息无法获取到。 可以修改此方法,在finally内, 将第二次的异常信息打印到log,并不实际去kill进程。
7、JEB
这是一个专业的用来反编译apk的付费软件,但网上有大牛分享的破解后的软件,有钱的可以支持一下正版,没钱可以从网上搜索破解版的下载,这个软件的特殊之处为:
有window、mac、linux 的版本
因为是商业软件,比较稳定且还在不断的更新
方便,使用软件去打开apk,会自动进行反编译,并在内部关联反编译后函数,可以直接在软件很方便的浏览反编译后的代码
调试,可以结合反编译后的代码,使用软件直接attach 到对应进程,进行调试
………….1
功能较为强大,有兴趣可以自行尝试摸索
8、apkanalyser
这是索尼出的一个很早期的一个跨平台软件,目前好像不在维护了,但旧版本依然可以使用, 它可以很方便的在 apk 的方法头或方法尾打桩, 去调试三方apk的调用流程.
ps:它缺点也较为明显,只能在方法头或方法尾打桩, 不能在方法中间进行处理,还有就是没有办法处理加固的应用。
9、smali
smali使用起来比较麻烦,但只要熟悉smali 语言,然后使用smali、baksmali 两个命令,可以实现任意打桩、修改apk流程、添加代码等目的,非常的强大。
ps:可以参考的博客
http://blog.csdn.net/xiaolli/article/details/51039600
https://www.cnblogs.com/daisin/articles/5512813.html
Tombstone是进程在native层挂了,然后由linker检测到异常,链接debuggerd去捕获异常信息,linker注册异常函数的流程为:
- /bionic/linker/arch/arm64/begin.S
- /bionic/linker/linker_main.cpp(__linker_init)
- /bionic/linker/linker_main.cpp(__linker_init_post_relocation)
- /system/core/debuggerd/handler/debuggerd_handler.cpp(debuggerd_init)
- /system/core/debuggerd/handler/debuggerd_handler.cpp(使用debuggerd_register_handlers方法,将进程的相关异常信号的处理函数设置为 debuggerd_signal_handler)
从流程中可知,系统在linker中使用debuggerd_register_handlers 函数,将进程的所有异常信号处理函数,全部注册为了debuggerd_signal_handler, 所以如果进程没有二次设置异常函数,当进程native 层发生异常,系统会自动进入debuggerd_signal_handler 函数中,进行异常处理,流程为:
- /system/core/debuggerd/handler/debuggerd_handler.cpp(debuggerd_signal_handler)
- /system/core/debuggerd/handler/debuggerd_handler.cpp(log_signal_summary)
在debuggerd_signal_handler 函数中,会先通过log_signal_summary函数获取一下简单的消息,然后获取一下简单的消息头,
之后在去获取堆栈信息,但8.0的代码与之前的代码有区别, 8.0之前的代码,会通过socket链接debuggerd,获取堆栈, 但在8.0上好像不用了。
获取堆栈信息后,根据此流程与堆栈延伸出来的定位方法为:
1、addr2line
tombstone的堆栈是以库地址调用展示,无法直接观看,需要使用对应版本带有符号标的 so库,结合 addr2line, 对堆栈进行解析观看,并且如果堆栈信息较少,也通过计算查看 stack的 调用信息, stack是backtrace里面函数调用的展开:
- backtrace:
- #00 pc 0000000000015730 /system/lib64/libc.so (flockfile+24)
-
- stack:
- 0000007fe718d138 0000007f7d045db0 /system/lib64/libc.so (dlmalloc+240)
- 0000007fe718d168 0000007f7d01eeac /system/lib64/libc.so (open64+132)
- 0000007fe718d178 0000007f7d05d3b0 /system/lib64/libc.so (vfprintf+28)
- 0000007fe718d188 0000007f7d05d3e0 /system/lib64/libc.so (vfprintf+76)
- 0000007fe718d190 0000007f7d0a4488 /system/lib64/libc.so
- memory map: (fault address prefixed with --->)
- 0000007f7d007000-0000007f7d08efff r-x 557056 /system/lib64/libc.so
- 0000007f7d08f000-0000007f7d09dfff --- 61440
- 0000007f7d09e000-0000007f7d0a1fff r-- 16384 /system/lib64/libc.so
- 0000007f7d0a2000-0000007f7d0a4fff rw- 12288 /system/lib64/libc.so
解析backtrace:
addr2line -fe libc.so(对应版本带符号表的so库) 0000000000015730(偏移地址)
解析stack:
libc.so 库的在系统中的基地址为:0000007f7d007000
addr2line -fe libc.so 0000007fe718d138-0000007f7d007000(绝对地址-基地址=偏移地址)
2、objdump
此命令可以将so库反编译,然后根据backtrace里面的地址查看当时的汇编指令,跟踪汇编的调用流程,去定位问题。
3、coredump
android里面coredump默认是关闭的,如果打开coredump,那当系统收到异常信号ARORT/SEGV时,系统会终止当前进程,并保留下来手机当时出现异常时的现场数据,类似于照相机按下快门的一瞬间,得到的照片即为我们的coredump。通常情况下coredmp包含了程序运行时的内存,寄存器状态,堆栈指针,内存管理信息等。可以理解为把程序工作的当前状态存储成一个文件。我们可以使用gdb命令 结合core文件,去定位当时发生的问题。
coredump在android上的打开方式:
http://blog.csdn.net/xiaolli/article/details/51058300
ps:开启coredump后,默认只能抓取native进程的core文件,java层进程 的core文件,需要修改代码才能生成。
4、gdb
gdb 类似jdb,只不过他是用来调试native的,android 版本的gdb 有两种:
- 通过google 提供的两个命令:gdb、gdbserver,来调试进程
- 交叉编译android版本的gdb:
gdb 的使用方法是:将带有 symbols 的so库,push到机器中,然后attach到需要需要调试的进程,进行调试。
gdb 的用法较多,可以从网上查询5、tombstone2anr
这个类似crash,有时获取的异常信息,不足以定位问题,需要获取当时tombstone的现场,但tombstone 是瞬时的,所以也需要将tombstone 转换为anr才行。
但Android中进程如果因为 tombstone 被kill,那么他的异常信号是在native 层处理的,处理之后进程died,然后才会通知ams进行后续的扫尾操作,因为进程在通知ams之前已经死亡,所以之前做的 crash2anr 并不好使。
tombstone2anr 的修改,是在debuggerd_handler.cpp的log_signal_summary方法里面,添加一个循环休眠即可。
6、oat文件的 backtrace 定位
这是5.0 换成art 虚拟机之后,出现的一种特殊的tombstone堆栈,它是没有办法使用addr2line来反编译的,它的栈帧是每一个oat文件,如果要查看此堆栈需要将对应的整个oat文件反编译,然后计算 栈帧 在oat文件中的位置,然后从反编译后的文件中,查找此位置,进行查看:
- backtrace 为:
- #00 pc 00000000026c0024 /data/dalvik-cache/arm64/system@framework@boot.oat (offset 0x26c0000)
- #01 pc 00000000026e9d38 /data/dalvik-cache/arm64/system@framework@boot.oat (offset 0x26c0000)
- 当时oat 的map 为:
- 70cfb000-733bb000 r--p 00000000 fd:00 2909220 /data/dalvik-cache/arm64/system@framework@boot.oat
- 733bb000-75d9f000 r-xp 026c0000 fd:00 2909220 /data/dalvik-cache/arm64/system@framework@boot.oat
- 75d9f000-75da0000 rw-p 050a4000 fd:00 2909220 /data/dalvik-cache/arm64/system@framework@boot.oat
- 7f9d97e000-7f9d97f000 r--p 00000000 fd:00 2909220 /data/dalvik-cache/arm64/system@framework@boot.oat
boot.oat 可执行代码的加载地址为:733bb000
相对于当时出问题的oat 文件,代码段的偏移地址为:0x26c0000
相对于当时出问题的oat 文件, 发生问题的代码在oat 中的偏移地址为:00000000026e9d38
*编译或从对应手机中pull 出一个boot.oat 文件使用oatdump进行反编译:
此boot.oat文件,代码段的偏移地址为:
在boot.oat 的反编译文件中搜索:EXECUTABLE OFFSET 对应的地址 (0x026bf000)
正常与异常时 代码段的地址相见:
0x26c0000 - 0x026bf000 = 0x00001000*
那么可知 发生异常时oat的代码段要比当前这个整体大 0x00001000
那么:
0x026c0024 - 0x00001000
0x026e9d38 - 0x00001000
即为backtrace,中地址转换到当前 oat 中的地址
ps:如果 backtrace 给的是绝对地址,还需要减去733bb000,去计算
ps:参考我的博客(多多观光,多多观光)
http://blog.csdn.net/xiaolli/article/details/70789075
panic 是 kernel 里面发生了异常,引起的重启,在aplog里面是看不出异常的,需要从kernel log里面,分析查看当时的堆栈,找kernel 的同事定位,我不擅长啊。
- Libc.debug.malloc 内存泄漏的检测
- 反编译、调试工具 IDA 的使用(付费的,可贵)
- android 自带工具debuggerd命令 的使用
- 源码里面自带的解析tombstone文件的命令 stack
- 调试内存的工具dlmalloc 的使用
- pstack 实时查看堆栈的命令
- taskset 命令
- am profile 命令
- 分析功耗的Battery Historian
……………………………………
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。