ANR产生的原因及定位分析

ANR全称是Application Not Responding,意思是应用程序无响应。相信从事Android开发的肯定遇到过。ANR的直观体验是用户在操作App的过程中,感觉界面卡顿,当界面卡顿超过一定时间(一般5秒),就会出现ANR对话框。ANR对于一个应用来说是不能承受之痛,其影响并不比应用发生Crash小。

ANR产生的原因

只有当应用程序的UI线程响应超时才会引起ANR,超时产生原因一般有两种。

  • 当前的事件没有机会得到处理,例如UI线程正在响应另一个事件,当前事件由于某种原因被阻塞了。
  • 当前的事件正在处理,但是由于耗时太长没能及时完成。

根据ANR产生的原因不同,超时事件也不尽相同,从本质上将,产生ANR的原因有三种,大致可以对应到android中四大组件中的三个(Activity/View,BroadcastReceiver和Service)。

KeyDispatchTimeout

最常见的一种类型,原因就是View的点击事件或者触摸事件在特定的时间(5s)内无法得到响应。

BroadcastTimeout

原因是BroadcastReceiver的onReceive()函数运行在主线程中,在特定的时间(10s)内无法完成处理。

ServiceTimeout

比较少出现的一种类型,原因是Service的各个生命周期函数在特定时间(20s)内无法完成处理。

典型的ANR问题场景

  • 应用程序UI线程存在耗时操作。例如在UI线程中进行联网请求,数据库操作或者文件操作等。
  • 应用程序的UI线程等待子线程释放某个锁,从而无法处理用户的输入。
  • 耗时的动画需要大量的计算工作,可能导致CPU负载过重。

ANR的定位和分析

当发生ANR时,可以通过结合Logcat日志和生成的位于手机内部存储的/data/anr/traces.tex文件进行分析和定位。

Cmd line: com.anly.githubapp  // 最新的ANR发生的进程(包名)

...

DALVIK THREADS (41):
"main" prio=5 tid=1 Sleeping
  | group="main" sCount=1 dsCount=0 obj=0x73467fa8 self=0x7fbf66c95000
  | sysTid=2976 nice=0 cgrp=default sched=0/0 handle=0x7fbf6a8953e0
  | state=S schedstat=( 0 0 0 ) utm=60 stm=37 core=1 HZ=100
  | stack=0x7ffff4ffd000-0x7ffff4fff000 stackSize=8MB
  | held mutexes=
  at java.lang.Thread.sleep!(Native method)
  - sleeping on <0x35fc9e33> (a java.lang.Object)
  at java.lang.Thread.sleep(Thread.java:1031)
  - locked <0x35fc9e33> (a java.lang.Object)
  at java.lang.Thread.sleep(Thread.java:985) // 主线程中sleep过长时间, 阻塞导致无响应.
  at com.tencent.bugly.crashreport.crash.c.l(BUGLY:258)
  - locked <@addr=0x12dadc70> (a com.tencent.bugly.crashreport.crash.c)
  at com.tencent.bugly.crashreport.CrashReport.testANRCrash(BUGLY:166)  // 产生ANR的那个函数调用
  - locked <@addr=0x12d1e840> (a java.lang.Class<com.tencent.bugly.crashreport.CrashReport>)
  at com.anly.githubapp.common.wrapper.CrashHelper.testAnr(CrashHelper.java:23)
  at com.anly.githubapp.ui.module.main.MineFragment.onClick(MineFragment.java:80) // ANR的起点
  at com.anly.githubapp.ui.module.main.MineFragment_ViewBinding$2.doClick(MineFragment_ViewBinding.java:47)
  at butterknife.internal.DebouncingOnClickListener.onClick(DebouncingOnClickListener.java:22)
  at android.view.View.performClick(View.java:4780)
  at android.view.View$PerformClick.run(View.java:19866)
  at android.os.Handler.handleCallback(Handler.java:739)
  at android.os.Handler.dispatchMessage(Handler.java:95)
  at android.os.Looper.loop(Looper.java:135)
  at android.app.ActivityThread.main(ActivityThread.java:5254)
  at java.lang.reflect.Method.invoke!(Native method)
  at java.lang.reflect.Method.invoke(Method.java:372)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)

现在找到了ANR产生的原因和位置了,就可以对产生ANR的代码进行修复。这么一个一个的找肯定不现实,那我们就来说说如何避免和检测ANR。

ANR的避免和检测

为了避免在开发中引入可能导致应用发生ANR的问题,除了切记不要在主线程中作耗时操作,我们也可以借助于一些工具来进行检测,从而更有效的避免ANR的引入。

StrictMode

严格模式StrictMode是Android SDK提供的一个用来检测代码中是否存在违规操作的工具类,StrictMode主要检测两大类问题。

  • 线程策略 ThreadPolicy
    • detectCustomSlowCalls:检测自定义耗时操作
    • detectDiskReads:检测是否存在磁盘读取操作
    • detectDiskWrites:检测是否存在磁盘写入操作
    • detectNetWork:检测是否存在网络操作
  • 虚拟机策略VmPolicy
    • detectActivityLeaks:检测是否存在Activity泄露
    • detectLeakedClosableObjects:检测是否存在未关闭的Closeable对象泄露
    • detectLeakedSqlLiteObjects:检测是否存在Sqlite对象泄露
    • setClassInstanceLimit:检测类实例个数是否超过限制

可以看到,ThreadPolicy可以用来检测可能催在的主线程耗时操作,需要注意的是我们只能在Debug版本中使用它,发布到市场上的版本要关闭掉。StrictMode的使用很简单,我们只需要在应用初始化的地方例如Application或者MainActivity类的onCreate方法中执行如下代码:

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

        // 开启线程模式
        StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                .detectAll()
                .penaltyLog()
                .penaltyDialog() ////打印logcat,当然也可以定位到dropbox,通过文件保存相应的log
                .build());
        // 开启虚拟机模式
        StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                .detectAll()
                .penaltyLog()
                .build());
    }
}

上面的初始化代码调用penaltyLog表示在Logcat中打印日志,调用detectAll方法表示启动所有的检测策略,我们也可以根据应用的具体要求只开启某些策略,语句如下:

         StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                 .detectDiskReads()
                 .detectDiskWrites()
                 .detectNetwork()  
                 .penaltyLog()
                 .build());

         StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                 .detectLeakedSqlLiteObjects()
                 .detectLeakedClosableObjects()
                 .detectActivityLeaks()
                 .penaltyLog()
                 .build());

BlockCanary

BlockCanary是一个非侵入式式的性能监控函数库,它的用法和leakCanary类似,只不过后者监控应用的内存泄露,而BlockCanary主要用来监控应用主线程的卡顿。它的基本原理是利用主线程的消息队列处理机制,通过对比消息分发开始和结束的时间点来判断是否超过设定的时间,如果是,则判断为主线程卡顿。它的集成很简单,首先在build.gradle中添加依赖

一般选取以下其中一个 case 引入即可

dependencies {
    compile 'com.github.markzhai:blockcanary-android:1.5.0'

    // 仅在debug包启用BlockCanary进行卡顿监控和提示的话,可以这么用
    debugCompile 'com.github.markzhai:blockcanary-android:1.5.0'
    releaseCompile 'com.github.markzhai:blockcanary-no-op:1.5.0'
}

然后在Application类中进行配置和初始化即可

public class AnrDemoApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        // 在主进程初始化调用哈
        BlockCanary.install(this, new AppBlockCanaryContext()).start();

    }
}

实现自己监控的上下文


public class AppBlockCanaryContext extends BlockCanaryContext {
    // 实现各种上下文,包括应用标示符,用户uid,网络类型,卡慢判断阙值,Log保存位置等

    /** * Implement in your project. * * @return Qualifier which can specify this installation, like version + flavor. */
    public String provideQualifier() {
        return "unknown";
    }


    /** * Implement in your project. * * @return user id */
    public String provideUid() {
        return "uid";
    }

    /** * Network type * * @return {@link String} like 2G, 3G, 4G, wifi, etc. */
    public String provideNetworkType() {
        return "unknown";
    }

    /** * Config monitor duration, after this time BlockCanary will stop, use * with {@code BlockCanary}'s isMonitorDurationEnd * * @return monitor last duration (in hour) */
    public int provideMonitorDuration() {
        return -1;
    }

    /** * Config block threshold (in millis), dispatch over this duration is regarded as a BLOCK. You may set it * from performance of device. * * @return threshold in mills */
    public int provideBlockThreshold() {
        return 1000;
    }

    /** * Thread stack dump interval, use when block happens, BlockCanary will dump on main thread * stack according to current sample cycle. * <p> * Because the implementation mechanism of Looper, real dump interval would be longer than * the period specified here (especially when cpu is busier). * </p> * * @return dump interval (in millis) */
    public int provideDumpInterval() {
        return provideBlockThreshold();
    }

    /** * Path to save log, like "/blockcanary/", will save to sdcard if can. * * @return path of log files */
    public String providePath() {
        return "/blockcanary/";
    }

    /** * If need notification to notice block. * * @return true if need, else if not need. */
    public boolean displayNotification() {
        return true;
    }

    /** * Implement in your project, bundle files into a zip file. * * @param src files before compress * @param dest files compressed * @return true if compression is successful */
    public boolean zip(File[] src, File dest) {
        return false;
    }

    /** * Implement in your project, bundled log files. * * @param zippedFile zipped file */
    public void upload(File zippedFile) {
        throw new UnsupportedOperationException();
    }


    /** * Packages that developer concern, by default it uses process name, * put high priority one in pre-order. * * @return null if simply concern only package with process name. */
    public List<String> concernPackages() {
        return null;
    }

    /** * Filter stack without any in concern package, used with @{code concernPackages}. * * @return true if filter, false it not. */
    public boolean filterNonConcernStack() {
        return false;
    }

    /** * Provide white list, entry in white list will not be shown in ui list. * * @return return null if you don't need white-list filter. */
    public List<String> provideWhiteList() {
        LinkedList<String> whiteList = new LinkedList<>();
        whiteList.add("org.chromium");
        return whiteList;
    }

    /** * Whether to delete files whose stack is in white list, used with white-list. * * @return true if delete, false it not. */
    public boolean deleteFilesInWhiteList() {
        return true;
    }

    /** * Block interceptor, developer may provide their own actions. */
    public void onBlock(Context context, BlockInfo blockInfo) {

    }
}

在AndroidManifest.xml文件中声明Application,一定不要忘记

《ANR产生的原因及定位分析》

现在就已经将BlockCanary集成到应用里面了,接下来,编译安装到手机上,点击测试按钮,将产生一个ANR,效果如图:

《ANR产生的原因及定位分析》

    原文作者:PackageManagerService
    原文地址: https://juejin.im/entry/597026806fb9a06bcb7fc660
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞