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

前言

       本文翻译自Android开发者文档中的一篇官方文档,用于介绍如何通过正确使用线程来提升应用性能(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 java
2 public class MainActivity extends Activity {
3   // ...
4   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 kotlin
2 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 java
2 public class MainActivity extends Activity {
3   // ...
4   static 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 Kotlin
2 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预览帧,您从onPreviewFrame()回调中收到它们,该回调被调用它的工作线程所调用。如果该回调在UI线程中被调用,处理巨大像素阵列的任务将会被渲染和事件进程工作所干扰。同样的问题也适用于AsyncTask,它也串行执行工作并且很容易阻塞。

       这是一种handler 线程可能适用的场景:handler线程实际上是一个长时间运行的线程,它从队列中获取任务并且在它上面操作。在这个例子中,当您的应用委派Camera.open()命令给handler线程上的工作块时,相关联的onPreviewFrame()回调降临到handler线程,而不是UI或AsyncTask线程。所以,如果您即将处理长时间运行的像素上的工作,对您来说这可能是一个更好的解决途径。

       当您的应用使用HandlerThread创建一个线程,不要忘记在它正在处理的这类工作的基础上设置这个线程的优先级。切记,CPU只能并行处理少量的线程。当所有其他线程在争夺关注时,设置优先级会帮助系统知道正确的方法调度这项任务。

       ThreadPoolExecutor类

       有一些明确类型的工作可以被简化为高度并行的分布式任务。例如,其中一项任务就是为每一个8百万像素图片的8x8块计算一个过滤器。由于创建了大量的工作包,AsyncTask和HandlerThread都不是合适的类。AsyncTask的单线程特性会把所有的线程池工作转变为一个线性系统。另一方面,使用HandlerThread类需要程序员手动管理一组线程之间的负载平衡。

       ThreadPoolExector类是一个帮助类,用于让进程更简单。这个类用于管理一组线程的创建,设置他们的优先级,以及管理这些线程之间如何分配工作。当工作量增加了或者减少了,该类创建或者销毁更多的线程以调整工作量。

       这个类也帮助您的应用生成适宜数量的线程。当构建一个ThreadPoolExecutor对象时,应用设置了一个最小和最大数量的线程。当给予ThreadPoolExecutor的工作量增加时,该类将会考虑初始化的最小和最大的线程数量,以及考虑即将要进行的工作的数据。基于这些因素,ThreadPoolExecutor决定了在任意给定的时间点多少线程应该是存活的。

       您应该创建多少个线程?

       虽然从软件层面上来看,您的代码有能力创建几百个线程,但是这样做会创建性能问题。您的应用和后台service、渲染器、音频引擎、网络以及更多功能共享有限的CPU资源。CPU确实只有能力并行处理少量的线程;以上的所有一切都会产生优先级和调度问题。因此,根据您工作量的需要创建线程的数量是很重要的。

       实际上,有很多变量造成这个原因,但是选择一个值(比如4,作为初始值),并且使用Systrace来测试是一个和其它方案一样稳定的策略。您可以使用反复试验的方法找到您可以使用的最小线程数量,而不会产生问题。

       另外一个决定拥有多少线程的考虑就是线程不是“免费”的:它们占用内存。每一个线程花费了至少64k内存。这通过安装在设备上的应用很快累积起来,尤其是在调用栈显著地增长的情形下。

       许多系统进程和第三方库经常创建它们自己的线程池。如果您的应用能够重复使用一个存在的线程池,那么这个重复使用可能通过降低内存竞争和进程资源对性能有所帮助。

 

结语

       本文最大限度保持原文的意思,由于笔者水平有限,若有翻译不准确或不妥当的地方,请指正,谢谢!

posted @ 2019-04-15 16:50 宋者为王 阅读(...) 评论(...) 编辑 收藏