背景问题
1. 什么是ANR?
ANR是Android系统中的一种错误状态,全称为Application Not Responding,中文翻译为“应用无响应”。当Android系统检测到应用程序在一段时间内未能响应用户输入或无法执行主要的UI线程操作时,就会触发ANR错误。ANR是一种系统保护机制,旨在确保应用的响应性,防止用户在使用应用时遇到卡顿或无响应的情况。
2. ANR对用户体验的影响
ANR问题会直接影响用户体验,以下是一些具体影响:
- 响应速度: 用户期望应用在点击屏幕或执行操作时能够迅速响应,ANR会导致应用无法及时响应用户的输入,使用户感到操作迟缓。
- 用户流失: 频繁的ANR错误可能导致用户流失,因为用户可能会认为应用质量较低,切换到其他更流畅的应用。
- 用户满意度: ANR直接与用户对应用的满意度相关。用户更倾向于使用那些反应迅速、没有卡顿的应用,而ANR问题会降低用户对应用的满意度。
3. 产生ANR的原因
- 主线程阻塞: 当应用在主线程执行耗时操作时,例如网络请求或复杂计算,会导致主线程无法及时响应用户输入,触发ANR。
- 死锁: 多线程编程中,死锁可能发生,当一个线程等待另一个线程释放锁时,应用就会无法继续执行,导致ANR。
- 非法耗时操作: Android规定在主线程中不允许执行耗时操作,违反这一规定会触发ANR。
- BroadcastReceiver超时: 当BroadcastReceiver的onReceive()方法执行时间过长,也会导致ANR。
问题分析
ANR的分类
1. InputDispatching Timeout ANR
触发原因:
当应用在规定时间内无法处理用户输入事件,系统会认为应用无响应,触发InputDispatching Timeout ANR。这通常与主线程阻塞或繁忙有关。
源码分析:
javaCopy code
// 位于 InputDispatcher.java
void monitorInputDispatchingLocked(long dispatchingTimeoutNanos) {
// 监控输入事件的分发
long currentTimeNanos = System.nanoTime();
long timeSinceDispatchStartedNanos = currentTimeNanos - mFirstInputEventTimeNanos;
// 检查是否超时
if (timeSinceDispatchStartedNanos > dispatchingTimeoutNanos) {
// 触发 ANR
handleANR();
}
}
在上述代码片段中,monitorInputDispatchingLocked
方法监控输入事件的分发情况,如果处理时间超过规定的时间,就会触发ANR。
2. BroadcastReceiver Timeout ANR
触发原因:
当BroadcastReceiver的onReceive()
方法执行时间过长,或者在该方法中执行了需要较长时间完成的任务,系统会认为应用无响应,触发ANR。
源码分析:
javaCopy code
// 位于 ActivityManagerService.java
boolean broadcastTimeoutLocked(long currentTime) {
// 监控广播接收器的执行时间
long elapsedTime = currentTime - mLastBroadcastTime;
// 检查是否超时
if (elapsedTime > mConstants.BROADCAST_FG_MSG_TIMEOUT) {
// 触发 ANR
return handleANR();
}
return false;
}
上述代码片段中,broadcastTimeoutLocked
方法监控广播接收器的执行时间,如果执行时间超过规定的时间,就会触发ANR。
3. Service Timeout ANR
触发原因:
当Service执行的任务耗时较长,或者在主线程执行了阻塞操作,系统会认为应用无响应,触发ANR。
源码分析:
javaCopy code
// 位于 ActivityManagerService.java
boolean serviceTimeoutLocked(long now) {
// 监控服务的执行时间
long maxTime = mConstants.SERVICE_TIMEOUT > mConstants.SERVICE_BACKGROUND_TIMEOUT
? mConstants.SERVICE_TIMEOUT : mConstants.SERVICE_BACKGROUND_TIMEOUT;
long executeTime = now - service.mExecuteNesting * maxTime;
// 检查是否超时
if (executeTime > maxTime) {
// 触发 ANR
return handleANR();
}
return false;
}
在上述代码片段中,serviceTimeoutLocked
方法监控服务的执行时间,如果执行时间超过规定的时间,就会触发ANR。
4. Activity Timeout ANR
触发原因:
当Activity的主线程在规定时间内无法响应用户输入或执行UI操作,系统会认为应用无响应,触发Activity Timeout ANR。
源码分析:
javaCopy code
// 位于 ActivityManagerService.java
boolean activityTimeoutLocked(ProcessRecord proc, HistoryRecord r, long now, boolean aboveSystem) {
// 监控Activity的执行时间
long maxTime = aboveSystem ? mConstants.ACTIVITY_BG_START_TIMEOUT
: mConstants.ACTIVITY_BG_START_TIMEOUT;
long executeTime = now - r.startUptime;
// 检查是否超时
if (executeTime > maxTime) {
// 触发 ANR
return handleANR();
}
return false;
}
在上述代码片段中,activityTimeoutLocked
方法监控Activity的执行时间,如果执行时间超过规定的时间,就会触发ANR。
ANR的监控方案
1. 监控的基础:SIGQUIT信号
Android系统提供了SIGQUIT信号来帮助监控ANR事件。本文首先介绍了两种监控信号的方法:一是通过SignalCatcher线程使用sigwait方法进行同步、阻塞地监听;二是使用sigaction方法注册signal handler进行异步监听。对比两者的实现,文章指出了在有多个线程同时监听同一个信号时可能出现的问题。
2. 防止误报
监控到SIGQUIT信号并不等于监控到了真正的ANR。文章详细讨论了两种误报情况:其他进程的ANR和非ANR发送SIGQUIT信号。通过标记进程的NOT_RESPONDING状态,并结合ActivityManager的ProcessErrorStateInfo,文章提出了防止误报的解决方案,确保只有真实的ANR事件被捕获。
3. 防止漏报
漏报是指虽然发生了ANR,但监控机制没有正确识别的情况。本文分析了两种可能的漏报情况:后台ANR和闪退ANR。通过轮询检查进程状态和主线程是否卡顿,文章提供了快速识别ANR的方法,确保监控不会错过任何潜在的ANR事件。
4. 获取ANR Trace
为了更好地定位问题,文章介绍了获取ANR Trace的方法。通过Hook的方式拦截系统dump的ANR Trace内容,开发人员可以获得包括线程状态、锁和堆栈等详细信息,有助于更全面地分析问题。
5. API兼容性
考虑到Android系统版本的差异,本文强调了在不同API级别上的兼容性。通过选择不同的Hook点和处理方式,确保监控方案在各种Android版本上都能够平稳运行。
常见的ANR原因分析
-
主线程耗时操作引发ANR
- 主线程阻塞:主线程在执行某些同步操作时被阻塞,例如I/O操作、数据库查询等。这可能导致ANR,影响用户体验。
- 主线程挂起:主线程被挂起,通常由于死锁、死循环或其他线程同步问题引起。这种情况下,主线程无法继续执行,导致ANR。
-
CPU资源争夺导致ANR
- 其他进程CPU占比过高:如果其他进程在某一时间点占用了大量CPU资源,可能导致当前应用在该时间段内无法获得足够的CPU时间片,从而引发ANR。
- 系统CPU占比过高:系统在某一刻的CPU占比过高,可能导致所有应用无法正常运行。这种情况也会引发ANR。
-
主线程卡在Binder通信的对端
- 使用binderinfo查看对端信息:inder通信是Android系统中组件之间进行跨进程通信的基础。如果主线程卡在Binder通信的对端,可以通过binderinfo查看详细信息,找出具体原因。
-
系统或应用内存紧张
- 系统或应用内存紧张时,可能触发系统的lowmemorykiller操作,导致应用进程被杀死,引发ANR。
-
应用频繁Crash
- 应用自身的Crash也是ANR的潜在原因。频繁的Crash可能导致前台应用出现ANR的现象,影响应用的稳定性。
-
应用内存泄露
- 应用中存在内存泄露时,长时间运行后内存占用会逐渐增加,最终导致内存不足,触发ANR。
-
系统原因导致ANR
- 系统在特殊情况下,如冻结或温度过高,可能导致应用ANR。
- 多媒体操作,如音视频处理、编解码等,可能在某些情况下引发ANR。
- I/O 操作被阻塞可能导致ANR,例如读写文件或网络操作。
- 底层服务的Native Crash可能导致整个应用无法正常运行,从而引发ANR。
- Watchdog 是Android系统中的一个守护线程,用于监控应用是否响应,超时未响应可能触发ANR。
- 特定芯片或硬件能力问题可能引发ANR。
- 某些情况下可能出现内存黑洞,导致ANR。
解决方案
使用异步任务和线程池
异步任务
将耗时操作放入异步任务中,以避免在主线程上执行导致ANR。Android提供了AsyncTask
类,可方便地执行后台操作并在主线程更新UI。
// 示例:使用异步任务执行耗时操作
private class MyAsyncTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
// 执行耗时操作
return null;
}
@Override
protected void onPostExecute(Void result) {
// 更新UI或执行其他操作
}
}
// 启动异步任务
new MyAsyncTask().execute();
线程池
合理使用线程池管理线程,以充分利用系统资源。通过线程池可以控制并发线程的数量,避免过多线程导致系统资源不足。
// 示例:使用线程池执行耗时操作
ExecutorService executorService = Executors.newFixedThreadPool(3);
executorService.execute(new Runnable() {
@Override
public void run() {
// 执行耗时操作
}
});
优化UI线程上的任务
避免长时间占用UI线程
确保UI线程上的任务能够在短时间内完成,避免长时间占用UI线程。
使用Handler和Looper
合理使用Handler
和Looper
进行异步消息处理,避免在UI线程上执行过多耗时操作。
// 示例:使用Handler进行异步消息处理
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
// 在UI线程执行任务
}
});
合理使用锁
避免UI线程上的阻塞
确保在UI线程上不要使用过多的同步锁,以避免发生死锁或长时间阻塞。
使用精确的锁
在需要同步的地方,使用精确的锁(如synchronized
关键字)而不是全局锁,以减小锁的范围。
异步加载数据
使用Loader或ViewModel
通过使用Loader
或ViewModel
来异步加载数据,可以在后台线程中执行,避免在UI线程上阻塞。
合理使用BroadcastReceiver
异步处理广播
广播接收器中的操作应该尽量简短,避免在主线程上执行长时间操作。可考虑将耗时任务放入异步任务或线程池中。
动态注册广播接收器
避免使用静态注册广播接收器,因为静态注册的接收器可能在应用处于非活动状态时触发,增加了ANR的风险。
通过采取上述优化方案,开发者可以显著减少Android应用中ANR的发生概率,提升应用的响应性和用户体验。
最佳实践
1. SharedPreference优化
问题描述: 线上ANR traces数据显示,SharedPreference(sp)导致的ANR问题主要集中在以下三类情况:
- SP文件加载与UI线程阻塞 在SP文件创建后,系统会单独使用一个线程来加载解析对应的SP文件。当UI线程尝试访问SP中的内容时,存在以下问题:UI线程阻塞: 如果SP文件还未被完全加载解析到内存,UI线程在尝试访问SP内容时会被阻塞,直到SP文件被完全加载到内存为止。这可能导致UI线程无法响应用户操作,引发ANR。加载顺序问题: 由于SP文件加载是在单独的线程中进行的,当UI线程需要访问SP内容时,可能发生加载尚未完成的情况,从而阻塞UI线程。
- 数据跨进程通信与主线程等待 为了确保数据的跨进程完整性,Google系统允许应用使用SP进行跨进程通信。然而,这可能导致ANR的原因包括:主线程等待SP写入: 在组件销毁或其他生命周期结束时,为了确保当前写入任务在当前组件的生命周期内完成写入,主线程可能在组件销毁或暂停的生命周期内等待SP完全写入到对应的文件中。这种等待会导致主线程被阻塞,直到写入任务完成。QueuedWork.waitToFinish()处阻塞: 在SP写入任务完成前,主线程可能会在QueuedWork.waitToFinish()处阻塞。这是为了确保写入任务的完整性,但也可能导致主线程阻塞时间过长,触发ANR。
优化方案: 通过对比线下测试中MMKV与sp的性能数据,发现MMKV在以下三个问题上表现较优。通过编译器切面的方式,接管所有getSharedPreferences接口调用,根据白名单配置返回MMKV实现或者原始系统的SharedPreferencesImpl实现,使业务层使用无感知。
2. 网络广播监听耗时优化
问题描述: 线上ANR traces数据显示,getActiveNetworkInfo的ipc调用耗时较长,可能是因为ipc跨进程通信本身的耗时,以及监听网络状态的广播监听者实例过多,每个都会重复调用一次查询网络状态,导致耗时加剧。
优化方案: 通过动态代理IConnectivityManager接口,拦截代理getActiveNetworkInfo方法,优先使用缓存。统一全局的网络广播监听器在异步线程IPC获取网络信息,更新缓存,后续可以直接使用缓存,避免多次IPC调用。
3. 启动组件延迟注册
问题描述: 在Application#onCreate阶段,串行任务会阻塞主线程执行,从而可能引发ANR。系统发送的关键消息得不到主线程调度。
优化方案: 尽量避免在启动阶段注册receiver、service等组件,或者延迟到onCreate全部执行完毕再注册。通过在Application的registerReceiver方法中判断是否完成初始化,如果未完成则通过Handler延迟注册。
网友评论