Android Dev Summit, October 23-24: two days of technical content, directly from the Android team. Sign-up for livestream updates.

使应用能迅速响应

图 1. 向用户显示的 ANR 对话框。

即使编写出能够通过世界上的所有性能测试的代码,却仍有可能让用户感觉迟钝、发生挂起或卡住很长时间,或者处理输入的时间过长。在应用响应能力方面可能发生的最糟糕的情况就是“应用无响应”(ANR) 对话框。

在 Android 中,系统会通过显示说明应用已停止响应的对话框(例如图 1 中的对话框)来防范在一段时间内响应不足的应用。此时,应用已在相当长的一段时间内没有响应,因此系统会向用户提供退出此应用的选项。在设计应用时应充分考虑响应能力,让系统永远不会向用户显示 ANR 对话框,这一点至关重要。

本文档介绍了 Android 系统如何确定应用是否没有作出响应,并提供了有关确保应用保持迅速响应的指南。

什么会触发 ANR?

通常,当应用无法响应用户输入时,系统即会显示 ANR。例如,如果应用在界面线程中屏蔽了某些 I/O 操作(通常是网络访问),导致系统无法处理传入的用户输入事件。或者,应用在界面线程中花费太多时间构建复杂的内存结构或计算游戏的下一个走法。确保高效的计算始终至关重要,但即使最高效的代码仍然需要时间来运行。

在您的应用面临任何可能需要执行冗长的操作的情况下,您不应在界面线程中执行这些操作,而是应该创建工作线程并在其中执行大部分操作。这样即可让界面线程(用于驱动界面事件循环)保持运行,并阻止系统断定您的代码已卡住。由于这种线程通常是在类级别完成的,因此您可以将响应能力视为一种类问题。(可将其与基本代码性能进行比较,后者是方法级问题。

在 Android 中,应用响应性由 Activity 管理器和窗口管理器系统服务监控。当 Android 检测到以下某一项条件时,便会针对特定应用显示 ANR 对话框:

  • 在 5 秒内对输入事件(例如按键或屏幕轻触事件)没有响应。
  • BroadcastReceiver 在 10 秒后尚未执行完毕。

如何避免 ANR

Android 应用通常完全在单个线程中运行(默认为“界面线程”或“主线程”)。这意味着应用在界面线程中执行的任何需要很长时间才能完成的操作都可能会触发 ANR 对话框,因为应用没有给自己处理输入事件或 intent 广播的机会。

因此,在界面线程中运行的所有方法都应该尽可能减少在此线程中的操作。具体而言,在 onCreate()onResume() 等关键生命周期方法中,Activity 应尽量减少进行设置所需的操作。可能会长时间运行的操作(例如网络或数据库操作)或计算成本高昂的计算(例如调整位图大小)应在工作线程中完成(如果是数据库操作,则应通过异步请求完成)。

为耗时较长的操作创建工作线程的最有效方法是使用 AsyncTask 类。只需扩展 AsyncTask 并实现 doInBackground() 方法即可执行相应操作。要向用户显示进度变化,您可以调用 publishProgress(),它会调用 onProgressUpdate() 回调方法。通过 onProgressUpdate()(在界面线程中运行)的实现,您可以向用户发送通知。例如:

Kotlin

    private class DownloadFilesTask : AsyncTask<URL, Int, Long>() {

        // Do the long-running work in here
        override fun doInBackground(vararg urls: URL): Long? {
            val count: Float = urls.size.toFloat()
            var totalSize: Long = 0
            urls.forEachIndexed { index, url ->
                totalSize += Downloader.downloadFile(url)
                publishProgress((index / count * 100).toInt())
                // Escape early if cancel() is called
                if (isCancelled) return totalSize
            }
            return totalSize
        }

        // This is called each time you call publishProgress()
        override fun onProgressUpdate(vararg progress: Int?) {
            setProgressPercent(progress.firstOrNull() ?: 0)
        }

        // This is called when doInBackground() is finished
        override fun onPostExecute(result: Long?) {
            showNotification("Downloaded $result bytes")
        }
    }
    

Java

    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");
        }
    }
    

要执行此工作线程,只需创建一个实例并调用 execute() 即可:

Kotlin

    DownloadFilesTask().execute(url1, url2, url3)
    

Java

    new DownloadFilesTask().execute(url1, url2, url3);
    

虽然比 AsyncTask 复杂,但您可能想要自己创建 ThreadHandlerThread 类。如果是这样,您应该调用 Process.setThreadPriority() 并传递 THREAD_PRIORITY_BACKGROUND,从而将线程优先级设为“后台”优先。如果您不通过这种方式将线程设为较低的优先级,则此线程仍可能会让应用变慢,因为默认情况下,此线程会按照与界面线程相同的优先级操作。

如果您实现了 ThreadHandlerThread,请确保在等待工作线程完成操作期间,界面线程不会阻塞;请勿调用 Thread.wait()Thread.sleep()。非但不应在等待工作线程完成操作期间阻塞,主线程在完成操作时还应提供 Handler 以供其他线程向回发送。以这种方式设计应用,即可让应用的界面线程对输入保持响应,从而避免因 5 秒的输入事件超时而导致系统显示 ANR 对话框。

BroadcastReceiver 执行时间的特定限制可以强调广播接收器的预期用途:在后台间续地执行少量操作,例如保存设置或注册 Notification。因此,与在界面线程中调用的其他方法一样,应用应避免在广播接收器中执行可能会长时间运行的操作或计算。但如果需要执行可能需要长时间运行的操作以响应 intent 广播,则应用应启动 IntentService,而不是通过工作线程执行密集型操作。

BroadcastReceiver 对象的另一个常见问题会在其频繁执行时出现。频繁的后台执行可能会减少其他应用可用的内存量。要详细了解如何高效地启用和停用 BroadcastReceiver 对象,请参阅随需操纵广播接收器

提示:您可以使用 StrictMode 查找可能会长时间运行的操作,例如您可能会无意中在主线程中执行的网络或数据库操作。

加强响应能力

通常,100 到 200 毫秒是一个阈值,一旦超出此阈值,用户便能够感受到应用速度缓慢。因此,除了采取措施以避免显示 ANR 之外,还有一些提示可以让用户感觉您的应用响应迅速:

  • 如果应用在后台执行操作以响应用户输入,则显示正在进行该操作(例如在界面中使用 ProgressBar)。
  • 特别是游戏,在工作线程中计算走法。
  • 如果应用具有耗时较长的初始设置阶段,考虑显示启动画面或尽快呈现主视图,表明正在加载,并异步填充信息。在任何一种情况下,您都应以某种方式表明操作正在进行,以免用户认为应用已卡住。
  • 使用 SystraceTraceview 等性能工具确定应用响应性方面的瓶颈。