在实际情况中,当Android项目的用户量特别大时候,一些细小的问题也会被放大,ANR问题就是一个典型的例子。
一些ANR问题只会发生在用户实际使用的情景,当系统资源比较紧张等一些特殊情况下才会遇到,而这些ANR问题有很大一部分是因为我们的代码不合理导致,这就需要我们定位问题,修复问题,并且在以后的代码设计中尽量避免这些不合理。
最近工作中集中分析了项目的大量的用户自动上报的ANR问题日志,虽然网上ANR相关的文章已经很多了,在这里还是做一个总结。

提纲

一. 什么情况下会出现ANR
二. ANR机制的原理
三. 如何分析ANR问题
四. 如何避免ANR问题

一.什么情况下会出现ANR问题:

ANR(Application Not responding)。Android中,主线程(UI线程)如果在规定时内没有处理完相应工作,就会出现ANR。
具体来说,ANR会在以下几种情况中出现:

  1. 输入事件(按键和触摸事件)5s内没被处理: Input event dispatching timed out
  2. BroadcastReceiver的事件(onRecieve方法)在规定时间内没处理完(前台广播为10s,后台广播为60s):Timeout of broadcast BroadcastRecord
    07-27 19:18:47.448 1707 1766 W BroadcastQueue: Receiver during timeout: ResolveInfo{ccd831e com.example.qintong.myapplication/.MyBroadCastReciever m=0x108000}
    07-27 19:18:47.502 3513 3728 I WtEventController: ANR com.example.qintong.myapplication 7573
  3. service 前台20s后台200s未完成启动 Timeout executing service
  4. ContentProvider的publish在10s内没进行完:timeout publishing content providers
    在android文档(https://developer.android.com/training/articles/perf-anr.html)中,只写了第一种和第二种情况,而根据源码和实际的实验,我们能发现service的启动和provider的publish同样会造成anr问题。
    这里需要注意的是,在后三种情况,以BroadcastReviever为例,在onRecieve()方法执行10秒内没发生第一种ANR(也就是在这个过程中没有输入事件或输入事件还没到5s)才会发生Receiver timeout,否则将先发生事件无相应ANR,所以onRecieve()是有可能执行不到10s就发生ANR的,所以不要在onRecieve()方法里面干活,service的onCreate()和ContentProvider的onCreate()也一样,他们都是主线程的,不要在这些方法里干活,这个会在本文最后再细说。

二.ANR机制的实现原理:

文章:http://gityuan.com/2016/07/02/android-anr/从源码角度详细的分析了ANR机制实现的原理。对于上一章讲到的1-4中情况,分别找到了其源码中是如何实现的,对于每一种大概原理如下:1.在进行相关操作调用hander.sendMessageAtTime()发送一个ANR的消息,延时时间为ANR发生的时间(如前台Service是当前时间20s之后)。2.进行相关的操作3.操作结束后向remove掉该条message。如果相关的操作在规定时间没有执行完成,该条message将被handler取出并执行,就发生了ANR。
下面以BroadcastReceiver为例详细介绍:
BroadcastQueue.processNextBroadcast()

     final void processNextBroadcast(boolean fromMsg) {
...
synchronized (mService) {
...
do {
if (mOrderedBroadcasts.size() == 0) {
...
if (mService.mProcessesReady && r.dispatchTime > 0) {
long now = SystemClock.uptimeMillis();
if ((numReceivers > 0) &&
(now > r.dispatchTime + (2 * mTimeoutPeriod * numReceivers))) {
//1.发送延时消息
broadcastTimeoutLocked(false); // forcibly finish this broadcast
forceReceive = true;
r.state = BroadcastRecord.IDLE;
}
} if (r.state != BroadcastRecord.IDLE) {
if (DEBUG_BROADCAST) Slog.d(TAG,
"processNextBroadcast("
+ mQueueName + ") called when not idle (state="
+ r.state + ")");
return;
} if (r.receivers == null || r.nextReceiver >= numReceivers
|| r.resultAbort || forceReceive) {
// No more receivers for this broadcast! Send the final
// result if requested...
if (r.resultTo != null) {
try {
//2. 处理广播消息
performReceiveLocked(r.callerApp, r.resultTo,
new Intent(r.intent), r.resultCode,
r.resultData, r.resultExtras, false, false, r.userId);
// Set this to null so that the reference
// (local and remote) isn't kept in the mBroadcastHistory.
r.resultTo = null;
} catch (RemoteException e) {
...
}
}
//3.取消延时消息
cancelBroadcastTimeoutLocked();
...
}
} while (r == null) ;
...
}
}
}

1.发送延时消息:broadcastTimeoutLocked(false):

    final void broadcastTimeoutLocked(boolean fromMsg) {
...
long now = SystemClock.uptimeMillis();
if (fromMsg) {
if (mService.mDidDexOpt) {
// Delay timeouts until dexopt finishes.
mService.mDidDexOpt = false;
long timeoutTime = SystemClock.uptimeMillis() + mTimeoutPeriod;
setBroadcastTimeoutLocked(timeoutTime);
return;
}
if (!mService.mProcessesReady) {
return;
} long timeoutTime = r.receiverTime + mTimeoutPeriod;
if (timeoutTime > now) {
setBroadcastTimeoutLocked(timeoutTime);
return;
}
}

他调用了setBroadcastTimeoutLocked(long timeoutTime):

    final void setBroadcastTimeoutLocked(long timeoutTime) {
if (! mPendingBroadcastTimeoutMessage) {
Message msg = mHandler.obtainMessage(BROADCAST_TIMEOUT_MSG, this);
mHandler.sendMessageAtTime(msg, timeoutTime);
mPendingBroadcastTimeoutMessage = true;
}
}

传入setBroadcastTimeoutLocked(long timeoutTime)的时间xxx + mTimeoutPeriod,mTimeoutPeriod就是onRecieve()可以执行的时间,在BroadcastQueue初始化时候被赋值,前台队列为10s后台队列为60s:
ActivityManagerService.java:

    public ActivityManagerService(Context systemContext) {
...
static final int BROADCAST_FG_TIMEOUT = 10 * 1000;
static final int BROADCAST_BG_TIMEOUT = 60 * 1000;
...
mFgBroadcastQueue = new BroadcastQueue(this, mHandler,
"foreground", BROADCAST_FG_TIMEOUT, false);
mBgBroadcastQueue = new BroadcastQueue(this, mHandler,
"background", BROADCAST_BG_TIMEOUT, true);
...
}
  1. performReceiveLocked()为广播的实际处理,就不展开了
  2. cancelBroadcastTimeoutLocked() :
    该方法的主要工作是当service启动完成,则移除服务超时消息SERVICE_TIMEOUT_MSG。
    final void cancelBroadcastTimeoutLocked() {
if (mPendingBroadcastTimeoutMessage) {
mHandler.removeMessages(BROADCAST_TIMEOUT_MSG, this);
mPendingBroadcastTimeoutMessage = false;
}
}

三.如何分析ANR问题:

从前文可以明确,ANR问题是由于主线程的任务在规定时间内没处理完任务,而造成这种情况的原因大致会有一下几点:

  1. 主线程在做一些耗时的工作
  2. 主线程被其他线程锁
  3. cpu被其他进程占用,该进程没被分配到足够的cpu资源。

判断一个ANR属于哪种情况便是分析ANR问题的关键。那么拿到一个anr的日志,应该如何分析呢?
在发生ANR的时候,系统会收集ANR相关的信息提供给开发者:首先在Log中有ANR相关的信息,其次会收集ANR时的CPU使用情况,还会收集trace信息,也就是当时各个线程的执行情况。trace文件保存到了/data/anr/traces.txt中,此外,ANR前后该进程打印出的log也有一定价值。一般来说可以按一下思路来分析:

  1. 从log中找到ANR反生的信息:可以从log中搜索“ANR in”或“am_anr”,会找到ANR发生的log,该行会包含了ANR的时间、进程、是何种ANR等信息,如果是BroadcastReceiver的ANR可以怀疑BroadCastReceiver.onRecieve()的问题,如果的Service或Provider就怀疑是否其onCreate()的问题。

  2. 在该条log之后会有CPU usage的信息,表明了CPU在ANR前后的用量(log会表明截取ANR的时间),从各种CPU Usage信息中大概可以分析如下几点:
    (1). 如果某些进程的CPU占用百分比较高,几乎占用了所有CPU资源,而发生ANR的进程CPU占用为0%或非常低,则认为CPU资源被占用,进程没有被分配足够的资源,从而发生了ANR。这种情况多数可以认为是系统状态的问题,并不是由本应用造成的。
    (2). 如果发生ANR的进程CPU占用较高,如到了80%或90%以上,则可以怀疑应用内一些代码不合理消耗掉了CPU资源,如出现了死循环或者后台有许多线程执行任务等等原因,这就要结合trace和ANR前后的log进一步分析了。
    (3). 如果CPU总用量不高,该进程和其他进程的占用过高,这有一定概率是由于某些主线程的操作就是耗时过长,或者是由于主进程被锁造成的。

  3. 除了上述的情况(1)以外,分析CPU usage之后,确定问题需要我们进一步分析trace文件。trace文件记录了发生ANR前后该进程的各个线程的stack。对我们分析ANR问题最有价值的就是其中主线程的stack,一般主线程的trace可能有如下几种情况:
    (1). 主线程是running或者native而对应的栈对应了我们应用中的函数,则很有可能就是执行该函数时候发生了超时。
    (2). 主线程被block:非常明显的线程被锁,这时候可以看是被哪个线程锁了,可以考虑优化代码。如果是死锁问题,就更需要及时解决了。
    (3). 由于抓trace的时刻很有可能耗时操作已经执行完了(ANR -> 耗时操作执行完毕 ->系统抓trace),这时候的trace就没有什么用了,主线程的stack就是这样的:

"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 obj=0x757855c8 self=0xb4d76500
| sysTid=3276 nice=0 cgrp=default sched=0/0 handle=0xb6ff5b34
| state=S schedstat=( 50540218363 186568972172 209049 ) utm=3290 stm=1764 core=3 HZ=100
| stack=0xbe307000-0xbe309000 stackSize=8MB
| held mutexes=
kernel: (couldn't read /proc/self/task/3276/stack)
native: #00 pc 0004099c /system/lib/libc.so (__epoll_pwait+20)
native: #01 pc 00019f63 /system/lib/libc.so (epoll_pwait+26)
native: #02 pc 00019f71 /system/lib/libc.so (epoll_wait+6)
native: #03 pc 00012ce7 /system/lib/libutils.so (_ZN7android6Looper9pollInnerEi+102)
native: #04 pc 00012f63 /system/lib/libutils.so (_ZN7android6Looper8pollOnceEiPiS1_PPv+130)
native: #05 pc 00086abd /system/lib/libandroid_runtime.so (_ZN7android18NativeMessageQueue8pollOnceEP7_JNIEnvP8_jobjecti+22)
native: #06 pc 0000055d /data/dalvik-cache/arm/system@framework@boot.oat (Java_android_os_MessageQueue_nativePollOnce__JI+96)
at android.os.MessageQueue.nativePollOnce(Native method)
at android.os.MessageQueue.next(MessageQueue.java:323)
at android.os.Looper.loop(Looper.java:138)
at android.app.ActivityThread.main(ActivityThread.java:5528)
at java.lang.reflect.Method.invoke!(Native method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:740)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:630)

当然这种情况很有可能是由于该进程的其他线程消耗掉了CPU资源,这就需要分析其他线程的trace以及ANR前后该进程自己输出的log了。

四.如何降低ANR的概率:

有一些操作是很危险的,非常容易发生ANR,在写代码时候一定要避免:

  1. 主线程读取数据:在Android中主线程去读取数据是非常不好的,Android是不允许主线程从网络读数据的,但系统允许主线程从数据库或者其他地方获取数据,但这种操作ANR风险很大,也会造成掉帧等,影响用户体验。
    (1). 避免在主线程query provider,首先这会比较耗时,另外这个操作provider那一方的进程如果挂掉了或者正在启动,我们应用的query就会很长时间不会返回,我们应该在其他线程中执行数据库query、provider的query等获取数据的操作。
    (2). sharePreference的调用:针对sharePreference的优化点有很多,文章http://weishu.me/2016/10/13/sharedpreference-advices/ 详细介绍了几点sharepreference使用时候的注意事项。首先sharePreference的commit()方法是同步的,apply()方法一般是异步执行的。所以在主线程不要用其commit(),用apply()替换。其次sharePreference的写是全量写而非增量写,所以尽量都修改完同一apply,避免改一点apply一次(apply()方法在Activity stop的时候主线程会等待写入完成,提交多次就很容易卡)。并且存储文本也不宜过大,这样会很慢。另外,如果写入的是json或者xml,由于需要加和删转义符号,速度会比较慢。
  2. 不要在broadcastReciever的onRecieve()方法中干活,这一点很容易被忽略,尤其应用在后台的时候。为避免这种情况,一种解决方案是直接开的异步线程执行,但此时应用可能在后台,系统优先级较低,进程很容易被系统杀死,所以可以选择开个IntentService去执行相应操作,即使是后台Service也会提高进程优先级,降低被杀可能性。
  3. 各个组件的生命周期函数都不应该有太耗时的操作,即使对于后台Service或者ContentProvider来讲,应用在后台运行时候其onCreate()时候不会有用户输入引起事件无响应ANR,但其执行时间过长也会引起Service的ANR和ContentProvider的ANR。
  4. 尽量避免主线程的被锁的情况,在一些同步的操作主线程有可能被锁,需要等待其他线程释放相应锁才能继续执行,这样会有一定的ANR风险,对于这种情况有时也可以用异步线程来执行相应的逻辑。另外, 我们要避免死锁的发生(主线程被死锁基本就等于要发生ANR了)。

Android ANR优化 2的更多相关文章

  1. Android ANR优化 1

    1, 你碰到ANR了吗 在App使用过程中, 你可能遇到过这样的情况: ANR 恭喜你, 这就是传说中的ANR. 1.1 何为ANR ANR全名Application Not Responding, ...

  2. Android App优化之ANR详解

    引言 背景:Android App优化, 要怎么做? Android App优化之性能分析工具 Android App优化之提升你的App启动速度之理论基础 Android App优化之提升你的App ...

  3. 【腾讯Bugly干货分享】Android性能优化典范——第6季

    本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/580d91208d80e49771f0a07c 导语 这里是Android性能优 ...

  4. Android性能优化系列 + Android官方培训课程中文版

    Android性能优化典范 - 第6季 http://hukai.me/android-performance-patterns-season-6/   Android性能优化典范 - 第5季 htt ...

  5. Android性能优化典范(二)

    Google前几天刚发布了Android性能优化典范第2季的课程,一共20个短视频,包括的内容大致有:电量优化,网络优化,Wear上如何做优化,使用对象池来提高效率,LRU Cache,Bitmap的 ...

  6. android app性能优化大汇总(google官方Android性能优化典范 - 第2季)

    Google前几天刚发布了Android性能优化典范第2季的课程,一共20个短视频,包括的内容大致有:电量优化,网络优化,Wear上如何做优化,使用对象池来提高效率,LRU Cache,Bitmap的 ...

  7. Android性能优化典范 - 第2季

    Google发布了Android性能优化典范第2季的课程,一共20个短视频,包括的内容大致有:电量优化,网络优化,Wear上如何做优化,使用对象池来提高效率,LRU Cache,Bitmap的缩放,缓 ...

  8. Android开发优化宝典

    I. 网络相关 http头信息带Cache-Control域 确定缓存过期时间 防止重复请求 直接用IP直连,不用域名,策略性跟新本地IP列表. – DNS解析过程耗时在百毫秒左右,并且还有可能存在D ...

  9. 《Android开发艺术探索》读书笔记 (13) 第13章 综合技术、第14章 JNI和NDK编程、第15章 Android性能优化

    第13章 综合技术 13.1 使用CrashHandler来获取应用的Crash信息 (1)应用发生Crash在所难免,但是如何采集crash信息以供后续开发处理这类问题呢?利用Thread类的set ...

随机推荐

  1. JavaScript原型与继承(1)

    内容: 创建对象的几种模式以及创建的过程 原型链prototype的理解,以及prototype与 __proto__([[Prototype]])的关系 继承的几种实现 1.常见模式与原型链的理解 ...

  2. C# 笔记——数据类型

    一张图读懂C#数据类型:

  3. 【Android开发日记】之基础篇(一)——TextView+SpannableStringBuilder

    TextView是控件中最最基础的一个控件,也是最简单的一个控件.但如果仅此,我不会专门为TextView写一篇文章.最近发现了Android中有趣的一个类,那就是标题上写的SpannableStri ...

  4. redis之(十四)redis的主从复制的原理

    一:redis主从复制的原理,步骤.   第一步:复制初始化 --->从redis启动后,会根据配置,向主redis发送SYNC命令.2.8版本以后,发送PSYNC命令. --->主red ...

  5. 前端javascript实现二进制读写操作

    由于种种原因,在浏览器中无法像nodejs那样操作二进制. 最近写了一个在浏览器端操作读写二进制的帮助类 !function (entrance) { "use strict"; ...

  6. 支持flv的播放神器

    h1:让浏览器支持flv去https://github.com/Bilibili/flv.js h2:让手机电脑都支持mp4使用: <link rel="stylesheet" ...

  7. centos7 samba安装

    http://www.cnblogs.com/muscleape/p/6385583.html 1 安装yum install samba 2 添加用户xxx smbpasswd -a xxx 3 s ...

  8. 关于DRY原则

    软件工程,模式,语言,设计思想发展到今天,说白了,所有的技巧,思想,原则归根结底都是为了这个DRY  从机器语言开始: 为了DRY,出现了汇编符号来代表指令,使开发人员不用“重复翻阅指令手册” 为了D ...

  9. Java基础知识(二)

    一.基本概念 1. Java程序初始化的顺序是怎么样的 2. Java和C++的区别 3. 反射 先看一个知乎回答 什么是反射 主要用途 获得Class对象 4. 注解 什么是注解 为什么要用注解 基 ...

  10. Codeforces Round #119 (Div. 2) Cut Ribbon(DP)

    Cut Ribbon time limit per test 1 second memory limit per test 256 megabytes input standard input out ...