ANR问题总结分析

3,485 阅读10分钟

四大组件的onCreate/onReceive方法中Thread.sleep(),会产生几个ANR?

先回答一下上面会产生几个ANR(Application Not Responding),答案是两个,分别是在BroadcastReceiver和Service中产生的。是不是和你想的一样呢,如果不一样可以看看我下面的分析。所谓耗时,即当前线程停滞不前,不再执行后面的逻辑,因此这里使用Thread.sleep()方法(耗时把时间耗在休眠上)。

ANR(Application Not Responding)指的是应用程序未响应,如果 Android 应用的界面线程处于阻塞状态的时间过长,会触发“应用无响应”(ANR) 错误。如果应用位于前台,系统会向用户显示一个对话框,如图 1 所示。ANR 对话框会为用户提供强行退出应用的选项。

哪些场景会造成ANR?

  • Service Timeout:比如前台服务在20s内未执行完成

    • 对于前台服务,则超时为SERVICE_TIMEOUT = 20s
    • 对于后台服务,则超时为SERVICE_BACKGROUND_TIMEOUT = 200s
  • BroadcastQueue Timeout:比如前台广播在10s内未执行完成

    对于广播队列有两个: foreground队列和background队列:

    • 对于前台广播,则超时为BROADCAST_FG_TIMEOUT = 10s
    • 对于后台广播,则超时为BROADCAST_BG_TIMEOUT = 60s
  • ContentProvider Timeout:内容提供者,在publish过超时10s

    ContentProvider Timeout是位于”ActivityManager”线程中的AMS.MainHandler收到CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG消息时触发。

  • InputDispatching Timeout: 输入事件分发超时5s,包括按键和触摸事件。

验证标题的疑问

下面我们开始实践一下,分别在四大组件和Application(顺便测试)的onCreate/onReceive方法中进行Thread.sleep(21_000)。

Activity

首先是Activity中的onCreate

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test2);
        Log.i(TAG, "onCreate: ");
        try {
            Thread.sleep(21_000);
            Log.i(TAG, "onCreate: ====2");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

并没有出现ANR,从上面总结的那些场景,也并没有这个场景。有些人可能理解在主线程做了耗时操作会引起ANR,ANR的意思是应用没有响应,但是耗时操作实际上并不一定会导致没有响应。

举个例子来验证,在onCreate方法中设置按钮点击事件

        findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                sleepTest();
            }
        });

sleepTest方法详情

public void sleepTest(){
    new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            Button button = findViewById(R.id.btn);
            button.setText("jackie");
        }
    },7000);
    try {
        Thread.sleep(21_000);
        Log.i(TAG, "onCreate: ====2");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

点击按钮,是否会发生ANR,答案是不会。这就是耗时操作不一定会导致没有响应,和onCreate方法中进行sleep同理,但是如果你点击了按钮之后又点击了返回键,这个时候就会出现ANR了,因为主线程被休眠了,你点击了按键在5s内没有响应,这个时候就会出现ANR。

Service

在Service中的onCreate方法,默认创建的启动Activity创建的是前台服务(20s),所以会造成ANR。

    @Override
    public void onCreate() {
        super.onCreate();
        try {
            Log.i(TAG, "onCreate: ====1===");
            //Thread.sleep(19000);  //不发生anr
            Thread.sleep(21000); //发生anr
            Log.i(TAG, "onCreate: ====2====");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
ContentProvider
@Override
public boolean onCreate() {
    Log.i(TAG, "onCreate: ===1====");
    try {
        Thread.sleep(21_000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    Log.i(TAG, "onCreate: =====2=======");
    return false;
}

这里也不会产生ANR,前面说到ContentProvider在publish过超时10s,”ActivityManager”线程中的AMS.MainHandler收到CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG消息时触发。而通过查看ContentProvider源码和启动流程,我们可以看到onCreate方法在publish之前执行,所以在onCreate方法中并不会造成ANR。

BroadcastReceiver

发送广播

Intent intent = new Intent("android.intent.jackie");
//设置为前台广播,默认是后台广播
intent.setPackage(getPackageName()); //一定要设置包名,否则无法显示anr,具体暂不深究
//intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
Test2Activity.this.sendBroadcast(intent);

onReceive中执行耗时

public class TestBroadcastReceiver extends BroadcastReceiver {
    private static final String TAG = "TestBroadcastReceiver";
    @Override
    public void onReceive(Context context, Intent intent) {
        Log.i(TAG, "onReceive: ===1====");
        try {
            //这里接受的都是前台广播
            //Thread.sleep(9000); //不发生anr
            Thread.sleep(21_000); //发生anr
            //这里接受的都是后台广播
            //Thread.sleep(65000); //发生anr
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Log.i(TAG, "onReceive: ===2====");
    }
}

对于前台广播,则超时为BROADCAST_FG_TIMEOUT = 10s,后台广播的耗时是60s。所以这里会产生ANR。注意发送广播的时候要添加intent的包名。

Application

顺便做一下Application的测试

@Override
public void onCreate() {
    super.onCreate();
    Log.i(TAG, "onCreate: ==a===");
    try {
        Thread.sleep(21_000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    Log.i(TAG, "onCreate: ===b===");

}

并不会发生ANR,Android中的ANR并没有这个场景。

小结

除了输入事件造成的ANR,我们可以把其他ANR比作是引爆炸弹,那么整个流程包含三部分组成:

  1. 埋定时炸弹:中控系统(system_server进程)启动倒计时,在规定时间内如果目标(应用进程)没有干完所有的活,则中控系统会定向炸毁(杀进程)目标。
  2. 拆炸弹:在规定的时间内干完工地的所有活,并及时向中控系统报告完成,请求解除定时炸弹,则幸免于难。
  3. 引爆炸弹:中控系统立即封装现场,抓取快照,搜集目标执行慢的罪证(traces),便于后续的案件侦破(调试分析),最后是炸毁目标。

ANR的流程可以分为,埋炸弹->拆炸弹->引爆炸弹,如果埋的炸弹在一定时间内没有被拆除,就是**发送延迟消息一定时间内没有被移除,那就会引爆(触发),产生ANR。**详细分析可以查看该文章

输入事件造成的ANR(特殊)

再来一个灵魂拷问:

在一个Activity中一个按钮点击事件中设置Thread.sleep(10_000),如果连续点击两次,会不会造成ANR,如果连续点击三次呢?

        findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                try {
                    Thread.sleep(10_000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

在Android29上(之前的版本并没有测试,预计应该一样的),连续点击两次并不会造成ANR,对于input事件,如果仅仅点击没有后续的点击输入事件不会造成ANR,可我们不是连续点击了两次了吗?因为第一次点击事件已经被消费了,在onClick中才进行sleep,然后我们点击第二次,这个时候只有一个input事件,没有后续的点击输入,所以不会造成ANR。

而如果我们连续点击三次,除去第一次被消费掉,这个时候就会有连续的input输入,会产生ANR。如果我们把Thread.sleep写在onTouch中,只要一次点击就行,因为该方法会被回调两次,down和up是两次input时间。 注意:如果是按键事件(假如点击返回键),如果主线程在sleep(),这个时候点击了返回键,5s没响应,直接抛出ANR。

Android R上的修改

在Android R(11)上,对于上面的输入事件造成的ANR的那个问题,只需要连续两次点击就可以造成ANR,该版本上对input机制做了较大的调整,有兴趣可以自行研究一下。

如何手动设计一个ANR监听

FileObserver

Android中发送ANR之后,Android系统会将ANR Log输出至/data/anr/trace.txt,**本方案就是监听trace.txt文件。**在Android中可以通过FileObserver来实现对文件的监听,FileObserver是一个用于监听文件的访问,创建,修改,删除,移动等操作的监听器。但是在android5.0的普通用户app,不一定有权限来实现对文件的监听,一些系统的app可以用该方式实现。

public class ANRFileObserver extends FileObserver {


    public ANRFileObserver(String path) {//data/anr/
        super(path);
    }

    public ANRFileObserver(String path, int mask) {
        super(path, mask);
    }

    @Override
        public void onEvent(int event, @Nullable String path) {
            switch (event)
        {
            case FileObserver.ACCESS://文件被访问
                Log.i("Jackie", "ACCESS: " + path);
                break;
            case FileObserver.ATTRIB://文件属性被修改,如 chmod、chown、touch 等
                Log.i("Jackie", "ATTRIB: " + path);
                break;
            case FileObserver.CLOSE_NOWRITE://不可写文件被 close
                Log.i("Jackie", "CLOSE_NOWRITE: " + path);
                break;
            case FileObserver.CLOSE_WRITE://可写文件被 close
                Log.i("Jackie", "CLOSE_WRITE: " + path);
                break;
            case FileObserver.CREATE://创建新文件
                Log.i("Jackie", "CREATE: " + path);
                break;
            case FileObserver.DELETE:// 文件被删除,如 rm
                Log.i("Jackie", "DELETE: " + path);
                break;
            case FileObserver.DELETE_SELF:// 自删除,即一个可执行文件在执行时删除自己
                Log.i("Jackie", "DELETE_SELF: " + path);
                break;
            case FileObserver.MODIFY://文件被修改
                Log.i("Jackie", "MODIFY: " + path);
                break;
            case FileObserver.MOVE_SELF://自移动,即一个可执行文件在执行时移动自己
                Log.i("Jackie", "MOVE_SELF: " + path);
                break;
            case FileObserver.MOVED_FROM://文件被移走,如 mv
                Log.i("Jackie", "MOVED_FROM: " + path);
                break;
            case FileObserver.MOVED_TO://文件被移来,如 mv、cp
                Log.i("Jackie", "MOVED_TO: " + path);
                break;
            case FileObserver.OPEN://文件被 open
                Log.i("Jackie", "OPEN: " + path);
                break;
            default:
                //CLOSE : 文件被关闭,等同于(IN_CLOSE_WRITE | IN_CLOSE_NOWRITE)
                //ALL_EVENTS : 包括上面的所有事件
                Log.i("Jackie", "DEFAULT(" + event + "): " + path);
                break;
        }
    }
}
ANRWatchDog

我们知道android是基于消息处理机制的,整个程序的运行都是在一个loop死循环中,通过handler对消息进行分发,所以我们可以开启一个线程通过Handler定时发送消息到我们的主线程,如果该消息超过一定时间未被处理,我们就可以判断发生了ANR。

public class ANRWatchDog extends Thread {

    private static final String TAG = "ANR";
    private int timeout = 5000;
    private boolean ignoreDebugger = true;

    static ANRWatchDog sWatchdog;

    private Handler mainHandler = new Handler(Looper.getMainLooper());

    private class ANRChecker implements Runnable{

        private boolean mCompleted;
        private long mStartTime;
        //这个值记录了系统启动到当前时刻经过的时间
        private long executeTime = SystemClock.uptimeMillis();

        @Override
        public void run() {
            synchronized (ANRWatchDog.this) {
                mCompleted = true;
                executeTime = SystemClock.uptimeMillis();
            }
        }

        void schedule() {
            mCompleted = false;
            mStartTime = SystemClock.uptimeMillis();
            //每次都post到该队列的前面
            mainHandler.postAtFrontOfQueue(this);
        }

        boolean isBlocked() {
            return !mCompleted || executeTime - mStartTime >= 5000;
        }
    }

    public interface ANRListener {
        void onAnrHappened(String stackTraceInfo);
    }

    private ANRChecker anrChecker = new ANRChecker();

    private ANRListener anrListener;

    public void addANRListener(ANRListener listener){
        this.anrListener = listener;
    }

    public static ANRWatchDog getInstance(){
        if(sWatchdog == null){
            sWatchdog = new ANRWatchDog();
        }
        return sWatchdog;
    }

    private ANRWatchDog(){
        super("ANR-WatchDog-Thread");
    }

    @Override
    public void run() {
        //super.run();
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); //设置为后台线程
        while (true){
            while (!isInterrupted()){
                synchronized (this){
                    anrChecker.schedule();
                    long waitTime = timeout;
                    long start = SystemClock.uptimeMillis();
                    //确保sleep一定的时间,防止假唤醒
                    while (waitTime > 0){
                        try{
                            wait(waitTime);
                        } catch (InterruptedException e){
                            Log.w(TAG, "run: ", e);
                        }
                        waitTime = timeout - (SystemClock.uptimeMillis() - start);
                    }
                    if (!anrChecker.isBlocked()){
                        continue;
                    }
                }
                if (!ignoreDebugger && Debug.isDebuggerConnected()){
                    continue;
                }
                String stackTraceInfo = getStackTraceInfo();
                if (anrListener != null){
                    anrListener.onAnrHappened(stackTraceInfo);
                }
            }
            anrListener = null;

        }

    }
    private String getStackTraceInfo() {
        StringBuilder stringBuilder = new StringBuilder();
        for (StackTraceElement stackTraceElement : Looper.getMainLooper().getThread().getStackTrace()) {
            stringBuilder
                    .append(stackTraceElement.toString())
                    .append("\r\n");
        }
        return stringBuilder.toString();
    }
}

模拟ANR的场景

void ANRTest(){
    handler = new Handler();
    ANRWatchDog.getInstance().addANRListener(new ANRWatchDog.ANRListener() {
        @Override
        public void onAnrHappened(String stackTraceInfo) {
            Log.i(TAG, "onAnrHappened: =======发生了ANR: "+ stackTraceInfo);
        }
    });
    ANRWatchDog.getInstance().start();
    //模拟场景,通过handler发送消息sleep 10秒,我们ANR的检测工具就会检测到自己发送的消息超过5s没有被处理,发生了"ANR"了。
    handler.postDelayed(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(1000 * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    },3000);

}

如何分析ANR文件

对于ANR的分析网上有很多文章,我们只要留意那些可能发生ANR的场景,就可能很大程度上避免ANR。诊断ANR 时需要考虑以下几种常见模式:

  1. 应用在主线程上非常缓慢地执行涉及 I/O 的操作。
  2. 应用在主线程上进行长时间的计算。
  3. 主线程在对另一个进程进行同步 binder 调用,而后者需要很长时间才能返回。
  4. 主线程处于阻塞状态,为发生在另一个线程上的长操作等待同步的块。
  5. 主线程在进程中或通过 binder 调用与另一个线程之间发生死锁。主线程不只是在等待长操作执行完毕,而且处于死锁状态。如需更多信息,请参阅维基百科上的死锁

对于具体的ANR文件分析和解决方案,可以参考官网和这篇文章

总结

本文对于ANR的分析,并没有仔细深入源码(如果想要看ANR源码的分析可以查看gityuan的一些列文章),而是做了大量的代码实践得出结论,当然我也是看了源码再进行代码验证,看源码固然重要,但是如果你仅仅停留在看源码的脚步,以上说的那些场景实践结果你不一定能全答对。希望这篇文章能帮助到你,喜欢的点个赞吧。

参考文章

gityuan.com/2016/07/02/…

gityuan.com/2019/04/06/…

my.oschina.net/u/920274/bl…

blog.csdn.net/lusing/arti…

juejin.cn/post/684490…