Android性能优化-线程性能优化
线程的性能
熟练使用Android上的线程可以帮助你提高应用程序的性能。 本篇文章讨论了使用线程的几个方面:使用UI或主线程; 应用程序生命周期和线程优先级之间的关系; 以及平台提供的帮助管理线程复杂性的方法。 在每一部分,本篇都描述了潜在的陷阱以及如何避免它们的策略。
主线程
当用户启动你的应用程序时,Android会创建一个新的 Linux process 以及一个执行线程。 这个main线程,也称为UI线程,负责屏幕上发生的一切。 了解其工作原理可以帮助你使用主线程设计你的应用程序以获得最佳性能。
内部细节
主线程具有非常简单的设计:它的唯一工作就是从线程安全的工作队列中取出并执行工作块,直到应用程序被终止。 框架从各个地方生成一些这些工作块。 这些地方包括与生命周期信息,用户事件(如输入)或来自其他应用程序和进程的事件相关联的回调。 此外,应用程序还可以在不使用框架的情况下显式地将工作块加入队列。
应用程序执行的 任何代码块都会被绑定到一个事件回调上,例如输入,布局填充或绘制。 当某个时间触发一个事件时,事件发生的所在线程会将事件加入到主线程的消息队列。 之后主线程可以处理该事件。
当发生动画或屏幕更新时,系统试图每16ms左右执行一个工作块(负责绘制屏幕),以便以 每秒60帧的速度平滑地渲染。 为了让系统达到这个目标,一些操作必须发生在主线程上。 但是,当主线程的消息队列包含太多或太耗时的任务,为了让主线程能够在16ms内完成工作,你应将这些任务移到工作线程中去。 如果主线程不能在16ms内完成执行的代码块,则用户可能感觉到卡顿或UI响应较慢。 如果主线程阻塞大约5秒钟,系统将显示“ (ANR)”对话框,允许用户直接关闭应用程序。
从主线程移除多个或耗时的任务,以便它们不会干扰到平滑渲染和对用户输入的快速响应,是你在应用程序中采用线程的最大原因。
线程和UI对象的引用
按照设计, Android UI对象不是线程安全的。 应用程序应该在主线程上创建,使用和销毁UI对象。 如果尝试修改或甚至引用除主线程之外的线程中的UI对象,结果可能是异常,静默失败,崩溃和其他未定义的错误行为。
UI对象引用导致的问题可以划分为两种:显式引用和隐式引用。
显示引用
许多非主线程上的任务在最后都会更新UI对象。 但是,如果某一个线程访问视图层级中的对象,可能会导致应用的不稳定性:如果工作线程修改了同时被任何其他线程引用的对象属性(这里都是指UI对象),则结果是不可预测的。
假设一个应用程序在工作线程上直接引用UI对象。 这个UI对象可能包含对一个 View
的引用; 但在工作完成之前,该View被从视图层次结构中删除了。 如果该引用将View对象保留在内存中并对其设置属性,用户并不会看到此对象,因为一旦对象的引用消失,应用程序就会删除该对象。
再举另一个例子,View对象(被工作线程引用)持有包含它们的Activity的引用。 如果该Activity被销毁了,但仍有一个工作的线程直接或间接引用它 - 垃圾收集器将不会回收Activity,直到该工作线程执行完成。
在某些Activity生命周期事件(如屏幕旋转)发生时,某些线程工作可能正在运行。 系统将无法执行垃圾回收,直到正在进行的工作完成。 因此,在内存中可能会有两个Activity对象,直到垃圾回收发生。
考虑到以上场景,我们建议你的应用程序的工作线程中不应该包含对UI对象的显式引用。 避免此类引用可帮助你避免这些类型的内存泄漏,同时避免线程竞争。
在所有情况下,应用程序应该只在主线程上更新UI对象。 如果有多个任务希望更新实际的UI,你应该制定一个策略,允许多个线程交互,最终将结果返回到主线程。
隐式引用
在以下代码片段中可以看到带有线程对象代码的常见设计缺陷:
|
这段代码的缺陷是将线程对象MyAsyncTask声明为一些Activity的内部类。 这种声明创建一个对Activity对象隐式引用。 因此,该对象持有对Activity的引用,直到线程工作完成,这样会导致所引用的Activity延迟销毁。 这种延迟会给内存带来更大的压力。
解决该问题的直接解决方案是在自己的文件中定义重载类实例,从而移除对Activity的隐式引用。
另一个解决方案是将AsyncTask声明为静态内部类。 这样做也可以消除隐式引用问题,因为静态内部类与普通内部类不同:普通内部类实例需要外部类的实例才可以实例化,并且可以直接访问其包含的方法和字段。 相比之下,静态内部类不需要引用外部类实例,因此它不包含对外部类成员的引用。
|
线程和应用程序以及Activity的生命周期
应用程序生命周期会对应用程序中线程的工作产生影响。 在Activity被销毁后,你可能需要决定一个线程是否应该持久化。 还应该注意线程优先级和Activity是否在前台或后台运行之间的关系。
持久化的线程
线程的生命周期大于生成它们的Activity的生命周期。 不管Activity的创建或销毁,线程继续执行,不会被打断。 在一些情况下,这种持久性是不期望的。
考虑一种情况,某个Activity发起了一组线程工作任务,但在工作线程执行完之前该Activity被销毁了。 应用程序应该如何处理那些还在执行的任务?
如果这些任务将来会去更新不再存在的UI,那么这些任务就不应该继续工作。例如,如果该任务是从数据库加载用户信息并更新视图,那么该线程就是不需要的。
相比之下,如果任务组不是完全和UI相关的,还是很有用的。例如,任务组可能在等待下载图片,并将其缓存到磁盘,然后去更新相关的 View
对象。尽管View对象不再存在,下载和缓存图像的行为仍然是有帮助的,因为用户有可能还会回到这个被销毁的Activity。
手动管理所有线程对象的生命周期可能非常复杂。如果你不能正确地管理它们,你的应用程序可能会遭受内存竞争和性能问题。 Loaders 是解决这个问题的一种方案。 Loaders 有助于异步加载数据,当configuration变化时仍旧会持久化信息。
线程的优先级
如 进程和应用生命周期中所述,应用程序线程接收的优先级部分取决于应用在其生命周期所处的阶段。 在应用程序中创建和管理线程时,设置其优先级很重要,这样可以让线程在正确的时间获得正确的优先级。 如果设置太高,你的线程可能会打断UI线程和渲染线程,导致你的应用程序丢帧。 如果设置太低,可能会导致你的异步任务(如图像加载)比它们实际需要的慢。
每次你创建一个线程,你应该调用 setThreadPriority()
方法。 系统的线程调度器程会优先选择优先级较高的线程,并根据需要权衡这些优先级,最终完成所有的工作。
通常, 前台组线程大约占用来设备总执行时间的95%,而后台组大约占5%。
系统也会通过Process类为每个线程分配其自己的优先级值。
默认情况下,系统将线程的优先级设置为与创建它的线程相同的优先级和组成员资格。 但是,你可以通过使用 setThreadPriority()
明确调整线程优先级。
Process
类通过提供一组常量来帮助你降低分配优先级的复杂性,你可以使用这些常量来设置线程优先级。 例如, THREAD_PRIORITY_DEFAULT 表示线程的默认值。
对于不那么紧急执行的工作线程,你应将其优先级设置为 THREAD_PRIORITY_BACKGROUND 。
你也可以使用 THREAD_PRIORITY_LESS_FAVORABLE 和 THREAD_PRIORITY_MORE_FAVORABLE 常量作为增量值来设定相对优先顺序。
所有这些枚举状态和修饰符的列表出现在 THREAD_PRIORITY_AUDIO
类的参考文档中。 有关管理线程的更多信息,请参阅有 Thread
and Process
的参考文档。
https://developer.android.com/reference/android/os/Process.html#THREAD_PRIORITY_AUDIO
线程的帮助类
框架为线程提供了和java相同的类和原始类,例如 Thread
和 Runnable
类。
为了帮助减少与开发Android线程应用程序相关的门槛,框架提供了一组帮助类。 每个助手类在性能上都有一些细微差别,以便去处理特定的线程问题。 对错误的场景使用了错误的类可能会导致性能问题。
AsyncTask 类
AsyncTask
类是一个简单,有用的原始类,可以帮你快速将工作从主线程移动到工作线程。 例如,输入事件可能会触发需要加载bitmap的UI更新。 AsyncTask
对象可以将bitmap加载和解码任务放到备用线程;
一旦处理完成, AsyncTask
对象会返回到主线程上去更新UI。
当使用 AsyncTask
时,有几个重要的性能方面的问题要记住。 首先,默认情况下,应用程序将其创建的所有 AsyncTask
对象推送到单个线程中。
因此,它们以串行方式执行,和主线程类似,特别耗时的工作组会阻塞队列。 因此,我们建议你只使用 AsyncTask
处理持续时间短于5ms的任务。
AsyncTask
对象也会导致常见的隐式引用问题。 而且, AsyncTask
对象也存在显式引用的风险,但通常这种问题比较容易解决。
例如,AsyncTask可能需要对UI对象的引用,以便在 AsyncTask
回调到主线程时更新UI对象。 在这种情况下,可以使用 WeakReference
存储对所需UI对象的引用,在 AsyncTask
回调到主线程时先访问一次该UI对象。
但你需要注意,持有某个对象的 WeakReference
并不会使该对象变为线程安全的; WeakReference
只提供了一个处理显式引用和垃圾回收问题的方法。
HandlerThread 类
虽然 AsyncTask
很有用,但对于你的线程问题,它可能 不会总是正确的解决方案。相反,你可能需要一种更传统的方法来在长时间运行的线程上执行一个工作块,并且有一些能力来手动管理该工作流。
我们考虑从Camera对象获取预览帧的场景。当你注册了相机预览事件,将会在 onPreviewFrame()
回调中收到它们,该回调会在调用它的事件线程上触发。如果这个回调在UI线程上触发,处理大量像素数组的任务将干扰渲染和事件处理工作。 AsyncTask
也会有同样的问题, AsyncTask
会串行地执行任务,容易受阻塞(这个高版本已经使用线程池了)。
这种情况使用 HandlerThread
更合适: HandlerThread
实际上是一个长时间运行的线程,它从队列中抓取工作,并对其进行操作。在这个例子中,当你的应用程序将 Camera.open()
命令委托给HandlerThread上的一个工作块时,相关的 onPreviewFrame()
回调会落在HandlerThread上,而不是UI或AsyncTask线程。所以,如果你要对像素进行长时间的操作,这可能是一个更好的解决方案。
当你的应用程序使用 HandlerThread
创建一个线程时,不要忘记 根据工作类型设置线程的优先级。
记住,CPU只能并行处理少数线程。当所有其他线程都在争取资源时, 设置优先级有助于系统知道如何正确的调度这项工作。
ThreadPoolExecutor 类
有些类型的工作是高度并行,分布式的。例如,为8百万像素图像的每个8×8块计算滤波。创建这种量级的工作,AsyncTask和HandlerThread都不合适。 AsyncTask的单线程性质将所有线程池工作转换为线性系统。另一方面,使用HandlerThread类将需要程序员手动管理一组线程之间的负载平衡。
这种情况,使用 ThreadPoolExecutor
类来处理会更容易。该类可以管理一组线程的创建,优先级设置,并权衡分配到这些线程的任务如何处理。随着工作负载增加或减少,该类会自动启动或销毁线程来适应工作负载。
此类还可以帮助你的应用程序创建最佳线程数。当在构造一个 ThreadPoolExecutor
对象时,可以设置最小和最大线程数。随着 ThreadPoolExecutor
的负载增加,该类将考虑初始化的最小和最大线程数,并考虑待处理的工作量。基于这些因素, ThreadPoolExecutor
决定在任何给定时间应该有多少线程存活。
你应该创建多少线程?
虽然从软件层面来看,你的代码有能力创建数百个线程,但这样做可能会造成性能问题。 CPU只有并行处理少量线程的能力;以上提到的都会遇到 优先级和调度问题。因此,只创建与你的工作负载需要的线程是很重要的。
实际上,许多因素都会对优先级和调度有影响,但你可以选择一个值(比如初始值设为4),并通过 Systrace进行测试。通过试错的方式来确定可以使用而又不会产生问题的最小线程数。
你需要考虑创建多少线程的另一个原因是线程不是免费的:它们占用内存。每个线程最少消耗64k内存。如果设备上安装了许多应用,该值就会快速添加,特别是在调用栈显著增长的情况下。
许多系统进程和第三方库经常调度自己的线程池。如果你的应用程序可以重用现有的线程池,则此重用能够减少内存和处理资源的竞争来帮助提高性能。
本人具有8年开发经验(6年android开发),android技术支持、培训和小项目开发,联系qq:1157464105,价格保证最优惠