ANR产生的原因及定位分析

xiaoxiao2021-02-28  64

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,一定不要忘记

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

转载请注明原文地址: https://www.6miu.com/read-29504.html

最新回复(0)