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

前言html

       本文翻译自Android开发者文档中的一篇官方文档,用于介绍如何经过正确使用线程来提高应用性能(Better performance through threading)。java

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

       路径为: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内存。这经过安装在设备上的应用很快累积起来,尤为是在调用栈显著地增加的情形下。

       许多系统进程和第三方库常常建立它们本身的线程池。若是您的应用可以重复使用一个存在的线程池,那么这个重复使用可能经过下降内存竞争和进程资源对性能有所帮助。

 

结语

       本文最大限度保持原文的意思,因为笔者水平有限,如有翻译不许确或不稳当的地方,请指正,谢谢!

相关文章
相关标签/搜索