熟练使用Android上的线程可以帮助你提高应用程序的性能。本文讨论使用线程的几个方面:使用UI或主线程、应用程序生命周期与线程优先级之间的关系、以及平台提供的帮助管理线程复杂性的方法。
主线程
当用户启动你的应用程序时,Android会创建一个新的Linux进程以及一个执行线程。这个主线程, 也被称为UI线程
,负责屏幕上发生的所有事情。了解它如何工作可以帮助您设计您的应用程序使用主线程以获得最佳性能。
主线程
的设计非常简单:它唯一的工作就是从线程安全的工作队列中取出并执行工作块,直到其应用程序终止。该框架从各个地方生成了这些工作的一部分。这些地方包括与生命周期信息相关的回调,用户事件(如输入)或来自其他应用程序和进程的事件。此外,应用程序可以自己明确排队,而无需使用框架。
应用程序 几乎执行任何代码块都与事件回调有关,例如输入,布局膨胀或绘制。当事件触发事件时,事件发生的线程将事件推出自身,并进入主线程的消息队列。主线程可以为事件提供服务。
当动画或画面更新发生时,系统每隔16ms
左右尝试执行一个工作块(负责绘制画面),以便以每秒60帧的速度平滑地进行渲染。为了达到这个目标,UI / View
层次结构必须在主线程上更新。但是,当主线程的消息传递队列包含的任务太多或太长时,主线程无法完成足够快的更新时,应用程序应将此工作移至工作线程。如果主线程无法在16ms
内完成工作块,用户可能会观察到拖尾,或者对输入的UI响应性不足。如果主线程阻塞大约五秒钟
,则系统显示应用程序不响应 (ANR
)对话框,允许用户直接关闭应用程序。
从主线程中移动大量或长时间的任务,以避免影响用户输入的平滑呈现和快速响应,是您在应用中采用线程的最大原因。
线程和UI对象引用
按照设计,Android视图对象不是线程安全的。预计应用程序将在主线程上创建,使用和销毁UI对象。如果你尝试修改甚至引用主线程以外的线程中的UI对象,则结果可能是异常,将无提示失败、崩溃以及其他未定义的错误行为。
引用的问题分为两个不同的类别:显式引用
和隐式引用
。
显式引用
非主线程上的许多任务都有更新UI对象的最终目标。但是,如果其中一个线程访问视图层次结构中的对象,则可能导致应用程序不稳定:如果工作线程在任何其他线程正在引用该对象的同时更改该对象的属性,则结果是未定义的。
例如,考虑在工作线程上保存对UI对象的直接引用的应用程序。工作线程上的对象可能包含对a的引用 View; 但在工作完成之前,View将从视图层次结构中移除。当这两个动作同时发生时,引用将View对象保存在内存中并在其上设置属性。但是,用户从不会看到这个对象,并且一旦对象的引用消失,该应用程序就会删除该对象。
在另一个例子中,View对象包含对拥有它们的活动的引用。如果这个活动被破坏了,但是仍有一个直接或间接引用它的线程块,那么垃圾收集器将不会收集这个活动,直到这个工作块完成执行。
在发生线程工作的情况下,如果发生某个活动生命周期事件(如屏幕旋转),此情形可能会导致出现问题。系统将无法执行垃圾收集,直到正在进行的工作完成。因此,Activity在垃圾收集发生之前,内存中可能有两个对象。
通过这样的场景,我们建议您的应用程序不要在线程工作任务中包含对UI对象的显式引用。避免这种引用可以帮助您避免这些类型的内存泄漏,同时避免线程争用。
在所有情况下,您的应用程序只应更新主线程上的UI对象。这意味着您应该制定一个协商策略,允许多个线程将工作交流回主线程,主线程将更新实际UI对象的工作作为最高活动或片段。
隐式引用
在下面的代码片段中可以看到一个带有线程对象的常见代码设计缺陷:
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实例的隐式引用
。因此,该对象包含对该活动的引用,直到线程工作完成,从而导致引用活动的销毁延迟。这种延迟反过来又会增加内存。
直接解决这个问题的方法是将你的重载的类实例定义为静态类
,或者在它们自己的文件中去掉隐式引用。
另一个解决方案是将AsyncTask对象声明为静态嵌套类
。这样做可以消除隐式引用问题,因为静态嵌套类不同于内部类:内部类的实例需要实例化外部类的实例,并且可以直接访问它的封闭方法和字段实例。相比之下,静态嵌套类不需要引用包含类的实例,因此它不包含对外部类成员的引用。
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) {...}
}
}
线程和应用程序以及活动生命周期
应用程序生命周期可以影响线程在您的应用程序中的工作方式。你可能需要决定一个线程应该或不应该在一个活动被破坏之后持续下去。你还应该了解线程优先级与活动是在前台还是在后台运行之间的关系。
持续执行线程
线程一直持续到产生它们的活动的生命周期。线程继续执行,不受干扰,无论创建或破坏活动。在某些情况下,这种持久性是可取的。
考虑一种情况,其中一个活动产生一组线程化的工作块,然后在工作线程可以执行块之前被销毁。应用程序应该怎样处理正在运行的程序段?
如果块要更新不再存在的用户界面,则没有理由继续工作。例如,如果工作是从数据库加载用户信息,然后更新视图,则不再需要该线程。
相比之下,工作包可能有一些不完全与用户界面相关的好处。在这种情况下,你应该坚持这个线程。例如,数据包可能正在等待下载映像,将其缓存到磁盘,并更新关联的 View对象。尽管该对象不再存在,但是在用户返回被销毁的活动的情况下,下载和缓存图像的行为仍可能是有帮助的。
手动管理所有线程对象的生命周期响应可能变得非常复杂。如果你不正确地管理它们,你的应用程序可能会遭受内存争用和性能问题。装载机 是解决这个问题的方法之一。加载程序有助于异步加载数据,同时还可以通过配置更改来保存信息。
线程优先级
如“ 进程和应用程序生命周期”
中所述,应用程序线程获得的优先级部分取决于应用程序生命周期中的应用程序的位置。在创建和管理应用程序中的线程时,重要的是设置其优先级,以便正确的线程在正确的时间获得正确的优先级。如果设置得太高,你的线程可能会中断UI线程和RenderThread
,导致你的应用程序丢帧。如果设置得太低,可以使你的异步任务(如图像加载)比他们需要的慢。
每当你创建一个线程,你应该打电话 setThreadPriority()
。系统的线程调度器优先考虑高优先级的线程,平衡这些优先级,最终完成所有工作。一般来说,前台组中的线程占设备总执行时间的95%左右,而后台组大约占5%。
系统还使用Process该类为每个线程分配自己的优先级值 。
默认情况下,系统将线程的优先级设置为与产卵线程相同的优先级和组成员资格。但是,您的应用程序可以使用明确调整线程优先级
setThreadPriority()
。
你的应用程序应该将线程的优先级设置为THREAD_PRIORITY_BACKGROUND
为执行不太紧急工作的线程。
你的应用程序可以使用THREAD_PRIORITY_LESS_FAVORABLE
和THREAD_PRIORITY_MORE_FAVORABLE
常量作为增量来设置相对优先级。有关线程优先级的列表,请参阅类中的THREAD_PRIORITY
常量Process。
线程的helper类
Fragment提供了相同的Java类和基本类型,以方便线程,比如Thread
,Runnable
和Executors
类。为了帮助减少与为Android开发线程应用程序相关的认知负载,框架提供了一系列可以帮助开发的帮助程序,例如AsyncTaskLoader
和AsyncTask
。每个辅助类都有一组特定的性能细微差别,这些细微差别使得它们对于特定的线程问题子集是唯一的。对错误的情况使用错误的类可能会导致性能问题。
AsyncTask类
本AsyncTask类是需要快速从主线程移动工作到工作线程应用程序的简单,实用的原始。例如,输入事件可能触发需要用加载的位图更新UI。一个AsyncTask 对象可以卸载位图加载和解码到另一个线程; 一旦处理完成,AsyncTask对象可以管理接收主线程上的工作以更新UI。
使用时AsyncTask,要记住一些重要的性能方面。首先,默认情况下,一个应用程序将AsyncTask 其创建的所有对象推送到单个线程中。因此,它们以串行方式
执行,并且与主线程一样,特别长的工作包可以阻塞队列。因此,我们建议您仅AsyncTask处理短于5ms的工作项目。
AsyncTask对象也是隐式引用问题的最常见的。 AsyncTask对象也存在与明确引用有关的风险,但这些有时候更容易解决。例如,AsyncTask 为了正确地更新UI对象,可能需要对UI对象的引用,一旦AsyncTask在主线程上执行其回调。在这种情况下,您可以使用一个WeakReference
来存储对所需UI对象的引用,并AsyncTask在主线程上运行时访问该对象 。要清楚,持有一个WeakReference
对象不会使对象线程安全; 在 WeakReference
仅提供处理与明确提到和垃圾收集问题的方法。
HandlerThread类
虽然一个AsyncTask 是有用的, 它可能并不总是正确的解决你的线程问题。相反,您可能需要更传统的方法来在较长时间运行的线程上执行一个工作块,以及手动管理该工作流的能力。
考虑从Camera对象获取PreviewFrame的常见挑战 。当你注册摄像头预览帧时,你会在onPreviewFrame()
调中接收到这些回调,该回调将在调用它的事件线程上调用。如果在UI线程上调用此回调函数,则处理巨大像素数组的任务将干扰渲染和事件处理工作。同样的问题也适用于AsyncTask连续执行作业,这容易被阻塞。
这是一个处理程序线程适当的情况:处理程序线程实际上是一个长时间运行的线程,它从队列中抓取工作,并对其进行操作。在这个例子中,当你的应用程序把这个Camera.open()
命令委托 给处理程序线程的一个工作块时,相关的onPreviewFrame()
回调就落在处理程序线程上,而不是UI或AsyncTask 线程上。所以,如果你要在像素上做长时间的工作,这可能是一个更好的解决方案。
当你的应用程序创建一个使用的线程时HandlerThread,不要忘记 根据它所做的工作类型来设置线程的 优先级。请记住,CPU只能并行处理少量的线程。设置优先级有助于系统知道正确的方式来安排这项工作,当所有其他线程争取注意。
ThreadPoolExecutor类
有一些类型的工作可以降低到高度并行的分布式任务。例如,一个这样的任务是为一个8兆像素图像的每个8×8块计算滤波器。这个工作包的数量很大,而且不是合适的类别。单线程本质将把所有的线程化工作转化为一个线性系统。另一方面,使用这个类会要求程序员手动管理一组线程之间的负载平衡。
ThreadPoolExecutor
是一个辅助类,使这个过程更容易。这个类管理着一组线程的创建,设置它们的优先级,并管理这些线程之间的工作分配方式。随着工作量的增加或减少,这个类会加速或破坏更多的线程以适应工作负载。
这个类也可以帮助你的应用产生最佳的线程数量。当它构造一个ThreadPoolExecutor
对象时,应用程序设置最小和最大线程数。由于ThreadPoolExecutor
增加的工作量 ,班级将考虑初始化的最小和最大线程数,并考虑待处理的工作量。基于这些因素,ThreadPoolExecutor
决定在任何给定时间应该有多少线程活着。
你应该创建多少个线程?
尽管从软件级别来看,你的代码有能力创建数百个线程,但这样做会造成性能问题。你的应用程序与后台服务,渲染器,音频引擎,网络等共享有限的CPU资源。CPU实际上只能并行处理少量的线程,上面的所有内容都会遇到 优先级和调度问题。因此,只需创建与您的工作负载所需的线程数量就很重要。
实际上,有很多变量对此负责,但是选择一个值(比如4,对于初学者),并且使用Systrace进行测试与 其他方法一样可靠。你可以使用反复试验来发现可以使用的最少线程数量,而不会遇到问题。
决定有多少线程的另一个考虑因素是线程不是免费的:它们占用内存。每个线程最少需要64k的内存。这在设备上安装的许多应用程序中快速增加,尤其是在调用堆栈显着增长的情况下。
许多系统进程和第三方库经常会启动自己的线程池。如果你的应用程序可以重复使用现有的线程池,则这种重用可以通过减少内存和处理资源的争用来帮助提高性能。
网友评论