线程性能

线程性能

  熟练使用android线程能帮助你提升应用的性能。此页讨论了采用线程工作的几个方面:采用UI或主线程工作;应用生命周期和线程优先级的关系;和采用平台提供的方法管理复杂的线程。在任何一方面,该页描述了避免这些问题的潜在陷阱和策略。

Main Thread

  但你启动应用程序的时候,Android创建了一个新的Linux进程以及执行线程。这就是main线程,也被称为UI线程,负责屏幕上发生的任何事情。

  理解它是如何工作的能帮助你设计应用程序以使用主线程获得最佳性能。

Internals

  主线程设计非常简单:它唯一的工作是从线程安全的工作队列中获取和执行工作块,直到其应用程序终止。该框架从各种场合生成一些这些工作块。该场合包括与生命周期相关联的回调,用户事件(如输入),或来自其它应用程序和进程的事件。此外,应用程序可以自己明确地排队,而不使用框架的。

  你应用程序执行的几乎所有的代码块都是和事件回调相联系的,例如,输入,layout inflation或绘制。当某些事情触发事件时,事件发生的线程把事件推出其自身之外,并进入主线程的消息队列。然后主线程为事件提供服务。

  当动画或者屏幕更新正在执行,系统尝试在每隔16ms或者更快的时间内执行工作块(负责屏幕绘制),以便以60帧/分钟平滑的呈现。为了使系统达到这个目标、UI/View的层次结构必须在主线程上更新。然而,当主线程消息队列包含太多或者太长任务时,为使主线程足够快的完成更新,应用程序应该把工作移到工作线程。假如主线程不能在16ms内执行完工作块,用户可能观察到hitching,lagging或UI输入的响应不足。假如主线程阻塞大约5秒,系统显示Application Not Responding(ANR)提示框,以允许用户直接关闭应用程序。

  从主线程异常许多或长任务,这样它们就不会干扰平滑渲染和快速响应用户的输入,这就是你在应用程序中采用线程的主要原因。  

Threading and UI Object References

  通过设计,Android View对象不是线程安全的。一个应用程序有望在主线程上创建,使用和销毁UI对象。如果您尝试在主线程以外的其它线程中修改甚至引用UI对象,则结果可能是异常,静默失败,崩溃和其他未定义的不当行为。

  引用的问题分为两个不同的类别:显式引用和隐式引用。

Explicit references(显式引用)

  非主线程上的许多任务具有更新UI对象的最终目标。但是,如果这些线程中的一个访问视图层次结构中的对象,则可能导致应用程序不稳定:如果工作线程在任何其他线程引用对象的同时更改该对象的属性,则结果未定义。

  例如,考虑一个在工作线程上直接引用UI对象的应用程序。工作线程上的对象可能包含对View的引用;但在工作完成之前,视图被从视图层次结构中删除。当这两个动作同时发生时,引用将View对象保存在内存中并设置属性。但是,用户永远不会看到此对象,并且一旦对该对象的引用消失,该应用就会删除该对象。

  在另一个示例中,View对象包含对拥有它们的activity的引用。如果该activity被销毁,但仍然有直接或间接引用它的线程工作块,则垃圾收集器将不会收集activity,直到该工作块完成执行。

  在一些activity生命周期事件(如屏幕旋转)发生的情况下,这种情况会导致线程工作可能在运行的情况下出现问题。在运行工作完成之前,系统将无法执行垃圾回收。因此,内存中可能会有两个Activity对象,直到垃圾收集发生。

   使用这些场景,我们建议您的应用程序不包括在线程工作任务中明确引用UI对象。避免这样的引用可以帮助您避免这些类型的内存泄漏,同时也可以避免线程争用。

   在所有情况下,您的应用程序只能更新主线程上的UI对象。这意味着您应该制定一个允许多个线程将工作回传到主线程的协商策略,哪些任务是最新activity或fragment与更新实际UI对象的工作。

Implicit references(隐式引用)

   在下面的代码片段中可以看到具有线程对象的常见代码设计缺陷:

public class MainActivity extends Activity {
  // …...
  public class MyAsyncTask extends AsyncTask<Void, Void, String>   {
    @Override protected String doInBackground(Void... params) {...}
    @Override protected void onPostExecute(String result) {...}
  }
}

  这个代码段的缺点在于,该代码将线程对象MyAsyncTask声明为某些activity的非静态内部类。此声明将为封装的Activity实例创建一个隐式引用。因此,在线程完成之前,该对象包含对该activity的引用,导致引用的activity的回收延迟。反过来,这种延迟给内存带来了更大的压力。

  直接解决这个问题的方法是将重载的类实例定义为静态类,或者在自己的文件中,从而删除隐式引用。

  另一个解决方案是将AsyncTask对象声明为静态嵌套类。这样做消除了隐式引用问题,因为静态嵌套类与内部类不同:内部类的实例需要实例化外部类的实例,并且可以直接访问其封装类实例的方法和字段。相比之下,静态嵌套类不需要引用封装类(enclosing class)的实例,因此它不包含外部类成员的引用。

public class MainActivity extends Activity {
  // …...
  static public class MyAsyncTask extends AsyncTask<Void, Void, String>   {
    @Override protected String doInBackground(Void... params) {...}
    @Override protected void onPostExecute(String result) {...}
  }
}

Threading and App and Activity Lifecycles

  应用程序生命周期可能会影响应用程序中线程的工作原理。您可能需要确定线程在活动被销毁后应该还是不应该持续。您还应该了解线程优先级与活动是否在前台或后台运行之间的关系。

Persisting threads(持续线程)

  线程在产生它们的activities的生命周期中持续存在。线程继续执行,不间断,不管activities的创建或破坏。在某些情况下,这种持久性是可取的。

   考虑一种情况,其中activity产生一组线程工作块,然后在工作线程可以执行块之前被销毁。应用程序应该怎么处理运行中的块?

   如果这些块将要更新不再存在的UI,则无需继续工作。例如,如果工作是从数据库加载用户信息,然后更新视图,则线程不再需要。

  相比之下,工作包可能具有与UI完全不相关的一些好处。在这种情况下,你应该持续线程。例如,数据包可能正在等待下载图像,将其缓存到磁盘,并更新关联的View对象。虽然该对象不再存在,如果用户返回被销毁的activity,下载和缓存图像的行为可能仍然有帮助。

   对所有线程对象手动管理生命周期响应可能变得非常复杂。如果您不正确管理它们,您的应用程序可能会遇到内存争用和性能问题。Loaders是这个问题的一个解决方案。Loader有助于异步加载数据,同时还可以通过配置更改来持久化信息。有关更多信息,请参阅AsyncTaskLoader

Thread priority

  如过程和应用程序生命周期Processes and the Application Lifecycle)中所述,应用程序的线程接收的优先级部分取决于应用程序在应用程序生命周期中的位置。当您在应用程序中创建和管理线程时,重要的是设置其优先级,以便正确的线程在正确的时间获取正确的优先级。如果设置太高,您的线程可能会中断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类的参考文档。

Helper Classes for Threading

  该框架提供了相同的Java类和基类以便于线程化,如Thread,Runnable和Executors类。为了帮助减少与Android开发线程应用程序相关联的认知负载,该框架提供了一组可帮助开发的帮助者,例如AsyncTaskLoaderAsyncTask。每个助手类都有一组特定的性能细微差别,使得它们对于特定的线程问题子集是唯一的。对错误的情况使用错误的类可能会导致性能问题。 

The AsyncTask class

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

  使用AsyncTask时,请记住几个重要的性能方面。首先,默认情况下,应用程序将其创建的所有AsyncTask对象推送到单个线程中。因此,它们以串行方式执行,并且与主线程一样,特别长的工作包可以阻止队列。因此,我们建议您只使用AsyncTask处理短于5ms的工作项。

   AsyncTask对象也是隐式应用问题的最常见的发生者。AsyncTask对象存在与显式引用相关的风险,但这些对象有时更易于解决。例如,AsyncTask可能需要对UI对象的引用,以便在AsyncTask在主线程上执行其回调时正确更新UI对象。在这种情况下,您可以使用WeakReference存储对所需UI对象的引用,并且一旦AsyncTask在主线程上运行,就可以访问对象。要清楚,对对象持有一个WeakReference不会使对象线程安全; WeakReference仅提供一种处理显示引用和垃圾收集问题的方法。

The HandlerThread class

  虽然AsyncTask是有用的,但它可能并不总是您的线程问题的正确解决方案( it may not always be the right solution)。相反,您可能需要一种更传统的方法来在更长时间运行的线程上执行一个工作块,以及一些手动管理该工作流的功能。

  从Camera对象获取预览框架时,考虑一个常见的挑战。当您注册相机预览框时,您可以在onPreviewFrame()回调中接收它们,该调用在调用的事件线程上被调用。如果在UI线程上调用了这个回调函数,处理巨大像素数组的任务将会干扰渲染和事件处理工作。同样的问题也适用于AsyncTask,它也连续执行作业并容易受到阻塞。

  这是一个处理程序线程是适当的情况:处理程序线程实际上是一个长时间运行的线程,它从队列中抓取工作并对其进行操作。在这个例子中,当你的应用程序将Camera.open()命令委托给处理程序线程上的一个工作块时,相关的onPreviewFrame()回调位于处理程序线程上,而不是UI或AsyncTask线程。所以,如果要在像素上做长时间的工作,这可能是一个更好的解决方案。

  当您的应用程序使用HandlerThread创建一个线程时,不要忘记根据其工作类型设置线程的优先级。记住,CPU只能并行处理少量线程。确定优先级有助于系统知道在所有其他线程正在争取注意时安排此工作的正确方法

The ThreadPoolExecutor class

  有些类型的工作可以减少高度并行的分布式任务。例如,一个这样的任务是计算一个800万像素图像的每个8×8块的滤镜。使用这个创建的工作包的绝对数量,AsyncTask和HandlerThread不是适当的类。AsyncTask的单线程性质将所有线程化工作转换为线性系统。

  ThreadPoolExecutor是一个帮助类,使此过程更容易。该类管理一组线程的创建,设置其优先级,并管理如何在这些线程之间分配工作。随着工作负载的增加或减少,该类会自动启动或销毁更多线程以适应工作负载。

  这个类也可以帮助你的应用程序产生最佳线程数。当它构造一个ThreadPoolExecutor对象时,应用程序设置最小和最大线程数。当给予ThreadPoolExecutor的工作量增加时,类将考虑初始化的最小和最大线程数,并考虑待处理的工作量。基于这些因素,ThreadPoolExecutor决定在任何给定时间应该有多少个线程。

How many threads should you create?

  尽管从软件层面来看,您的代码可以创建数百个线程,但这样做可能会导致性能问题。您的应用程序与后台服务,渲染器,音频引擎,网络等共享有限的CPU资源。CPU真的只有并行处理少量线程的能力;上面的一切都处于优先级和调度问题。因此,只需要创建与工作负载需求一样多的线程就很重要。

  实际上,有一些变数负责这个,但挑选一个价值(如4,为初学者),并用Systrace测试它是与其他任何策略一样坚实。您可以使用试错来发现可以使用的最少线程数,而不会遇到问题。

  决定有多少线程的另一个考虑是线程不是免费的:它们占用内存。每个线程至少需要64k的内存。这在安装在设备上的许多应用程序中迅速增加,特别是在调用堆栈显着增长的情况下。

  许多系统进程和第三方库通常会旋转自己的线程池。如果您的应用程序可以重用现有的线程池,则此重用可能会通过减少内存和处理资源的争用来帮助执行性能。

 

 

原文地址:https://developer.android.google.cn/topic/performance/threads.html

posted @ 2017-09-19 09:06  流浪三毛  阅读(372)  评论(0编辑  收藏  举报