Andorid性能优化(五) 之 ANR总结和分析

1 概述

ANR(Application Not responding)是指应用程序无响应,Android中会在主线程中针对不同的场景监控应用程序的响应时间,如果在超出该场景限定的超时时间还没有将逻辑处理完毕就会造成ANR。一般情况下系统会弹出一个对话框告知用户当前应用程序无响应,用户可以选择继续等待或者选择关闭结束掉当前应用程序。

2 ANR的场景

触发ANR的场景这以下这些:

InputDispatching Time out:        5秒内无法响应屏幕触摸事件或者键盘输入事件触发ANR。

Broadcast Time out                    前台Broadcast为10秒;后台Broadcast为60秒内,onRecieve方法逻辑未处理完成触发ANR。

Service Time out                         前台Service超过20秒;后台Service超过200秒未完成启动触发ANR。

ContentProvider Time out          ContentProvider的启动过程超过10秒未完成触发ANR。

3 ANR机制原理

ANR的机制原理很简单,就是在相应场景要进行相关逻辑处理之前会先通过sendMessageAtTime或sendMessageDelayed发送一个ANR Message,延时时间为上述场景中所述的时间。待开发者相关逻辑执行完毕后就remove掉该条Message。如果开发者写的相关逻辑在规定时间内没有执行完成,Message就会被Handler执行,就会发生ANR。

对于ContentProvider是在其进程启动时发生ANR,所以它不会弹出ANR对话框,而是直接杀进程以及清理相应信息。无论是四大组件还是进程等场景,只要发生ANR之后,最终都会调用到ActivityManagerSerfvice中的appNotResponding方法去。

在执行AMS的appNotResponding方法后,其内部做了以下几件事情:

  1. 输出ANR相关信息到EventLog中;
  2. 收集并输出Java进程中各个线程的traces信息到/data/anr/traces.txt;
  3. 记录当前各个进程的CPU使用和负载情况;
  4. 将traces文件和CPU使用信息保存到data/system/dropbox目录;
  5. 根据情况决定是否杀掉进程还是弹出ANR对话框。

4 避免ANR的途径

ANR往往是我们开发过程中的疏忽或贪途方便而不经意导致的,有些可能在开发过程环境中正常不过的逻辑跑到用户手机上就会出现ANR,因为用户环境的复杂性是不可预知的,所以我们在开发过程中设立有效的代码军规来避免或减少ANR的发生概率是非常有必要的。

4.1 避免主线程进行IO操作

Android中4.0以后就不允许主线程进行网络请求操作了,但是依然允许主线程中进行数据库或文件的IO操作。主线程进行IO操作是风险极大的,在性能稍差的手机可能会造成掉帧从而影响体验,严重的话还会导致ANR。所以一定要避免此类不负责的代码逻辑。

4.2 正确使用SharedPreference

其实SharedPreferences的提交也是IO操作。因为SharedPreferences的commit()方法是在主线程中去执行的,而apply()方法是发生在工作线程中。所以最好不要在主线程中去执行commit(),而使用apply()替换。而且还要注意的是,尽量减少edit的使用频率,可以改为批量处理,apply()方法在Activity的stop时主线程会等待写入完成,所以如果多次提交很容量引起卡顿。另外,如果存储的文件过大也会影响读取速度,所以读取频繁的key最好单独存放。

4.3 组件生命周期函数不做耗时操作

任何时候,各种组件的生命周期函数都不应该做耗时的操作。需要耗时操作逻辑交由线程或线程池去处理就是了。即使是后台Service,虽然超时时间是200秒,也不会有用户输入引起事件无响应ANR,但是其执行时间过长终究是风险。还要注意BroadcastReciever的onRecieve()方法中也不能做耗时操作,若需要耗时操作也是可以交由线程或线程池去处理,如果担心在后台,进程优先级低容易被杀死的话,也可以选择使用IntentService去执行相应操作。

4.4 避免主线程被锁

主线程一旦被死锁基本就是等于ANR了,我们一定要尽量避免主线程被锁的情况。在一些同步的操作时主线程就有可能被锁,需要等待其它子线程相应锁得到释放才能继续执行,这样就很可能造成ANR。所以我们在开发过程中一定要注意主线程和子线程抢占同步锁的情况。下面列出避免死锁的方法:

避免嵌套锁

这是死锁最常见的原因,如果你已经持有一个资源,请避免锁定另一个资源。如果只使用一个对象锁,则几乎不可能出现死锁情况。所以在实际中应该只锁需要的部分来避免嵌套锁。

加锁顺序

有些时候,往往避免不了嵌套锁的发生,所以在这时我们就要注意加锁的顺序。当每个线程访问加锁的地方时,只要每个线程访问加锁的顺序是一样的话,那么就不会发生死锁情况。这是一种有效的预防死锁的机制。但是,这种方式需要你事先知道所有可能会用到的锁。

5 分析ANR问题

5.1 造成ANR原因

造成ANR的原因我们已经清楚,就是在主线程中的逻辑在规定时间内没有处理完成。一般地造成这种情况的原因有下面几点:

  1. CPU资源被占用,进程没有被分配足够的资源
  2. 主线程中做了耗时的工作
  3. 主线程被锁

前面介绍原理时讲到,在发生ANR时,系统会输出ANR相关的信息到日志中和将各个线程的trace信息收集并输出到/data/anr/traces.txt文件中去。所以我们要定位ANR原因,可以通过ANR日志+traces.txt文件来进行分析(可以通过adb命令:adb pull /data/anr/traces.txt traces.txt将其拷贝到电脑中来)。

5.2 分析Log

首先,我们先来模拟一个较简单和常见的ANR例子,代码如下:

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button btn = findViewById(R.id.btn_1);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                SystemClock.sleep(20 * 1000);
            }
        });
    }
}

如下图,是一个模拟主线程做了耗时工作所发生的ANR日志截图。一般可以从日志中可以搜索关键字“ANR in”来锁定ANR信息部分。从中可以看到了发生ANR的时间、进程、类(这里是.MainActivity)、场景类型(这里是Input dispatching time out)、CPU等信息。

这里的CPU信息中有两个时间,表示在ANR前和后的用量。接着下面一行开始是系统当前活着的进程的CPU占用百分比。

如果发现某些进程的CPU占用百分比比较高,几乎占用了所有的CPU资源,而发生ANR的进程CPU占用为0%或者非常低,则可以判断为CPU资源被占用,进程没有被分配到足够的资源导致的ANR。这种情况可以认为是系统问题。

如果发现发生ANR的进程CPU占用比较高,那么应该要怀疑是否我们自己程序中由于逻辑使用不当导致CPU资源被消耗,这时就要结合trace.txt来进一步分析。

5.3 分析trace.txt

继续上述中的模拟例子,我们通过adb将trace.txt文件拷贝出来并打开。如下两张图,图1中第2行中pid 2888是表示发生ANR的进程ID是2888,所以我们可以通过搜索2888来定位进程的信息,如图2所示。

图中可以看到有tid=1,表示这是主线程的信息,因为此处ANR是主线程做了耗时操作阻塞造成的。然后往下看能直到一些代码堆栈信息,当中便能发现图中第92行中定位到源码中MainActivity的第21行造成的阻塞引起的点击长时间无响应。

像由于代码耗时造成阻塞的问题还是比较好定位的,下面我们继续来模拟一个稍微复杂点ANR的例子。代码如下:

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

//        Button btn = findViewById(R.id.btn_1);
//        btn.setOnClickListener(new View.OnClickListener() {
//            @Override
//            public void onClick(View v) {
//                SystemClock.sleep(20 * 1000);
//            }
//        });

        new Thread(new Runnable() {
            @Override
            public void run() {
                testANR();
            }
        }).start();

        SystemClock.sleep(1000);
        initView();
    }

    private synchronized void testANR() {
        SystemClock.sleep(30 * 1000);
    }

    private synchronized void initView() {
    }
}

上述例子代码在运行后就会导致ANR,原因是在Activity的onCreate中开启了一个线程执行了带锁的方法testANR(),而主线程中也同时执行了一个带锁的方法initView(),这样一来initView()肯定会因为等待testANR()所持有的锁而被同步住,这样就产生了一个稍微复杂点的ANR情况。

我们现在就来分析trace.txt信息,首先还是通过adb将trace.txt文件拷贝出来并打开。如下两张图,图1中第2行中pid 3516是表示发生ANR的进程ID是3516,所以我们可以通过搜索3516来定位进程的信息,如图2所示。

图中可以看到有tid=1,表示这是主线程的信息,因为此处ANR是主线程做了耗时操作阻塞造成的。图中第87行就是定位出MainActivity.initView()问题所在处,请继续看下一行,也就是红框圈住的那一行,其中有关键字“waiting to lock <0xXXX>”,表示主线程正在等待一个锁<0x0c5f896f>,这个锁的类型对象是MainActivity,并且被线程id为13的线程持有。因此需要再看一下线程id为13的情况,可以通过搜索功能搜索“tid=13”来定位,如下图:

从上图中可以看到,该线程是正在处于休眠状态,sleep的原因就是MainActivity的第38行。这个时候就可以发现子线程的testANR()和主线程的initView方法都加在了synchronized关键字在竞争同一个锁导致的,这样一来ANR的原因就明确了。

 

 

 

 

发布了106 篇原创文章 · 获赞 37 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/lyz_zyx/article/details/86229578