深度探索Android ANR:监控、排查、优化全攻略

2023-12-19  本文已影响0人  BlueSocks

背景问题

1. 什么是ANR?

ANR是Android系统中的一种错误状态,全称为Application Not Responding,中文翻译为“应用无响应”。当Android系统检测到应用程序在一段时间内未能响应用户输入或无法执行主要的UI线程操作时,就会触发ANR错误。ANR是一种系统保护机制,旨在确保应用的响应性,防止用户在使用应用时遇到卡顿或无响应的情况。

2. ANR对用户体验的影响

ANR问题会直接影响用户体验,以下是一些具体影响:

3. 产生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。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

合理使用HandlerLooper进行异步消息处理,避免在UI线程上执行过多耗时操作。

// 示例:使用Handler进行异步消息处理
Handler handler = new Handler(Looper.getMainLooper());

handler.post(new Runnable() {
    @Override
    public void run() {
        // 在UI线程执行任务
    }
});

合理使用锁

避免UI线程上的阻塞

确保在UI线程上不要使用过多的同步锁,以避免发生死锁或长时间阻塞。

使用精确的锁

在需要同步的地方,使用精确的锁(如synchronized关键字)而不是全局锁,以减小锁的范围。

异步加载数据

使用Loader或ViewModel

通过使用LoaderViewModel来异步加载数据,可以在后台线程中执行,避免在UI线程上阻塞。

合理使用BroadcastReceiver

异步处理广播

广播接收器中的操作应该尽量简短,避免在主线程上执行长时间操作。可考虑将耗时任务放入异步任务或线程池中。

动态注册广播接收器

避免使用静态注册广播接收器,因为静态注册的接收器可能在应用处于非活动状态时触发,增加了ANR的风险。

通过采取上述优化方案,开发者可以显著减少Android应用中ANR的发生概率,提升应用的响应性和用户体验。

最佳实践

1. SharedPreference优化

问题描述: 线上ANR traces数据显示,SharedPreference(sp)导致的ANR问题主要集中在以下三类情况:

  1. SP文件加载与UI线程阻塞 在SP文件创建后,系统会单独使用一个线程来加载解析对应的SP文件。当UI线程尝试访问SP中的内容时,存在以下问题:UI线程阻塞: 如果SP文件还未被完全加载解析到内存,UI线程在尝试访问SP内容时会被阻塞,直到SP文件被完全加载到内存为止。这可能导致UI线程无法响应用户操作,引发ANR。加载顺序问题: 由于SP文件加载是在单独的线程中进行的,当UI线程需要访问SP内容时,可能发生加载尚未完成的情况,从而阻塞UI线程。
  2. 数据跨进程通信与主线程等待 为了确保数据的跨进程完整性,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延迟注册。

上一篇下一篇

猜你喜欢

热点阅读