Android性能优化系列---避免ANR
有这样一种情况:即使你写的代码通过世界上的每一个性能测试,但程序在特定的操作和重要的阶段仍然让人感觉运行缓慢,或者需要花很长时间处理输入。这种发生在你的app里的糟糕的响应是“Application Not Responding(ANR)”对话框。
Figure 1. An ANR dialog displayed to the user.
在Android里,当app一段时间无响应时,系统会弹出一个对话框,告诉你,你的app长时间没响应。如图一。
出现该对话框,表明你的app已在合理的一段时间内已没有响应用户操作,因此,系统弹出该对话框窗口,给用户选择是否退出应用。设计高响应的应用以便于系统从不展示ANR对话框给用户是很关键的。
这篇文档描述了android系统如何判断app是否是ANR的,也提供了相关如何避免ANR的建议。
What Triggers ANR?
一般地,如果app不能响应用户的输入,系统将展示ANR。例如,应用在UI线程里阻塞在I/O操作(频繁的网络访问)导致系统不能处理来自用户的输入事件。又或者在在游戏app里在UI线程上进行耗时的位置计算(从一个位置移动到另一个位置)。
在你的app里任何潜在的耗时操作,你都不应该在UI线程里执行。而是产生一个非UI线程,在U该非UI线程里进行耗时操作。这确保了你的UI线程(能循环驱动用户界面事件)运行,并且避免了系统认为你的应用已被阻塞。因为如此的线程实现在一个类级别上,你可以认为app响应的问题是类级别上的问题(相比于基本的代码性能问题,代码性能问题可以认为是方法级别上的问题)。
在Android里,Activity Manager 和 Window Manager 系统服务会监控app是否有ANR发生并当监测到有ANR发生时展示ANR对话框。下面两种情境将触发ANR:
1. 5秒内不能响应用户输入(例如:键盘按下或者屏幕触摸事件)
2. BroadcastReciever在10秒内不能执行完成
How to Avoid ANRs
Android应用一般情况下默认运行在一个单一线程上(称为UI线程或者主线程)。这意味着任何在你的UI线程里做的耗时操作都能触发ANR。因为在这种情况下,你的app将没有机会处理输入事件和广播意图。
因此,任何在UI线程里运行的方法都尽量不要做耗时操作。特别地,在activity的关键生命周期方法里例如像onCreate()和onResume()不要进行耗时操作。潜在的耗时操作例如网络和数据库操作、复杂的耗时计算(例如:重新计算bitmap的大小)都应该放在子线程里。
产生子线程的最有效的方式用AsyncTask类。简单地继承AsyncTask,然后实现doInBackground()方法,在该方法里进行耗时操作。为了提交进度改变给用户,你能调用publishProgress().该方法会回调onProgressUpdate()方法。
由于onProgressUpdate()运行在UI线程。你能通知用户更新结果。例如:
private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
// 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");
}
}
为了执行子线程,简单地产生AnsyncTask的实例,调用execute()方法。
new DownloadFilesTask().execute(url1, url2, url3);
有时你可能需要自己产生Thread或者HandlerThread类,虽然这比起AnsyncTask更复杂点。如果你这样做,你需要设置线程优先级为“background”优先级。通过Process.setThreadPriority()来设置线程优先级,传递参数THREAD_PRIORITY_BACKGROUND即可。如果你不设置你的线程优先级为THREAD_PRIORITY _BACKGROUND,该线程能然可能放慢你的应用。因为它默认和你UI线程一个优先级。
如果,你实现Thead或者HandlerThread,确保你的UI线程在等待子线程运行结果完成时不要被阻塞--即在主线程里不要调用Thread.wait()或者Thread.sleep().为了避免阻塞你的主线程,你的主线程应该给子线程提供一个Handler用于通知UI线程操作执行完成。按这种方式设计你的应用将能让你的UI线程能响应用户输入并且避免5秒输入响应超时而弹出ANR。
Android专门针对BroadcastReceiver的执行时间限制主要是为了强调广播接收者主要用于做如下事情:小的、零碎离散的后台工作例如:保存设置或者注册Notification等。像其他在UI线程里调用的方法一样,广播接收者里也应该避免潜在的长时间操作或者计算。但是不同于把耗时操作放在子线程里,为了响应一个耗时操作的广播意图,你的应用应该开启一个intentService。
Tip:你能用StrictMode帮助你发现不小心放在UI线程里的潜在的耗时操作(例如网络或者数据库操作)。
Reinforce Responsiveness
一般地,100-200ms是用户感知缓慢的时间分割点。如此,下面是一些为了避免ANR你应该遵循的一些额外的建议:
1.为了响应用户操作你的app将一些操作放在了后台,你应该给用户显示进度条(例如在你的UI上放一个ProgressBar)。
2.特别地开发游戏应用时,位置计算(例如移动)放在后台进程。
3.如果你的应用有一个长时间初始化阶段,可以考虑显示一个渐进缓冲的屏幕或者尽可能快的渲染主界面,已表明加载正在进行,然后异步填充数据。不管如何,你都应该表明你的app正在加载,以免让用户
觉得你的app好像死掉了一样。
4.使用如Systrace和Traceview这样的性能工具分析你的app响应的瓶颈。