在世界上,编写可以赢得每一个性能测试的代码是有可能的,但是仍然感觉在某些显著的时间段内缓慢、挂起或者冻住,或者花费太长的时间来处理输入事件。可能发生的应用响应上最糟糕的事情是“应用未响应”(ANR)对话框。
在Android中,系统通过显示一个说明您的应用已经停止响应的对话框来防止应用在一段时间内响应不足,就像图1中的对话框一样。在这个点上,您的应用已经相当一段时间没有响应了,所以系统给用户提供了一个选项来终止该应用。将响应设计到您的应用,来让系统从来不给用户显示ANR对话框是极其重要的。
图1.展示给用户的ANR对话框
本文描述了Android系统如何决定是否应用是不响应的并且提供指导来确保您的应用保持响应。
是什么触发了ANR?
一般来说,如果应用不能响应用户输入事件,系统会显示ANR。例如,如果应用在UI线程上的某些I/O操作(频繁地访问网络)上阻塞了,以至于系统无法处理进来的用户输入事件。或者可能应用在UI线程上花费了太多时间来构建一个复杂的内存结构或者在游戏中计算下一个移动。确保这些计算高效总是很重要的,但是即使是最高效的代码仍然会花费时间来运行。
在任何您的应用可能执行长时间操作的场景下,您都不应该在UI线程中执行这项工作,而应该创建一个工作线程来处理大部分的工作。这让UI线程(它驱动用户接口时间循环)保持运行并且防止系统断定您的代码已经冻住了。因为这样的线程通常是在class级别上完成的,所以您可以把响应看成是一个class问题。(将这和基本的代码性能进行比较,代码性能问题是一个方法级别的概念。)
在Android中,应用响应被Activity Manager和Window Manager 系统服务监视,当检测到有以下条件之一时,Android将会为特定的应用显示ANR对话框:
- 5秒内对输入事件(比如键被按下或者屏幕触摸事件)没有响应。
- BroadcastReceiver在10秒内没有完成执行。
如何避免ANR
Android应用通常全部在一个单一的线程中运行(默认为“UI线程”或者“主线程”)。这意味着您的应用在UI线程中正在执行的需要花费很长时间来完成的任何任务,都可能触发ANR对话框,因为您的应用没有给自己机会来处理输入事件或者意图广播。
因此,任何在UI线程中运行的方法应该尽可能做少量的工作。尤其是,Activity应该尽可能少地在如onCreate()和onResume()这样的关键生命周期方法中设置。潜在的诸如网络操作或者数据库操作这样的长时间运行的操作,或者诸如重新设置bitmap大小等这样昂贵的计算,应该在工作线程中来执行(或者在数据操作的情况下,通过异步请求)。
为更长时间的操作创建工作线程最有效的方式是使用AsyncTask类。简单地继承AsyncTask并且实现doInBackground()方法来执行工作。为了将进度改变发送给用户,您可以调用publishProgress(),它调用了onProgressUpdate()回调方法。从onProgressUpdate()方法(它在UI线程中运行)的实现,您可以通知用户。例如:
1 private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> { 2 // Do the long-running work in here 3 protected Long doInBackground(URL... urls) { 4 int count = urls.length; 5 long totalSize = 0; 6 for (int i = 0; i < count; i++) { 7 totalSize += Downloader.downloadFile(urls[i]); 8 publishProgress((int) ((i / (float) count) * 100)); 9 // Escape early if cancel() is called 10 if (isCancelled()) break; 11 } 12 return totalSize; 13 } 14 15 // This is called each time you call publishProgress() 16 protected void onProgressUpdate(Integer... progress) { 17 setProgressPercent(progress[0]); 18 } 19 20 // This is called when doInBackground() is finished 21 protected void onPostExecute(Long result) { 22 showNotification("Downloaded " + result + " bytes"); 23 } 24 }
要执行该工作线程,简单地创建一个实例并且调用excute()方法:
1 new DownloadFilesTask().execute(url1, url2, url3);
您可能希望创建您自己的Thread或者HandlerThread类,虽然这比AsyncTask更加复杂。如果您这样做,您应该通过调用Process.setThreadPriority()方法并且传入THREAD_PRIORITY_BACKGROUND值来给“后台”优先级设置线程优先级。如果您没有通过这个方法来将该线程设置为更低的优先级,那么该线程可能仍然会拉低您应用的速度,因为它默认情况下会以和UI线程相同的优先级来运行。
如果您实现Thread或者HandlerThread,请确保当正在等待工作线程完成时,UI线程不会阻塞——不要调用Thread.wait()或者Thread.sleep()。当等待工作线程完成时,主线程不应该阻塞,而应该为其它线程提供一个Handler,当工作线程完成时将其传回主线程。通过这种方式设计应用将允许应用的UI线程保持对输入事件的响应,并且这样避免了5秒的输入事件超时所引起的ANR对话框。
BroadcastReceiver执行时间的特别限制强调了广播接收器应该做什么:小的,离散的后台工作量,比如保存设置或者注册通知。所以,当其它方法在UI线程中被调用时,在广播接收器中应用应该避免潜在的长时间运行操作或者计算。但是,如果潜在的长时间运行的action需要处理来响应intent广播,您的应用不应该通过工作线程处理密集的任务,而应该通过启动IntentService。
当BroadcastReceiver对象执行太频繁时,另外一个常见的BroadcastReceiver对象问题会发生。频繁的后台执行会降低其它应用可用内存的数量。更多关于如何有效地让BroadcastReceiver对象有效/失效,请查阅【按要求操作广播接收器】
★ 提示:您可以使用StrictMode来协助找到潜在的长时间运行操作,比如您可能无意间在主线程中执行的网络或者数据库操作。
加强响应
一般来说,100到200毫秒时阈值,超过这个阈值用户将察觉到应用缓慢。所以,在为了避免ANR你应该做的之外,这里有一些附加的提示,让您的应用看起来对用户是响应的:
- 如果您的应用正在后台处理工作来响应用户输入,显示它的进度(比如在UI中使用ProgressBar)。
- 特别是对于游戏,在工作线程中计算移动。
- 如果您的应用有一个耗时的初始化设置阶段,考虑尽快显示一个初始化屏幕或者展示主视图,表明正在加载并且异步地填充信息。在其中任意一种情况,您应该指明取得的进展,以免用户以为应用冻住了。
- 使用性能功能如【Systrace】和【TraceView】来确定应用响应的瓶颈。