天天看点

Android - 《Keeping Your App Responsive》

翻译自Android 官方文档。

保持你的App有响应

在这个世界上,写出能跑赢所有性能测试的代码是可能,但是在重大的周期里,仍然让人感觉缓慢、卡顿或者卡住,或者花太多时间来处理输入。这种能发生在你的App的响应能力的最坏的事情是一个“ANR”Dialog。

在Android系统里,系统守卫依靠在一个周期里确认App没有响应能力,从而显示一个对话框来告诉你,你的App已经停止响应,就像图1显示的那个Dialog那样。

图1:一个显示给用户的ANR窗口

Android - 《Keeping Your App Responsive》

在这时,你的App已经在一个足够的时间周期里不响应,所以系统提供给用户一个退出App的选项。设计响应性好的App,保证系统不现实ANR窗口给用户,这是非常关键的。

这篇文档描述Android如果判断一个App是否是无响应,并且提供一个指导,用来帮助你保证你的App一直有响应。

什么会触发ANR?

通常,如果一个App不响应用户输入,那么Android系统就会显示一个ANR窗口。例如,一个App的UI 线程阻塞在IO操作上,那么系统就不能处理到来的输入事件。又或许App在UI线程里花费了太多时间来构建一个精细的内存结构,或者在游戏中计算下一步的移动。确认这些计算是有效率的总是很重要的,但即使是最有效率的代码,仍然要花时间来运行。

在一些情景下,在你执行一个尽可能长时间的操作时,你不应该在UI线程做这个工作,而是用创建一个工作线程来代替,这个工作线程能做更多的事情。这能保证UI线程(驱动UI时间循环)能一直运行,并且能阻止系统认定你的代码已经卡死。因为这些线程通常是一个完整的类,所以你能将响应性想成一个类问题(比较这个代码和基础代码的效率,是方法层级关心的事情)。

在Android系统里,app的响应性是由Activity Manager和 Window Manager 来监测的。Android将会为检测到以下条件中的一个的特定App显示一个ANR窗口:

·不响应输入事件5秒以内。

·一个广播接收器不能完成处理10秒以内。

怎样避免ANR?

Android App通常完全运行在单个线程上,默认是UI线程或者主线程。这意味着你的App在UI线程内做的需要花很长时间完成的任何事情都会触发ANR窗口,因为你的App不会给自己一个机会来处理输入事件或广播Intent。

因此,任何运行在UI线程上的方法应该尽量的在线程上做一些微小的工作。特别是,Activity应该尽可能小的在关键函数:OnCreate、OnResume里做设置。可能长时间的操作,比如网络连接,数据库操作,或者计算成本昂贵的计算,比如改变位图大小应该放在工作者线程内(或者在操作数据库的情况下,通过一个异步请求来操作)。

为长时间操作创建工作者线程的最有效的方法是使用AsyncTask 类。简单的扩展自AsyncTask类,并且实现doInBackground()

方法来执行工作。为了发送进度变化给用户,你能调用

publishProgress()方法,系统调用onProgressUpdate()

回调函数。在你的

onProgressUpdate()

方法(这个方法运行在UI线程中)实现,你能通知用户。例如:

private classDownloadFilesTask 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()方法:      
new DownloadFilesTask().execute(url1, url2, url3);      
你应该会想要创建自己的Thread或者HandlerThread类,虽然它们比AsyncTask更复杂。如果你那么做了,你应该设置那个线程的优先级为“background”,通过调用Process.setThreadPriority()         方法和传递                THREAD_PRIORITY_BACKGROUND标识。如果你不想要走设置低线程优先级这条路,那么你的线程就会一直减缓你的App的运行速度,因为默认你的线程和UI线程在同一优先级。      
如果你实现了Thread和HandlerThread,请确认,当UI线程等待工作线程完成时,不会被阻塞,不要在UI线程里调用Thread.wait() , Thread.sleep()。替代UI线程阻塞等待工作者线程完成的方法是,你的主线程应该提供一个Handler给其他线程,其他线程可以用Handler来回传工作者线程完成消息。用这种方法来设计你的App,将会允许你的UI线程保留对输入事件的响应性,并且因此避免因输入事件5秒超时引起的ANR对话框。      
广播接收器的特殊规则着重于广播接收器会这么做:小,分散大量的后台工作,比如保存一个设置,或者注册一个Notification。所以,就像调用UI线程的其他方法那样, App应该避免在一个广播接收器进行潜在的长时间操作或计算。而是通过工作线程来替代这样的任务。如果一个潜在的长时间的Action需要被用来响应一个Intent 广播,你的应用应该开启一个IntentService 。      

小技巧:你能通过

StrictMode

模式来帮助你找到潜在的长时间操作,比如网络连接、数据库操作这种你可能很意外的在你的主线程里做的动作。

强化响应能力

通常,在一个App上,100到200毫秒是一个极限,超过这个极限用户会感受到缓慢。同样地,这里有一些额外的小技巧,让你应该做什么来避免ANR,并且让你的App看起来像是响应用户了。

·如果你的App正在后台做一些工作,并且还响应用户输入,显示一个进度条是有用的(比如在你的UI里添加一个进度条控件)。

·特别对于游戏,将计算移动放到工作线程里。

·如果你的App有一个很耗时的初始化设置阶段,考虑显示一个启动画页,或者尽可能快的渲染一个主View,表明加载正在进行并且正在异步填充信息。在其他的Case下,你也总得指示出正在进行中的样子,以免让用户感觉App已经卡死。

StrictMode说明

  StrictMode是一个开发工具,用来检测一些你可能意外做的事情,并且将你的注意力带到它们身上,那样你就可以修理它们了。

StrictMode最常用来在你的App的主线程里捕获意外的磁盘或网络访问动作,这个主线程就是UI动作被接收的地方,动画播放的地方。保持磁盘和网络操作远离主线程,将使App更顺畅,更有响应性。通过保持你的App的主线程的响应,你就能阻止ANR窗口显示给用户。、

注意:虽然Android设备的磁盘经常在闪存上,很多设备运行一个并发性有限的文件系统在内存上。下面这个是个经常的情况,几乎所有磁盘访问都很快,但当某一个IO在后台的其他进程里发生意外,就会出奇的慢。如果可能,最好的假设就是这样的事情并不快。

举例代码,从早期启用这样的功能,在你的Application、Activity,或其他App的组件的OnCreate函数:

 public void onCreate() {
     if (DEVELOPER_MODE) {
         StrictMode.setThreadPolicy(new          StrictMode.ThreadPolicy.Builder                ()
                 .detectDiskReads()
                 .detectDiskWrites()
                 .detectNetwork()   // or .detectAll() for all detectable problems
                 .penaltyLog()
                 .build());
         StrictMode.setVmPolicy(new          StrictMode.VmPolicy.Builder                ()
                 .detectLeakedSqlLiteObjects()
                 .detectLeakedClosableObjects()
                 .penaltyLog()
                 .penaltyDeath()
                 .build());
     }
     super.onCreate();
 }
      

你能决定,当一个违规被检测到能发生什么。例如,使用

penaltyLog()函数,你能观察到 adb logcat输出,在你用你的App来看发生的违规时。

如果你发现你感觉有问题的违规,这里有各种各样的工具来帮你解析它们:threads,

Handler

,

AsyncTask

,

IntentService

,等等。但是不要感觉强迫要修改StrictMode找到的每个事物。尤其是,很多磁盘访问的Case经常是必要的,在正常的Activity生命周期里。用StrictMode来找你意外做的事情。把网络请求放在UI线程几乎总是有问题的。

注意:StrictMode不是一个安全的机制,并且不保证找到所有磁盘和网络访问。在做binder调用时,在它传递它的状态通过进程边界时,基本上它一直就是最好的机制。尤其是,从JNI开始的磁盘和网络调用不是必定会触发StrictMode模式。未来的Android版本有可能会捕获更多的操作,所以你就不再离开StrictMode模式,它会被启动在分布在Google play的App里。