本节书摘来自异步社区《Android UI基础教程》一书中的第2章,第2.6节 防止应用程序无响应(ANR),作者 【美】Jason Ostrander,更多章节内容可以访问云栖社区“异步社区”公众号查看
2.6 防止应用程序无响应(ANR)
Android UI基础教程
一个Android应用程序运行在它自身的进程之上,是与其他应用无关的沙盒应用。应用被单个线程操控:主线程,或者叫做UI线程。要让应用能够快速响应,Android限制了函数调用的时间。如果函数超过了它的时间限制,则会出现一个应用程序没有响应(ANR)的对话框,提示用户选择继续等待或者强制关闭应用。你应该不惜任何代价避免ANR的出现。当你在主线程上执行长时间的操作时ANR会出现,例子包括网络I/O、磁盘I/O、数据库查询以及密集的CPU运算。
提示: 任何时候你收到的Android系统的回调函数都是由主线程完成。这包括活动和服务回调函数、时间处理程序、按键监听程序等。记住不要在这些回调函数中执行任何阻塞操作。如果你确实需要执行这样的操作,开始一个后台线程或者使用AsyncTask来处 理它。
2.6.1 StrictMode
Android 2.3推出了一个新的开发者工具,名字叫做StrictMode。这个工具会检测发生在主线程上的磁盘或者网络操作并且会采取行动来警告开发者。它提供了许多方法来警告开发者,从简单的日志记录到应用程序崩溃等。
StrictMode并不能保证能够找到发生在主线程上的所有的磁盘和网络I/O。尤其是,任何通过Java本地接口(JNI)产生的访问都不会被检测到。需要清醒认识到虽然StrictMode很有用,但是它并不足以创建及时响应的程序。
声明StrictMode**
下面是一个简单的检测所有类型的网络和磁盘I/O的StrictMode。
`StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()`
` .detectAll()`
` .penaltyLog()`
` .penaltyDialog()`
` .build());`
这将会检测正在执行的线程上的所有网络和磁盘I/O,并且会采取两种行动:打印警告到日志中并对用户显示警告对话框。这个例子设置只能在当前线程设置警告。要检测任意线程上的冲突,使用setVmPolicy调用:
`StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()`
` .detectAll()`
` .penaltyLog()`
` .penaltyDeath()`
` .build());`
建议在所有创建的工程中都启用StrictMode。在开发初期捕获这些例子更好,不会发生大的架构改变。
禁用StrictMode
尽管StrictMode对于创建快速响应的应用非常有帮助,在市场上发布应用时应当禁用它。否则,用户可能会遇到违反政策的对话框或者甚至会经历应用崩溃。处理这个的一个简单方法就是只在调试模式时启用StrictMode(有一个调试键作为签名)。要检测一个应用是否运行在调试模式,检测ApplicationInfo标志就行。下面的代码片段会检测应用是否使用了调试签名:
`public static boolean isDebugMode(Context context) {`
` PackageManager pm = context.getPackageManager();`
` try {`
` ApplicationInfo info = pm.getApplicationInfo`
` → (context.getPackageName(), 0);`
` return (info.flags & ApplicationInfo.FLAG`_`DEBUGGABLE) != 0;`
` } catch (NameNotFoundException e) {`
` }`
` return true;`
`}`
2.6.2 后台任务
你将会遇到一种常见情况是需要执行不能在UI线程上运行的长时间操作——比如下载RSS订阅、写文件或者运行定时器等。运行这些任务需要许多时间,这将会阻止UI线程的更新。你可以采用一些策略来处理这种情况。通常情况下,你会创建一个能够执行该任务的新线程,当完成任务之后再更新UI或者应用程序的状态。下面是实现这一行为的几个策略。
Handler和消息队列
防止阻塞UI线程的一个好方法是让任务线程在后台运行。然而,当任务完成时,你常常需要更新UI。对于UI的更新只能由UI线程执行,否则将会产生异常。可以使用Handler类来做到这一点。Handler允许你发送在一段时间之后再由其处理的消息。这些消息可以立即进行处理,也可以计划好在将来的某段时间进行处理。Handler在handleMessage方法中处理消息。
默认情况下,一个Handler实例是绑定在创建它的线程上的(通常是主线程)。绑定Handler到UI线程上为异步更新UI提供了一个方便的方法。然而,你同样可以选择提供一个可选的Looper实例,让Handler运行在单独的线程上。循环类用于为一个线程运行消息循环。通过使用循环类,你可以发送消息并让它们运行于任何线程实例上。
注意: Looper类创建和管理一个包含单个线程的所有消息的MessageQueue对象。UI线程已经为你创建了一个消息队列和循环类。
Handler具有发送在一段时间之后再进行处理的消息的能力,这一点使得它非常适合实现基于时间的行为。下面是一个TimeTracker应用将会用到的跟踪时间间隔的简单Handler:
`private Handler mHandler = new Handler() {`
` public void handleMessage(Message msg) {`
` long current = System.currentTimeMillis();`
` mTime += current - mStart;`
` mStart = current;`
` TextView counter = (TextView) TimeTrackerActivity.this.`
` → findViewById(R.id.counter);`
` counter.setText(DateUtils.formatElapsedTime(mTime/1000));`
` mHandler.sendEmptyMessageDelayed(0, 250);`
` };`
`};`
这段代码更新了这个Activity类的两个变量,该类被用于保存当前时间。之后它更新了UI(记住Handler回调函数将会默认在UI线程上执行,除非你明确地给出它会运行于另一个线程上)。最后,它指定另一条消息在100毫秒之后执行。SendEmptyMessage方法也传入了一个用于区分的整数参数。由于这里只有一个单条消息,所以第一个参数被设置为0。使用Handler的消息处理的API,你可以为使用定时器创建方便的方法。
1.在TimeTrackerActivity类中创建一个startTimer方法。这个方法将会记录当前的系统时间并且发送一条消息给Handler,开启一个定时器。为了阻止开启定时器两次,在发送下一条消息时移除任何存在的消息。
`private void startTimer() {`
` `` ``mStart = System.currentTimeMillis();`
` `` ``mHandler.removeMessages(0);`
` `` ``mHandler.sendEmptyMessage(0);`
` }`
2.stopTimer方法从Handler的消息队列中移除所有消息。
`private void stopTimer() {`
` mHandler.removeMessages(0);`
`}`
3.resetTimer方法将会先调用stopTimer,随后往列表适配器中添加当前时间,并在列表中展示。
`private void resetTimer() {`
` stopTimer();`
` if (mTimeListAdapter != null)`
` `` ``mTimeListAdapter.add(mTime/1000);`
` mTime = 0;`
`}`
最终,你将会需要知道定时器是否已经被停止。
4.创建一个检测消息队列中消息的方法。
`private boolean isTimerRunning() {`
` ``return mHandler.hasMessages(0);`
`}`
你现在明白完成定时器的所有逻辑了。
Activity.runOnUIThread
仅仅为了从一个后台线程中更新UI而创建一个Handler的情况很常见。Android提供了Activity.runOnUIThread方法,为这种情况提供了一条捷径。这个方法采用一个runnable并将其发送给UI线程的处理消息的Handler。主线程会在空闲时运行在runnable中的代码。
AsyncTask
开始一个后台线程执行一些任务并在任务结束之后更新UI,这种情况很常见。你可以只使用一个线程来执行这些任务并使用runOnUIThread方法来将数据展示给用户。但是如果你需要展示进程将会发生什么呢?在这些情况下,向UI消息处理的Handler发送runnable是大材小用了。Android中有一个叫做AsyncTask的类,这个类是针对这种情况特别设计的。
你可以扩展AsyncTask类来创建一个简单的线程,这个线程可以被用来执行后台任务以及在UI线程上打印结果。它包括了在任务被完成前后的UI更新,还有顺便进行的进程更新。下面是一个AsyncTask的基本形式:
`private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {`
` protected Long doInBackground(URL... urls) {`
` while (true) {`
` // Do some work`
` publishProgress((int) ((i / (float) count) `*` 100));`
` }`
` return result;`
` }`
` protected void onProgressUpdate(Integer... progress) {`
` setProgressPercent(progress[0]);`
` }`
` protected void onPostExecute(Long result) {`
` showDialog("Result is " + result);`
` }`
`}`
传入任务的3个参数分别用来指定执行时的参数类型、设置进程的参数类型以及当后台任务完成时的返回结果类型。你可以在任务运行之前使用onPreExecute来更新UI,使用onProgressUpdate来更新UI进程的指示器,使用onPostExecute在任务结束时更新UI。这些方法都运行于主UI线程之上,所以更新视图并没有风险。所有的代码都运行于doInBackground方法中,你可以将其看作仅仅是一个线程的运行方法。
AsyncTask对于需要更新一个UI组件的快速一次性任务很有用(例如从Twitter下载新的帖子并把这些帖子加载到时间线中去)。