专业编程培训机构——完成蜕变以后轻松拿高薪
电话+V:159999-78052 ,欢迎咨询android阻塞式对话框,[python实用课程],[C++单片机原理],[C#网站搭建],[Nodejs小程序开发],[ios游戏开发],[安卓游戏开发],[教会用大脑用想法赚钱实现阶层跨越]
一、androidsurfaceview会不会阻塞ui线程
系统不会为每个组件单独创建线程,在同一个进程里的UI组件都会在UI线程里实例化,系统对每一个组件的调用都从UI线程分发出去。
结果就是,响应系统回调的方法(比如响应用户动作的onKeyDown()和各种生命周期回调)永远都是在UI线程里运行。
当App做一些比较重(intensive)的工作的时候,除非你合理地实现,否则单线程模型的performance会很poor。
特别的是,如果所有的工作都在UI线程,做一些比较耗时的工作比如访问网络或者数据库查询,都会阻塞UI线程,导致事件停止分发(包括绘制事件)。对于用户来说,应用看起来像是卡住了,更坏的情况是,如果UI线程blocked的时间太长(大约超过5秒),用户就会看到ANR(applicationnotresponding)的对话框。
另外,AndoidUItoolkit并不是线程安全的,所以你不能从非UI线程来操纵UI组件。你必须把所有的UI操作放在UI线程里,所以Android的单线程模型有两条原则:
1.不要阻塞UI线程。
二、android中Dialog和PopupWindow的区别
Android中的对话框有两种:PopupWindow和AlertDialog。它们都可以实现弹窗功能,但是他们之间有一些差别,下面总结了一点。(1)Popupwindow在显示之前一定要设置宽高,Dialog无此限制。(2)Popupwindow默认不会响应物理键盘的back,除非显示设置了popup.setFocusable(true);而在点击back的时候,Dialog会消失。(3)Popupwindow不会给页面其他的部分添加蒙层,而Dialog会。(4)Popupwindow没有标题,Dialog默认有标题,可以通过dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);取消标题(5)二者显示的时候都要设置Gravity。如果不设置,Dialog默认是Gravity.CENTER。(6)二者都有默认的背景,都可以通过setBackgroundDrawable(newColorDrawable(android.R.color.transparent));去掉。其中最本质的差别就是:AlertDialog是非阻塞式对话框:AlertDialog弹出时,后台还可以做事情;而PopupWindow是阻塞式对话框:PopupWindow弹出时,程序会等待,在PopupWindow退出前,程序一直等待,只有当我们调用了dismiss方法的后,PopupWindow退出,程序才会向下执行。这两种区别的表现是:AlertDialog弹出时,背景是黑色的,但是当我们点击背景,AlertDialog会消失,证明程序不仅响应AlertDialog的操作,还响应其他操作,其他程序没有被阻塞,这说明了AlertDialog是非阻塞式对话框;PopupWindow弹出时,背景没有什么变化,但是当我们点击背景的时候,程序没有响应,只允许我们操作PopupWindow,其他操作被阻塞。我们在写程序的过程中可以根据自己的需要选择使用Popupwindow或者是Dialog。
AndroidANR|原理解析及常见案例
2024-05-0710:19·闪念基因
如果Android应用的界面线程处于阻塞状态的时间过长,会触发“应用无响应”(ANR)错误。如果应用位于前台,系统会向用户显示一个对话框。
--https://developer.android.google.cn/
前言
网上关于ANR的技术文章很多,其中不乏优秀的精品,可为何当我们遇到ANR问题时,还是经常束手无策呢?猜测一方面是不熟悉原理机制,还有一方面对CPU/RAM/IO/GC/Thermal、GC机制、内存分配、LMK机制、同步锁原理等基础掌握的不够。本文侧重介绍常见的ANR类型以及一般分析策略,旨在希望通过本文能够处理绝大部分的ANR案例。
本文涉及代码基于AndroidS
画了一张图,列举了ANR的影响因素
ANR原理
我们平时遇到的ANR问题大部分是inputANR类型,本文以inputANR为例进行梳理,这块机制并不复杂,受限于篇幅,本文只介绍埋下计时和check超时的代码部分。
正常输入事件的分发流程如下InputDispatcher::dispatchOnce()->InputDispatcher::dispatchOnceInnerLocked->InputDispatcher::dispatchKeyLocked->InputDispatcher::findFocusedWindowTargetsLocked......
findFocusedWindowTargetsLocked这个函数从字面不难猜出其意图:查找有焦点的window。该函数较长,我们将其拆分开来进行梳理
未找到focused的window,也未找到focused的application
//Ifthereisnocurrentlyfocusedwindowandnofocusedapplication//thendroptheevent.if(focusedWindowHandle==nullptrfocusedApplicationHandle==nullptr){ALOGI("Dropping%seventbecausethereisnofocusedwindoworfocusedapplicationin"
"display%"PRId32".",NamedEnum::string(entry.type).c_str(),displayId);returnInputEventInjectionResult::FAILED;}这种情况下,则drop该事件
未找到focused的window,有focused的applicationif(focusedWindowHandle==nullptrfocusedApplicationHandle!=nullptr){if(!mNoFocusedWindowTimeoutTime.has_value()){//Wejustdiscoveredthatthere'snofocusedwindow.StarttheANRtimerstd::chrono::nanosecondstimeout=focusedApplicationHandle->getDispatchingTimeout(DEFAULT_INPUT_DISPATCHING_TIMEOUT);//更新超时时间,该focused事件开始进入计时mNoFocusedWindowTimeoutTime=currentTime+timeout.count();mAwaitedFocusedApplication=focusedApplicationHandle;mAwaitedApplicationDisplayId=displayId;ALOGW("Waitingbecausenowindowhasfocusbut%smayeventuallyadda""windowwhenitfinishesstartingup.Willwaitfor%"PRId64"ms",mAwaitedFocusedApplication->getName().c_str(),millis(timeout));*nextWakeupTime=*mNoFocusedWindowTimeoutTime;returnInputEventInjectionResult::PENDING;}elseif(currentTime>*mNoFocusedWindowTimeoutTime){//AlreadyraisedANR.DroptheeventALOGE("Dropping%seventbecausethereisnofocusedwindow",NamedEnum::string(entry.type).c_str());returnInputEventInjectionResult::FAILED;}else{//说明之前已经埋过计时,此时还未到超时时间则继续等待//StillwaitingforthefocusedwindowreturnInputEventInjectionResult::PENDING;}}
重置超时时间//wehaveavalid,non-nullfocusedwindowresetNoFocusedWindowTimeoutLocked();如果执行到这步的话,则说明本次
findFocusedWindowTargetsLocked找到了非空的window,对于这种情况会
resetNoFocusedWindowTimeoutLocked。
除此之外,系统还有多个场景下也会触发该重置接口,比如setFocusedApplicationLocked当前focused应用发生变化setInputDispatchMode调用了分发模式resetAndDropEverythingLocked这个接口存在多处会调用的场景,如stopFreezingDisplayLocked、performEnableScreen等场景。
其它窗口异常情况
如果当前window存在异常情况,也会做pending处理,同样可能会成为造成ANR的原因。比如窗口处于paused状态if(focusedWindowHandle->getInfo()->paused){ALOGI("Waitingbecause%sispaused",focusedWindowHandle->getName().c_str());returnInputEventInjectionResult::PENDING;}还有其他情况也会导致pending,如窗口未连接、窗口连接已满、窗口连接死亡等,不一一列出。
这里提到了造成消息pending的情况,我们自然会想到那什么场景下消息会drop掉呢?frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cppvoidInputDispatcher::dropInboundEventLocked(constEventEntryentry,DropReasondropReason){constchar*reason;switch(dropReason){caseDropReason::POLICY:#ifDEBUG_INBOUND_EVENT_DETAILSALOGD("Droppedeventbecausepolicyconsumedit.");#endifreason="inboundeventwasdroppedbecausethepolicyconsumedit";break;caseDropReason::DISABLED:if(mLastDropReason!=DropReason::DISABLED){ALOGI("Droppedeventbecauseinputdispatchisdisabled.");}reason="inboundeventwasdroppedbecauseinputdispatchisdisabled";break;caseDropReason::APP_SWITCH:ALOGI("Droppedeventbecauseofpendingoverdueappswitch.");reason="inboundeventwasdroppedbecauseofpendingoverdueappswitch";break;caseDropReason::BLOCKED:ALOGI("Droppedeventbecausethecurrentapplicationisnotrespondingandtheuser""hasstartedinteractingwithadifferentapplication.");reason="inboundeventwasdroppedbecausethecurrentapplicationisnotresponding""andtheuserhasstartedinteractingwithadifferentapplication";break;caseDropReason::STALE:ALOGI("Droppedeventbecauseitisstale.");reason="inboundeventwasdroppedbecauseitisstale";break;caseDropReason::NOT_DROPPED:{LOG_ALWAYS_FATAL("ShouldnotbedroppingaNOT_DROPPEDevent");return;}}
有如上几种场景会造成消息drop,dropInboundEventLocked的触发时机是在
InputDispatcher::dispatchOnceInnerLocked中。到这里我们已经清楚了埋下超时时间的流程,那么什么时候会检查超时时间有没有到呢?InputDispatcher.cpp@dispatchOnce->InputDispatcher.cpp@processAnrsLocked/***Checkifanyoftheconnections'waitqueueshaveeventsthataretooold.*Ifwewaitedforeventstobeack'edformorethanthewindowtimeout,raiseanANR.*Returnthetimeatwhichweshouldwakeupnext.*/nsecs_tInputDispatcher::processAnrsLocked(){constnsecs_tcurrentTime=now();nsecs_tnextAnrCheck=LONG_LONG_MAX;//Checkifwearewaitingforafocusedwindowtoappear.RaiseANRifwaitedtoolongif(mNoFocusedWindowTimeoutTime.has_value()mAwaitedFocusedApplication!=nullptr){if(currentTime>=*mNoFocusedWindowTimeoutTime){processNoFocusedWindowAnrLocked();mAwaitedFocusedApplication.reset();mNoFocusedWindowTimeoutTime=std::nullopt;returnLONG_LONG_MIN;}else{//mNoFocusedWindowTimeoutTime代表的是这个window超时的时间点//Keepwaiting.WewilldroptheeventwhenmNoFocusedWindowTimeoutTimecomes.nextAnrCheck=*mNoFocusedWindowTimeoutTime;}}//CheckifanyconnectionANRsareduenextAnrCheck=std::min(nextAnrCheck,mAnrTracker.firstTimeout());if(currentTime<nextAnrCheck){//mostlikelyscenarioreturnnextAnrCheck;//everythingisnormal.Let'scheckagainatnextAnrCheck}//Ifwereachedhere,wehaveanunresponsiveconnection.sp<Connection>connection=getConnectionLocked(mAnrTracker.firstToken());if(connection==nullptr){ALOGE("Couldnotfindconnectionforentry%"PRId64,mAnrTracker.firstTimeout());returnnextAnrCheck;}connection->responsive=false;//StopwakingupforthisunresponsiveconnectionmAnrTracker.eraseToken(connection->inputChannel->getConnectionToken());onAnrLocked(connection);returnLONG_LONG_MIN;}
如果当前时间已经满足超时时间,则触发onAnrLocked。voidInputDispatcher::onAnrLocked(std::shared_ptr<InputApplicationHandle>application){std::stringreason=StringPrintf("%sdoesnothaveafocusedwindow",application->getName().c_str());updateLastAnrStateLocked(*application,reason);std::unique_ptr<CommandEntry>commandEntry=std::make_unique<CommandEntry>(InputDispatcher::doNotifyNoFocusedWindowAnrLockedInterruptible);commandEntry->inputApplicationHandle=std::move(application);postCommandLocked(std::move(commandEntry));}
onAnrLocked这个函数所起到的主要作用是将
doNotifyNoFocusedWindowAnrLockedInterruptible通过postCommandLocked塞进队列中。在下一次触发
InputDispatcher.dispatchOnce函数会执行
runCommandsLockedInterruptible
voidInputDispatcher::dispatchOnce(){nsecs_tnextWakeupTime=LONG_LONG_MAX;{//acquirelockstd::scoped_lock_l(mLock);mDispatcherIsAlive.notify_all();//Runadispatchloopiftherearenopendingcommands.//Thedispatchloopmightenqueuecommandstorunafterwards.if(!haveCommandsLocked()){dispatchOnceInnerLocked(nextWakeupTime);}//Runallpendingcommandsifthereareany.//Ifanycommandswererunthenforcethenextpolltowakeupimmediately.if(runCommandsLockedInterruptible()){nextWakeupTime=LONG_LONG_MIN;}//....}
runCommandsLockedInterruptible函数作用其实比较简单,就是取出所有的Command执行一遍boolInputDispatcher::runCommandsLockedInterruptible(){if(mCommandQueue.empty()){returnfalse;}do{std::unique_ptr<CommandEntry>commandEntry=std::move(mCommandQueue.front());mCommandQueue.pop_front();Commandcommand=commandEntry->command;command(*this,commandEntry.get());//commandsareimplicitly'LockedInterruptible'commandEntry->connection.clear();}while(!mCommandQueue.empty());returntrue;}这里顺便提一下,我们平时分析日志时经常会遇到类似这样的片段
上面的日志片段其实是在processAnrsLocked中打印的,这块日志打印在S上已经被谷歌移除了
一般分析步骤
确认是否是系统环境影响
首先判断下是否受系统因素影响,这里所说的系统因素通常指的是整机负载/低内存/系统异常等。
下面以高负载和低内存这两个场景为例进行说明
1.受整机负载影响搜索"ANRin"关键词01-2906:24:46.61845212105962IAnrManager:ANRincom.journeyui.calculator(com.journeyui.calculator/.Calculator),time=26043986201-2906:24:46.61845212105962IAnrManager:Reason:Inputdispatchingtimedout(ActivityRecord{51e27cau0com.journeyui.calculator/.Calculatort1837}doesnothaveafocusedwindow)01-2906:24:46.61845212105962IAnrManager:Load:31.7/33.43/30.9801-2906:24:46.61845212105962IAnrManager:Androidtime:[2022-01-2906:24:46.60][260451.594]01-2906:24:46.61845212105962IAnrManager:CPUusagefrom167270msto0msago(2022-01-2906:21:47.589to2022-01-2906:24:34.860)with99%awake:01-2906:24:46.61845212105962IAnrManager:226%1210/system_server:173%user+52%kernel/faults:1026822minor8major01-2906:24:46.61845212105962IAnrManager:125%459/logd:16%user+109%kernel/faults:408minor01-2906:24:46.61845212105962IAnrManager:29%21567/com.journeyui.globalsearch:18%user+10%kernel/faults:45071minor25major01-2906:24:46.61845212105962IAnrManager:26%639/surfaceflinger:18%user+8.6%kernel/faults:4704minor01-2906:24:46.61845212105962IAnrManager:26%20889/com.yulong.android.gamecenter:16%user+9.3%kernel/faults:21143minor01-2906:24:46.61845212105962IAnrManager:24%29706/com.sohu.inputmethod.sogou:home:16%user+8.6%kernel/faults:21832minor01-2906:24:46.61845212105962IAnrManager:24%545/com.android.messaging:15%user+9%kernel/faults:26023minor2major01-2906:24:46.61845212105962IAnrManager:19%803/guardserver:3%user+16%kernel//....01-2906:24:46.61845212105962IAnrManager:1.7%3589/com.journeyui.calculator:1%user+0.6%kernel/faults:3365minor5major//.....01-2906:24:46.61848012105962IAnrManager:85%TOTAL:42%user+33%kernel+0%iowait+0%softirqANRincom.journeyui.calculatorANR进程名Reason:Inputdispatchingtimedout(ActivityRecord{51e27cau0com.journeyui.calculator/.Calculatort1837}doesnothaveafocusedwindow)Reason后面跟的是本次ANR原因,上面这个例子中通俗的解释是:事件落到com.tencent.mm/.ui.LauncherUI窗口上,不过该窗口直到超时时间到了仍未响应输入事件,input超时时间系统默认是5s。Load:31.7/33.43/30.98前1,前5,前15分钟的负载,我们通常看变化趋势。2022-01-2906:21:47.589to2022-01-2906:24:34.860统计的时间区域85%TOTAL:42%user+33%kernel+0%iowait+0%softirq代表了整体的负载情况,85%TOTAL说明整机负载很高。图中(4)-(5)之间的片段则是各个进程占据的CPU情况,发生ANR的进程com.journeyui.calculator的CPU占用只有1.7%
2.受低内存影响
我们知道通常内存紧张的时候,kswapd线程会活跃起来进行回收内存比如下面这个案例
kswapd的CPU占据排进top3的话,这个问题受系统低内存影响比较大。
低内存为何会成为ANR的诱发因素呢?
整机一旦陷入低内存,响应度和流畅度都会受到影响,这是因为低内存往往会伴随着IO升高,内存回收线程如kswapd、HeapTaskDaemon会变得活跃。表现出来的现象就是操作界面如滑动列表明显卡顿、冷启动时间变长、动画顿挫掉帧严重等。上面提到的这些现象从Systrace中能够直观的看出来,低内存时会出现大量的UninterruptibleSleep|WakeKill,BlockI/Oblock信息都是wait_on_page_bit_killable。而触发wait_on_page_bit_killable的上游正是do_page_fault。这点其实比较好理解,低内存时系统往往会竭尽可能的回收内存,可能触发的fastpath回收\kswapd回收\directreclaim回收\LMK杀进程回收等行为,都会造成do_page_fault高频发生。
另外当出现大量的IO操作的时候,应用主线程的UninterruptibleSleep也会变多,此时涉及到io操作(比如view,读文件,读配置文件、读odex文件),都会触发UninterruptibleSleep,导致整个操作的时间变长,这就解释了为何低内存下为何应用响应卡顿。
所以,分析ANR问题时如果遇到kswapd占据很高(top3),则认为该问题受低内存影响很大。
另外这个时候,对于低内存的案例,搜索"lowmemorykiller"关键词,可以看到问题时间区域会有较多的查杀进程行为,我们通常会关注下"lowmemorykiller"这一行杀的进程adj值,adj值越低,说明当前系统内存越吃紧。
小结:通常拿到一份完整的ANR日志,先看下是否受系统环境因素影响,比如判断是否陷入严重的低内存,是否存在系统整机负载很高等情况。如果存在上述的这些情况,说明本次ANR的进程可能只是受害者。
分析堆栈
如果前面已经排除了系统环境影响的话,那么接下来就要分析进程的堆栈。event日志中搜索"am_anr"关键字可以看到时间点:01-2514:40:44,进程号:17728
紧接着我们再根据时间戳,找到anr文件夹下对应的trace文件
可以通过Cmdline中的进程名,再次确认下文件没有找错该份trace文件中搜索关键字"sysTid=17728",这里的17728就是上面"am_anr"一行中包含的进程号,也是进程的主线程号。依次介绍下上图中标记出来的(1)-(5)的含义
线程运行状态nice值代表该线程优先级,nice的取值范围为-20到19。通常来说nice的值越大,进程的优先级就越低,获得CPU调用的机会越少,nice值越小,进程的优先级则越高,获得CPU调用的机会越多。utm:该线程在用户态所执行的时间(单位是jiffies)stm:该线程在内核态所执行的时间线程的cpu耗时是两者相加(utm+stm),utm,stm单位换算成时间单位为1比10ms。core:后面的数字表示的跑在哪个核上,core=7表示打印这段日志时该线程跑在大核CPU7上。函数调用堆栈,也是我们最为关心的部分。
典型案例
下面我们将介绍导致ANR的主要场景案例
主线程耗时
这种情况是最为常见一种类型,也是最容易分析的类型比如下面这个案例,是com.tencent.qqlive的ANR,callstack如下通常来说,如果打印的堆栈是该进程内部的堆栈,并且经过业务证实这段代码确实可能存在耗时,那么根据callstack位置找到代码修改即可。
主线程Blocked/Waiting/Sleeping
从上面图中我们可以看到,主线程当前的状态是(1)Blocked,原因是它在等锁(2)0x06573567,而0x06573567被(3)线程13所持有。此时我们自然想到去看下tid=13线程的CallStack,在该份trace文件中搜索关键字"0x06573567"找到线程13的CallStack。
主线程陷入Blocked状态的缘由,通常是由于持锁线程在做繁琐的事务,或者是主线程和其它线程发生了死锁。当然除了主线程陷入Blocked状态这种常见的情况之外,还有一些比较少见的情况。主线程处于waiting,说明其正在等待其他线程来notify它,堆栈同样是等锁,分析思路一样。主线程处于Sleeping状态,说明当前线程主动调用sleep,其堆栈通常是sleepingon<锁ID>。
Binder阻塞等待
这种案例在项目中比较常见,所谓的Binder阻塞等待指的是什么呢?
比如进程A在其主线程中向进程B发起了Binder请求,进程B因为一些原因比如正在处理耗时消息或网络异常等原因,无法及时响应进程A的Binder请求,造成进程A在主线程上一直阻塞等待Binder的返回结果,最终触发ANR。
比如下面这个案例
从图中可以看到,Keyguard在做native层的Binder通信,并处于阻塞等待对端结果返回的状态中。
这个时候我们很容易会产生这样的疑问:对于这种等待Binder的案例,我们该如何快速的找到Binder对端?
我们以mtk平台为例,anr文件夹下的binderinfo文件,通常会打印出Binder交互信息。可以通过搜索关键字"outgoingtransaction"来进行查找,这个关键字表示的是当前线程扮演的是Client角色,向其它进程发起Binder。
上图中这一行红色标记处代表的含义是:进程号为28444的进程中的28536线程,向进程号为22767发起了Binder。需要注意一点的是:22767:0中的0表示该进程此时没有可用Binder线程。
如果我们在binderinfo文件中没有找到有价值线索的话,也可以在kernel日志中搜索"binder:release"关键词,这行日志通常会在Binder所在进程通信结束后打印出来。
Binder线程池耗尽
我们知道应用最多支持16个binder线程,SystemServer支持最多32个binder线程。我们实际项目上,不时会遇到对端Binder线程池耗尽导致不能响应的案例。
什么情况下会出现Binder池耗尽的情况呢?
举个例子,进程A短时间内发送很多Binder请求给进程B,这种情况下就有可能会导致在短时间内,接收端进程B的Binder线程池中的16个Binder线程,都用来响应进程A的Binder请求。也就是我们常说的Binder线程池耗尽的情况,此时进程B无法处理进程A发过来的新的Binder请求。
比如下面这段日志
图中可以看到17728这个进程的Binder线程池已经耗尽,这个时候就需要找到发送端是谁及其对应的callstack,如何找对端前面已经讲过,这里不再详细展开。
关于Binder耗尽的情况,通常是由于代码设计上的缺陷,导致短时间内高频发起Binder。不过还有一种情况也可能会出现,那就是在Monkey测试环境下。
无效堆栈
在我们的实际项目上,经常会遇到这样的情况,日志提供的是完整的,可堆栈看起来却不像"作案"堆栈,即出现的堆栈并不像是真正的凶手。常见的一种情况:堆栈落在nativePollOnce上。
这种情况通常说明当前主线程的消息队列是空闲的,此时在等待处理下一个msg,打印日志时真正的block消息已经走完了。
我们以下面这个计算器的anr为例01-2906:24:46.61845212105962IAnrManager:ANRincom.journeyui.calculator(com.journeyui.calculator/.Calculator),time=26043986201-2906:24:46.61845212105962IAnrManager:Reason:Inputdispatchingtimedout(ActivityRecord{51e27cau0com.journeyui.calculator/.Calculatort1837}doesnothaveafocusedwindow)01-2906:24:46.61845212105962IAnrManager:Load:31.7/33.43/30.9801-2906:24:46.61845212105962IAnrManager:Androidtime:[2022-01-2906:24:46.60][260451.594]01-2906:24:46.61845212105962IAnrManager:CPUusagefrom167270msto0msago(2022-01-2906:21:47.589to2022-01-2906:24:34.860)with99%awake:01-2906:24:46.61845212105962IAnrManager:226%1210/system_server:173%user+52%kernel/faults:1026822minor8major01-2906:24:46.61845212105962IAnrManager:125%459/logd:16%user+109%kernel/faults:408minor01-2906:24:46.61845212105962IAnrManager:29%21567/com.journeyui.globalsearch:18%user+10%kernel/faults:45071minor25major01-2906:24:46.61845212105962IAnrManager:26%639/surfaceflinger:18%user+8.6%kernel/faults:4704minor01-2906:24:46.61845212105962IAnrManager:26%20889/com.yulong.android.gamecenter:16%user+9.3%kernel/faults:21143minor01-2906:24:46.61845212105962IAnrManager:24%29706/com.sohu.inputmethod.sogou:home:16%user+8.6%kernel/faults:21832minor01-2906:24:46.61845212105962IAnrManager:24%545/com.android.messaging:15%user+9%kernel/faults:26023minor2major//...01-2906:24:46.61845212105962IAnrManager:1.7%3589/com.journeyui.calculator:1%user+0.6%kernel/faults:3365minor5major//...01-2906:24:46.61848012105962IAnrManager:75%TOTAL:42%user+33%kernel+0%iowait+0%softirq正如我们前面所讲的策略,首先看下是否受系统环境影响。从上面的日志片段可以看出,整机负载较高达到了75%,但是前台进程com.journeyui.calculator占用并不高只有1.7%。top中没有kswapd身影,排除低内存的影响,所以这个问题在一定程度上受整机高负载的影响。
PS:ANR发生时SystemServer占据稍高可能是正常的,因为DumpTrace时需要获取系统整体及各进程CPU使用情况,短时间内会造成SystemServer升高。
接着event日志中搜索"am_anr"关键字01-2906:24:34.90747112105962Iam_anr:[0,3589,com.journeyui.calculator,684244549,Inputdispatchingtimedout(ActivityRecord{51e27cau0com.journeyui.calculator/.Calculatort1837}doesnothaveafocusedwindow)]
根据时间戳找到对应的anr文件
此时我们注意到堆栈落在了nativePollOnce上,前面说过落在nativePollOnce上,说明应用此时已经处于idle状态了。
对于这种情况,说明耗时消息已埋没在历史消息中,历史消息的耗时可能存在下面的几种情况应用主线程历史调度中存在严重耗时的消息应用主线程历史调度中存在多个耗时的消息应用主线程历史调度中存在大量消息比如高频发送消息应用主线程本身并不耗时,而是受到系统环境因素影响(IO/低内存/高负载等)
那么历史调度中的耗时消息我们应该如何得知呢?
通常手机厂商会做日志增强,常见的思路如下:对于主线程的bindertransaction耗时情况,超出设定阈值则输出调用信息。当线程等锁时间超出设定阈值,则输出当前的持锁状态。主线程的生命周期回调方法执行时间超出设定阈值,则输出相应信息。对于非异步Binder调用耗时超出设定阈值的时候,输出Binder信息。
对于一些头部应用厂商如字节跳动有自研的Raster消息监控平台,和手机厂商做的日志增强目的是一样的,都是为了尽可能多的收集线索。
除了落在nativePollOnce的情况,还有一种情况则更加隐蔽,容易将我们带偏分析的方向。那就是堆栈打印出来的确实是应用自身的堆栈。但是根据堆栈找到对应代码后发现这段代码不可能会出现耗时,说明这段堆栈可能只是充当了"替罪羊"的角色,而真正的"凶手"早已藏身在了历史消息中。
应用内存问题
我们实际项目中,不时会遇到应用自身内存使用不当导致的ANR。
比如下面的美团ANR案例01-0814:53:20.130107432039IAnrManager:ANRincom.sankuai.meituan(com.sankuai.meituan/com.meituan.android.pt.homepage.activity.MainActivity),time=4832953801-0814:53:20.130107432039IAnrManager:Reason:Inputdispatchingtimedout(c961943com.sankuai.meituan/com.meituan.android.pt.homepage.activity.MainActivity(server)isnotresponding.Waited8006msforMotionEvent)01-0814:53:20.130107432039IAnrManager:Load:27.74/27.04/27.1901-0814:53:20.130107432039IAnrManager:Androidtime:[2022-01-0814:53:20.10][48351.410]01-0814:53:20.130107432039IAnrManager:CPUusagefrom9706msto0msago(2022-01-0814:52:48.524to2022-01-0814:52:58.230):01-0814:53:20.130107432039IAnrManager:100%32613/com.sankuai.meituan:99%user+1.5%kernel/faults:72075minor01-0814:53:20.130107432039IAnrManager:24%16662/com.ss.android.ugc.aweme:miniappX:17%user+7.3%kernel/faults:1762minor01-0814:53:20.130107432039IAnrManager:17%4548/com.ss.android.ugc.aweme:11%user+5.9%kernel/faults:2500minor01-0814:53:20.130107432039IAnrManager:11%1074/system_server:8%user+3.6%kernel/faults:6412minor1major//...01-0814:53:20.130107432039IAnrManager:30%TOTAL:21%user+8.1%kernel+0.1%iowait+1%softirq
从上面日志片段可以看到整机负载并不高,且kswapd占比很低,排除系统因素的影响。01-0814:52:58.243107432039Iam_anr:[0,32613,com.sankuai.meituan,949501508,Inputdispatchingtimedout(c961943com.sankuai.meituan/com.meituan.android.pt.homepage.activity.MainActivity(server)isnotresponding.Waited8006msforMotionEvent)]时间点14:52:58进程号32613
这份日志缺少anr文件,所以直接看系统日志,根据上面的时间点往前推(取决于该ANR类型对应的阈值时间),找到案发最初的时间点。
这个案例中,我们在案发时间点附近,发现大量的GC片段且很多GC耗时都较长。
ClamptargetGCheapfrom这行日志是在SetIdealFootprint即调整目标堆上限值时会打印,这块不太熟悉的话可以参见我们之前发表过的一篇文章<AndroidSARTGC基础篇>。上面这段日志说明当前应用堆使用已超出上限512M,为了满足新分配的对象内存需求,系统一直持续不断的对该应用进行阻塞GC(GC分为不同力度,阻塞GC说明应用此时内存情况比较糟糕)。通过日志可以发现,尽管应用持续不断的阻塞GC,应用内存依旧没有降下来。
这种情况下很可能是出现了应用内存泄漏的情况。
关于应用内存使用不当,通常有如下几种情况频繁的生成临时对象导致堆内存增长迅速,达到下次堆GC触发阈值后便会触发BgGC,进而导致回收线程跑大核和前台应用争抢CPU。另外GC回收阶段会存在一次锁堆,应用的主线程会被pause,这种情况下势必会造成应用使用卡顿甚至ANR。还有一种比较常见的情况是应用发生了较为严重的内存泄漏,导致GC一直无法回收足够的内存。应用申请大内存触发阻塞GC以便能够申请到足够的内存,这种情况通常会引起应用界面的黑屏或者明显的卡顿。我们知道系统低内存时会触发OnTrimMemory回调,如果应用在OnTrimMemory中并且是在主线程中直接调用显式GC接口即System.gc(),也容易引起应用卡顿,对于这个接口的使用需要谨慎。
上面这些情况虽不一定会导致ANR,但是应用操作上的卡顿可能在所难免。
结语
ANR是卡顿的一种严重表现形式,当我们遇到卡顿问题时,应趁早解决,防患于未然。到这里,关于ANR的介绍到此为止,希望通过本文,在日后处理ANR问题时,能够多一些思路。
作者:执笔写青春来源-微信公众号:酷派技术团队出处
:https://mp.weixin.qq.com/s/40T6ITvJNWR8F42530k4DA【WINDRISES EMPLOYMENT PROGRAMMING】尊享对接老板
电话+V:159999-78052
机构由一批拥有10年以上开发管理经验,且来自互联网或研究机构的IT精英组成,负责研究、开发教学模式和课程内容。公司具有完善的课程研发体系,一直走在整个行业发展的前端,在行业内竖立起了良好的品质口碑。