【官网】性能篇(二)通过线程提高性能

前言本文用于介绍如何通过正确使用线程来提升应用性能(Betterperformancethroughthreading)。中国版官网原文地址为:https://developer.android.google.cn/topic/performance/threads。路径为:AndroidDevelopers>Docs>指南>Bestpracties>Betterperfo...

前言

本文用于介绍如何通过正确使用线程来提升应用性能(Better performance through threading)。

中国版官网原文地址为:https://developer.android.google.cn/topic/performance/threads。

路径为:Android Developers > Docs > 指南 > Best practies > Better performance through threading

正文

在Android中熟练使用线程能够帮助您提升您应用的性能。本页将会讨论用线程工作的几个方面:使用UI线程或主线程工作;应用的生命周期和线程优先级之间的关系,以及平台提供的用于管理线程复杂度的方法。本页将描述这其中任何一个方面中可能的陷阱和避免它们的策略。

主线程

当用户启动您的应用时,Android会创建一个携带执行线程的Linux进程。这个主线程,也作为UI线程被熟知,对屏幕上所发生的一切负责。理解它是如何工作的可以帮助您设计应用,使其使用主线程以获得最佳性能。

内幕

主线程有一个非常简单的设计:它唯一的工作就是获取并执行来自于线程安全工作队列的工作块,直到它的应用终止。框架从不同的地方生成了这些工作块中的一部分。这些地方包括与生命周期信息相关联的回调,输入等用户事件,或者来自于其他应用和进程的事件。除此之外,应用可以在不使用框架的情况下,通过自己来显示地将这些块加入到队列(线程安全工作队列:译者注)中。

几乎任何一个你的应用执行的代码块都被绑定到一个事件回调,比如输入,布局填充,或者绘制。当某事物触发了一个事件,这个事件所发生的线程会把该事件推出,并且推入到主线程消息队列中。然后这个主线程会服务该事件。

当动画事件或者屏幕更新发生了,为了以60帧每秒的频率平滑地渲染,系统会尝试每16毫秒执行一个工作块(该工作块用于负责绘制屏幕)。为了让系统到达这个目标,UI/View层级必须在主线程中更新。可是,当主线程消息队列包含了太多或太长的任务以至于 主线程无法足够快地完成更新时,应用应该把这些工作移到工作线程中。如果主线程无法在16毫秒以内无法完成执行工作块,用户可能会观察到钩住、滞后或者对输入缺乏UI响应。如果主线程阻塞了大约5秒时间,系统会显示一个“应用程序没有响应(ANR)”对话框,以允许用户直接关闭这个应用。

从主线程中移除大量或太长时间的任务,这样的话它们就不会干扰平滑的渲染和对用户输入的响应,这是你在应用中采用线程的最大原因。

线程和UI对象引用

通过设计,Android View对象不是线程安全的。应用预期是创建、使用以及销毁UI对象,都在主线程中。如果你尝试在其它线程而不是主线程修改甚至引用一个UI对象,结果可能是异常,无声故障,崩溃,以及其它未定义的错误行为。

引用问题被分为两类:显示引用和隐式引用。

显示引用

许多在非主线程上的任务都有一个更新UI对象的最终目标。可是,如果这些线程在View层级上访问对象,可能会导致应用不稳定:如果一个工作线程改变了一个对象的属性,而与此同时其它线程正在引用这个对象,其结果是未知的。

例如,设想一个在工作线程上持有UI对象直接引用的应用。在工作线程上的对象可能包含了一个对View的引用;但是在工作完成之前,这个View被从view层级中移除了。当这两个动作同时发生时,引用将View对象保留在了内存中并且在它上面设置了属性。可是,用户从未看到过这个对象,并且一旦对它引用消失,应用就会删除这个对象。

举另外一个例子,View对象包含了对activity的引用,而这个actvity又拥有这些View对象。如果那个activity销毁了,但是仍然存在一个引用它的线程工作块——直接或间接地——垃圾收集器将不会收集activity,直到那个工作块执行结束。

当某个诸如屏幕旋转等activity生命周期事件发生时,线程工作可能正在运行,在这种情形下可能会导致一个问题。系统将无法执行垃圾收集,直到正在运行的工作完成。结果,可能会有两个Activity对象在内存中,直到能够发生垃圾收集。

在像这样的场景下,我们建议您的应用在工线程工作任务中不要包含对UI对象的显示引用。避免这样的应用会帮助您避免这些类型的内存泄漏,同时避免线程竞争。

在所有情形下,您的应用在主线程中应该只用于更新UI对象。这意味着您应该制定一个协商策略,以允许多个线程将工作传递回主线程,主线程通过更新实际的UI对象来执行最顶层的activity或fragment。

隐式引用

一个常用的使用线程对象的代码设计瑕疵可能如以下代码片段所看到的:

1 //for java2 public class MainActivity extends Activity {3// ...4public class MyAsyncTask extends AsyncTask<Void, Void, String>{5  @Override protected String doInBackground(Void... params) {...}6  @Override protected void onPostExecute(String result) {...}7   }8 }
1 //for kotlin2 class MainActivity : Activity() {3  // ...4  class MyAsyncTask : AsyncTask<Unit, Unit, String>() {5   override fun doInBackground(vararg params: Unit): String {...}6   override fun onPostExecute(result: String) {...}7  }8 }

这个片段中的缺陷是,这段代码声明了线程对象MyAsyncTask为一个非静态Activity内部类(或者Kotlin中的内部类)。这个声明创建了一个封装Activity实例的隐式引用。因此,这个对象包含了一个activity引用,直到线程工作完成,在销毁这个被引用的activity时导致了一个延迟。反过来,这个延迟给内存施加了更多的压力。

解决这个问题最直接的途径是定义您的重载类实例为静态类,或者在它们自己的文件中,这样以移除隐式引用。

另外一种解决途径是声明这个AsyncTask对象为一个静态嵌套类(或者在Kotlin中移除内部修饰符)。这样做消除了隐式引用问题,因为静态嵌套类的方式和内部类有所不同:内部类的实例需要外部类实例进行实例化,并直接访问该封装实例的方法和字段。相比之下,一个静态的嵌套类不需要引用封装类的实例,所以它不包含对外部类成员的引用。

1 //for java2 public class MainActivity extends Activity {3// ...4static public class MyAsyncTask extends AsyncTask<Void, Void, String>{5  @Override protected String doInBackground(Void... params) {...}6  @Override protected void onPostExecute(String result) {...}7   }8 }
1 //for Kotlin2 class MainActivity : Activity() {3  // ...4  class MyAsyncTask : AsyncTask<Unit, Unit, String>() {5   override fun doInBackground(vararg params: Unit): String {...}6   override fun onPostExecute(result: String) {...}7  }8 }

线程和应用activity生命周期

应用生命周期可以影响在您的应用中线程如何工作。您可能需要决定一个线程在activity销毁后应该或者不应该继续存在。您也应该意识到线程优先级之间的关系以及activity是运行在前台还是后台。

持久线程

线程伴随着孵化它们的activity的一生而一直存在。线程继续执行,不被中断,不管activity的创建和销毁。在有些情况下,这个持久是令人满意的。

考虑一种情况,activity孵化了一组线程工作块,并且随后在工作线程可以执行这些块之前被销毁。应用应该如何处理这些正在运行的块呢?

如果这些块正要去更新不再存在的UI,没有任何让该工作继续的理由。例如,如果该工作用于加载来自数据库的用户信息,然后更新视图,那么这个线程就是不必要的。

相比之下,工作包可能有一些不和UI完全相关的好处。在这种情况下,您应该存留这个线程。例如,这些包可能正在等待下载一张图片,缓存到磁盘,以及更新这个相关的View对象。虽然这个对象不再存在了,但是下载和缓存图片的动作可能仍然是有帮助的,万一用户返回到这个被销毁的activity。

手动为所有线程对象管理生命周期响应可能变得异常复杂。如果您不正确管理它们,您的应用可能忍受内存竞争和性能问题。组合ViewModel和LiveData允许您加载数据,以及当它发生改变时被通知,而不用担心生命周期。ViewModel对象是解决这个问题的一种途径。ViewModels是在配置的更改中被维护的,这提供了一种简单的方法来保留您的视图数据。关于ViewModels的更多信息,请查看【ViewModel指导】,以及学习更多关于LiveData的知识,请查看【LiveData指导】。如果您可能也喜欢更多关于应用架构的信息,请阅读【应用架构指导】

线程优先级

正如在【进程和应用生命周期】中描述的那样,您应用的线程所接收到的优先级部分依赖于应用所处的应用生命周期。当您创建和管理您应用中的线程时,设置它们的优先级从而让正确的线程在正确的时间获取正确的优先级是一件重要的事。如果设置得太高,您的线程可能会中断UI线程和RenderThread,这会导致您的应用丢帧。如果设置得太低,您会使得您的同步任务(比如图片加载)比它们需要的慢。

任何时刻您创建线程,您应该调用setThreadPriority()。系统的线程调度器优先选择高优先级的线程,让优先级和最终完成所有工作的需要相平衡。一般来说,前台组线程获取了大约95%的设备总执行时间,然而后台组大约只获取约5%。

系统也使用Process类给每一个线程分配它们自己的优先值。

默认情况下,系统给线程优先级设置为和孵化线程相同的优先级和组成员身份。但是,您的应用可以通过使用setThreadPriority()显示地调整线程优先级。

Process类通过提供一组常量来帮助降低分配优先级值时的复杂度,您的应用可以使用这组常量来设置线程优先级。例如,THREAD_PRIORITY_DEFAULT代表了线程的默认值。您的应用应该把那些正在执行的非紧急工作的线程的线程优先级设置为THREAD_PRIORITY_BACKGROUND。

你的应用可以使用THREAD_PRIORITY_LESS_FAVORABLE 和 THREAD_PRIORITY_MORE_FAVORABLE常量作为增量来设置相对优先级。对于线程优先级列表,可以在Process类中查看【THREAD_PRIORITY】常量。

对于更多管理线程方面的信息,请查看关于【Thread】和【Process】类的引用文档。

线程帮助类

框架提供了相同的Java类和基础来帮助使用线程,比如Thread,Runnable以及Executors类。为了帮助降低和正在开发的Android线程应用相关的负载,框架提供了一组可以辅助开发的助手,比如AsyncTaskLoader和AsyncTask。每个帮助类都有一组特定的性能细微差别,使得它们对于线程问题的特定子集来说是独一无二的。在错误的场景使用错误的类会引起性能问题。

AsyncTask类

对于那些需要快速将任务从主线程转移到工作线程的应用而言,AsyncTask类是一个简单的,有用的基类。例如,输入事件可能会触发使用加载的位图来更新UI的需求。AsyncTask对象能够将位图加载和解码卸载到备用线程;一旦处理完成,AsyncTask对象可以管理接收返回到主线程的工作来更新UI。

当使用AsyncTask时,有一些重要的性能方面需要考虑。首先,默认情况下,应用会把它所创建的所有AsyncTask对象推入一个当线程。所以,它们以串行方式执行,并且和主线程一样,特别长的工作包会阻塞队列。所以,我建议您只使用AsyncTask处理时长少于5ms的工作项。

AsyncTask对象也是隐式引用问题最普遍的罪魁祸首。AsyncTask对象也会产生和显示引用相关的风险,但有时更容易解决这些问题。例如,一旦AsyncTask在主线程上执行它的回调,为了正确地更新UI对象,AsyncTask可能需要引用UI对象。在这种情况下,您可以使用WeakReference来存储对所需的UI对象引用,以及一旦AsyncTask在主线程上运行,可以访问该对象。需要清楚的是,持有对一个对象弱引用,不会让这个对象线程安全;弱引用仅仅提供了一种方法处理显示引用和垃圾收集问题。

HandlerThread类

虽然AsyncTask可用,但它可能并不总是您线程问题正确的解决途径。相反,您可能需要一个更加传统的途径来执行长时间运行的线程上的工作块,以及一些手动管理那些工作流的能力。

通过从您的Camera对象中获取预览帧,考虑一个常见的挑战。当您注册了Camera预览帧,您从onPreviewFra

源文地址:http://www.guoxiongfei.cn/cntech/14854.html