Android性能优化(五)--更快--稳定--响应速度ANR

1 ANR简介

  • ANR,是“Application Not Responding”的缩写,即“应用程序无响应”。
  • 在Android中,ActivityManagerService(简称AMS)和WindowManagerService(简称WMS)会监测应用程序的响应时间,如果应用程序主线程(即UI线程)在超时时间内对输入事件没有处理完毕(或没有处理),或者对特定操作没有执行完毕,就会出现ANR。
  • 对于输入事件没有处理完毕产生的ANR,Android会显示一个对话框,提示用户当前应用程序没有响应,用户可以选择继续等待或者关闭这个应用程序(也就是杀掉这个应用程序的进程)。
2 为什么会有ANR ANR的产生需要同时满足三个条件:
  • 主线程:只有应用程序进程的主线程响应超时才会产生ANR;
  • 超时时间:产生ANR的上下文不同,超时时间也会不同,但只要在这个时间上限内没有响应就会ANR;
  • 输入事件/特定操作:输入事件是指按键、触屏等设备输入事件,特定操作是指BroadcastReceiver和Service的生命周期中的各个函数,产生ANR的上下文不同,导致ANR的原因也会不同;
3 三种情况会触发ANR 3.1 主线程对输入事件在5秒内没有处理完毕
  • Android的事件系统从2.3开始做了完全不同的实现,原先2.2中是在Java层实现的,但在2.3中整体转移到了C++层,本书基于2.3以后的版本进行说明。我们先简单了解一下产生这种ANR的整个流程。
  • 当应用程序的Window处于Active状态并且能够接收输入事件(例如按键事件、触摸事件等)时,系统底层上报的事件就会被InputDispatcher分发给这个应用程序,应用程序的主线程通过InputChannel读取输入事件并交给界面视图处理,界面视图是一个树状结构,DecorView是视图树的根,事件从树根开始一层一层向端点(例如一个Button)传递。
  • 我们通常会注册一个监听器来接收并处理事件,或者创建自定义的视图控件来处理事件。
  • InputDispatcher运行在系统进程(进程名为system_server)的一个单独的线程中,应用程序的主线程在处理事件的过程中,InputDispatcher会不断的检测处理过程是否超时,一旦超时,会通过一系列的回调通知WMS的notifyANR函数,最终会调用到AMS中mHandler对象里的SHOW_NOT_RESPONDING_MSG这个case,此时界面上就显示系统提示对话框了,同时使用logcat命令查看log(日志信息)也可以看到关于ANR的信息。
  • Window:具体指的是PhoneWindow对象,表示一个能够显示的窗口,它能够接收系统分发的各种输入事件;
  • InputDispatcher:将系统上报的输入事件分发给当前活动的窗口;
  • InputChannel:InputDispatcher和应用程序分别运行在两个不同的进程中,InputDispatcher就是通过InputChannel将事件对象传递给应用进程的。
  • 注意:产生这种ANR的前提是要有输入事件,如果用户没有触发任何输入事件,即便是主线程阻塞了,也不会产生ANR,因为InputDispatcher没有分发事件给应用程序,当然也不会检测处理超时和报告ANR了。
3.2 主线程在执行BroadcastReceiver的onReceive函数时10秒内没有执行完毕
  • BroadcastReceiver(简称BR)的onReceive函数运行在主线程中,当这个函数超过10秒钟没有返回就会触发ANR。不过对这种情况的ANR系统不会显示对话框提示,仅是输出log而已。
3.3 主线程在执行Service的各个生命周期函数时20秒内没有执行完毕
  • Service的各个生命周期函数也运行在主线程中,当这些函数超过20秒钟没有返回就会触发ANR。同样对对这种情况的ANR系统不会显示对话框提示,仅是输出log而已。
3.4 小结
  • 1.三种ANR中只有第1种会显示系统提示对话框,因为用户正在做界面交互操作,如果长时间没有任何响应,会让用户怀疑设备死机了,大多数人此时会开始乱按,甚至拔出电池重启,给用户的体验肯定是非常糟糕的。
  • 2.三种ANR发生时都会在log中输出错误信息,你会发现各个应用进程和系统进程的函数堆栈信息都输出到了一个/data/anr/traces.txt的文件中,这个文件是分析ANR原因的关键文件,同时在日志中还会看到当时的CPU使用率,这也是重要信息,可以利用它们分析ANR问题。
  • 3.这三种ANR不是孤立的,有可能会相互影响。例如一个应用程序进程中同时有一个正在显示的Activity和一个正在处理消息的BroadcastReceiver,它们都运行在这个进程的主线程中。如果BR的onReceive函数没有返回,此时用户点击屏幕,而onReceive超过5秒仍然没有返回,主线程无法处理用户输入事件,就会引起第1种ANR。如果继续超过10秒没有返回,又会引起第2种ANR。
4 traces.txt--ANR分析 4.1 获取ANR产生的trace文件
ANR产生时, 系统会生成一个traces.txt的文件放在/data/anr/下. 可以通过adb命令将其导出到本地:
$adb pull data/anr/traces.txt .

4.2 分析traces.txt
4.2.1 普通阻塞导致的ANR 强行sleep thread产生的一个ANR
获取到的tracs.txt文件一般如下:
----- pid 2976 at 2016-09-08 23:02:47 ----- 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) 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)

如上trace信息中的添加的中文注释已基本说明了trace文件该怎么分析:
  • 1.文件最上的即为最新产生的ANR的trace信息.
  • 2.前面两行表明ANR发生的进程pid, 时间, 以及进程名字(包名).
  • 3.寻找我们的代码点, 然后往前推, 看方法调用栈, 追溯到问题产生的根源.
4.2.2 CPU满负荷 race信息可能会包含这样的信息:
Process:com.anly.githubapp ... CPU usage from 3330ms to 814ms ago: 6% 178/system_server: 3.5% user + 1.4% kernel / faults: 86 minor 20 major 4.6% 2976/com.anly.githubapp: 0.7% user + 3.7% kernel /faults: 52 minor 19 major 0.9% 252/com.android.systemui: 0.9% user + 0% kernel ...100%TOTAL: 5.9% user + 4.1% kernel + 89% iowait

  • 最后一句表明了:
  • 1.当是CPU占用100%, 满负荷了.
  • 2.其中绝大数是被iowait即I/O操作占用了.
4.2.3 内存原因
  • 其实内存原因有可能会导致ANR,
    例如如果由于内存泄露, App可使用内存所剩无几, 我们点击按钮启动一个大图片作为背景的activity, 就可能会产生ANR, 这时trace信息可能是这样的:
// 以下trace信息来自网络, 用来做个示例 Cmdline: android.process.acoreDALVIK THREADS: "main"prio=5 tid=3 VMWAIT |group="main" sCount=1 dsCount=0 s=N obj=0x40026240self=0xbda8 | sysTid=1815 nice=0 sched=0/0 cgrp=unknownhandle=-1344001376 atdalvik.system.VMRuntime.trackExternalAllocation(NativeMethod) atandroid.graphics.Bitmap.nativeCreate(Native Method) atandroid.graphics.Bitmap.createBitmap(Bitmap.java:468) atandroid.view.View.buildDrawingCache(View.java:6324) atandroid.view.View.getDrawingCache(View.java:6178)...MEMINFO in pid 1360 [android.process.acore] ** native dalvik other total size: 17036 23111 N/A 40147 allocated: 16484 20675 N/A 37159 free: 296 2436 N/A 2732

可以看到free的内存已所剩无几
当然这种情况可能更多的是会产生OOM的异常...
5 ANR的避免和检测 5.1 切记不要在主线程中作耗时操作
针对三种不同的情况, 一般的处理情况如下
  • 1.主线程阻塞的
    开辟单独的子线程来处理耗时阻塞事务.
  • 2.CPU满负荷, I/O阻塞的
    I/O阻塞一般来说就是文件读写或数据库操作执行在主线程了, 也可以通过开辟子线程的方式异步执行.
  • 3.内存不够用的
    增大VM内存, 使用largeHeap属性, 排查内存泄露。
5.2 借助于一些工具来进行检测,从而更有效的避免ANR的引入。
5.2.1 StrictMode 严格模式StrictMode是Android SDK提供的一个用来检测代码中是否存在违规操作的工具类,StrictMode主要检测两大类问题。
  • 线程策略 ThreadPolicy
    1.detectCustomSlowCalls:检测自定义耗时操作
    2.detectDiskReads:检测是否存在磁盘读取操作
    3.detectDiskWrites:检测是否存在磁盘写入操作
    4.detectNetWork:检测是否存在网络操作
  • 虚拟机策略VmPolicy
    1.detectActivityLeaks:检测是否存在Activity泄露
    2.detectLeakedClosableObjects:检测是否存在未关闭的Closeable对象泄露
    3.detectLeakedSqlLiteObjects:检测是否存在Sqlite对象泄露
    4.setClassInstanceLimit:检测类实例个数是否超过限制
    Debug版本中使用
    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()//调用detectAll方法表示启动所有的检测策略 .penaltyLog() .penaltyDialog() ////打印logcat,当然也可以定位到dropbox,通过文件保存相应的log .build()); // 开启虚拟机模式 StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectAll()//调用detectAll方法表示启动所有的检测策略 .penaltyLog()//调用penaltyLog表示在Logcat中打印日志 .build()); }

也可以根据应用的具体要求只开启某些策略
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectDiskReads() .detectDiskWrites() .detectNetwork() .penaltyLog() .build()); StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectLeakedSqlLiteObjects() .detectLeakedClosableObjects() .detectActivityLeaks() .penaltyLog() .build());

5.2.2 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 * 配置监视器持续时间,此时BlockCanary将停止,并使用{@Code BlockCanary}的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. * 配置块阈值(以毫为单位),在此持续时间内的调度被视为BLOCK。 您可以根据设备的性能进行设置。 * @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. * * Because the implementation mechanism of Looper, real dump interval would be longer than * the period specified here (especially when cpu is busier). *
* * @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. * 将文件捆绑到一个zip文件中。 * @param srcfiles 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. * 如果仅仅关注包含进程名称的包,则返回null。 */ public List concernPackages() { return null; }/** * Filter stack without any in concern package, used with @{code concernPackages}. * 无需关注包的过滤器堆栈,与@ {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 provideWhiteList() { LinkedList 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. * Block拦截器,开发者可以提供自己的操作。 */ public void onBlock(Context context, BlockInfo blockInfo) {} }

6 使用子线程的方式 6.1 启Thread方式
这个其实也是Java实现多线程的方式. 有两种实现方法, 继承Thread 或 实现Runnable接口:
继承Thread
class PrimeThread extends Thread { long minPrime; PrimeThread(long minPrime) { this.minPrime = minPrime; }public void run() { // compute primes larger than minPrime . . . } }PrimeThread p = new PrimeThread(143); p.start();

实现Runnable接口
class PrimeRun implements Runnable { long minPrime; PrimeRun(long minPrime) { this.minPrime = minPrime; }public void run() { // compute primes larger than minPrime . . . } }PrimeRun p = new PrimeRun(143); new Thread(p).start();

6.2 使用AsyncTask
这个是Android特有的方式, AsyncTask顾名思义, 就是异步任务的意思.
private class DownloadFilesTask extends AsyncTask { // Do the long-running work in here // 执行在子线程 protected Long doInBackground(URL... urls) { int count = urls.length; long totalSize = 0; for (int i = 0; i < count; i++) { totalSize += Downloader.downloadFile(urls[i]); publishProgress((int) ((i / (float) count) * 100)); // Escape early if cancel() is called if (isCancelled()) break; } return totalSize; }// This is called each time you call publishProgress() // 执行在主线程 protected void onProgressUpdate(Integer... progress) { setProgressPercent(progress[0]); }// This is called when doInBackground() is finished // 执行在主线程 protected void onPostExecute(Long result) { showNotification("Downloaded " + result + " bytes"); } }// 启动方式 new DownloadFilesTask().execute(url1, url2, url3);

6.3 HandlerThread
Android中结合Handler和Thread的一种方式. 前面有云, 默认情况下Handler的handleMessage是执行在主线程的, 但是如果我给这个Handler传入了子线程的looper, handleMessage就会执行在这个子线程中的. HandlerThread正是这样的一个结合体:
// 启动一个名为new_thread的子线程 HandlerThread thread = new HandlerThread("new_thread"); thread.start(); // 取new_thread赋值给ServiceHandler private ServiceHandler mServiceHandler; mServiceLooper = thread.getLooper(); mServiceHandler = new ServiceHandler(mServiceLooper); private final class ServiceHandler extends Handler { public ServiceHandler(Looper looper) { super(looper); }@Override public void handleMessage(Message msg) { // 此时handleMessage是运行在new_thread这个子线程中了. } }

6.4 IntentService
【Android性能优化(五)--更快--稳定--响应速度ANR】Service是运行在主线程的, 然而IntentService是运行在子线程的.
实际上IntentService就是实现了一个HandlerThread + ServiceHandler的模式.
6.5 Loader
Android 3.0引入的数据加载器, 可以在Activity/Fragment中使用. 支持异步加载数据, 并可监控数据源在数据发生变化时传递新结果. 常用的有CursorLoader, 用来加载数据库数据.
// 创建一个Loader public Loader onCreateLoader(int id, Bundle args) { // This is called when a new Loader needs to be created.This // sample only has one Loader, so we don't care about the ID. // First, pick the base URI to use depending on whether we are // currently filtering. Uri baseUri; if (mCurFilter != null) { baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, Uri.encode(mCurFilter)); } else { baseUri = Contacts.CONTENT_URI; }// Now create and return a CursorLoader that will take care of // creating a Cursor for the data being displayed. String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND (" + Contacts.HAS_PHONE_NUMBER + "=1) AND (" + Contacts.DISPLAY_NAME + " != '' ))"; return new CursorLoader(getActivity(), baseUri, CONTACTS_SUMMARY_PROJECTION, select, null, Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC"); }// 加载完成 public void onLoadFinished(Loader loader, Cursor data) { // Swap the new cursor in.(The framework will take care of closing the // old cursor once we return.) mAdapter.swapCursor(data); }

6.6 特别注意
使用Thread和HandlerThread时, 为了使效果更好, 建议设置Thread的优先级偏低一点:
Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);

因为如果没有做任何优先级设置的话, 你创建的Thread默认和UI Thread是具有同样的优先级的, 你懂的. 同样的优先级的Thread, CPU调度上还是可能会阻塞掉你的UI Thread, 导致ANR的.
参考 https://www.jianshu.com/p/c8c15608c68e
https://www.jianshu.com/p/6d855e984b99
https://blog.csdn.net/sinat_36668731/article/details/74924432

    推荐阅读