原文链接:Better Performance through Threading

线程的性能

熟练使用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,你应该制定一个策略,允许多个线程交互,最终将结果返回到主线程。

隐式引用

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

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

这段代码的缺陷是将线程对象MyAsyncTask声明为一些Activity的内部类。 这种声明创建一个对Activity对象隐式引用。 因此,该对象持有对Activity的引用,直到线程工作完成,这样会导致所引用的Activity延迟销毁。 这种延迟会给内存带来更大的压力。

解决该问题的直接解决方案是在自己的文件中定义重载类实例,从而移除对Activity的隐式引用。

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

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

线程和应用程序以及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内存。如果设备上安装了许多应用,该值就会快速添加,特别是在调用栈显著增长的情况下。

许多系统进程和第三方库经常调度自己的线程池。如果你的应用程序可以重用现有的线程池,则此重用能够减少内存和处理资源的竞争来帮助提高性能。

来源:http://www.lightskystreet.com/2016/10/18/android-optimize-thread/

Android性能优化-线程性能优化的更多相关文章

  1. Android性能优化之启动速度优化

    Android性能优化之启动速度优化   Android app 启动速度优化,首先谈谈为什么会走到优化这一步,如果一开始创建 app 项目的时候就把这个启动速度考虑进去,那么肯定就不需要重新再来优化 ...

  2. Android性能优化-App启动优化

    原文地址:https://developer.android.com/topic/performance/launch-time.html#common 通常用户期望app响应和加载速度越快越好.一个 ...

  3. Android比较实用的性能优化

    Android设备作为一种移动设备,无论是内存还是CPU的性能都受到了很大的限制,这导致Android程序的性能问题异常突出,随着产品的不断更新迭代,对于性能优化提出了更高的要求.本篇文章从稳定性.流 ...

  4. Android性能优化之数据库优化

    本文为性能优化的第一篇——数据库性能优化,原理适用于大部分数据库包括Sqlite.Mysql.Oracle.Sql server,详细介绍了索引(优缺点.分类.场景.规则)和事务,最后介绍了部分单独针 ...

  5. Android性能优化之布局优化

    最新最准确内容建议直接访问原文:Android性能优化之布局优化 本文为Android性能优化的第二篇——布局优化,主要介绍使用抽象布局标签(include, viewstub, merge).去除不 ...

  6. 【转】Android性能优化之布局优化篇

     转自:http://blog.csdn.net/feiduclear_up/article/details/46670433 Android性能优化之布局优化篇 分类: andorid 开发2015 ...

  7. Android 性能优化 三 布局优化ViewStub标签的使用

    小黑与小白的故事,通过虚拟这两个人物进行一问一答的形式来共同学习ViewStub的使用 小白:Hi,小黑,ViewStub是什么?听说能够用来进行布局优化. 小黑:ViewStub 是一个隐藏的,不占 ...

  8. Android开发学习之路--性能优化之布局优化

      Android性能优化方面也有很多文章了,这里就做一个总结,从原理到方法,工具等做一个简单的了解,从而可以慢慢地改变编码风格,从而提高性能. 一.Android系统是如何处理UI组件的更新操作的 ...

  9. 在 Android开发中,性能优化策略十分重要

    在 Android开发中,性能优化策略十分重要本文主要讲解性能优化中的布局优化,希望你们会喜欢.目录 示意图 1. 影响的性能 布局性能的好坏 主要影响 :Android应用中的页面显示速度 2. 如 ...

随机推荐

  1. hdu4052矩形面积并

    建模需要注意下细节,,这是做扫描线的惯例,就是最好把模型建立在笛卡尔坐标系上 剩下的看链接和注释https://blog.csdn.net/shiqi_614/article/details/7983 ...

  2. 使用vmware提示无法打开内核设备 \\.\Global\vmx86: 系统找不到指定的文件

    问题描述 打开虚拟机时候提示 “vmware没有正常关闭,再次打开使用时蓝屏,在安全模式下再次打开不会蓝屏,但提示“无法打开内核设备 \\.\Global\vmx86: 系统找不到指定的文件,你想要安 ...

  3. 和组合数有关的dp

    1. UVaLive 7143 Room Assignment 用dp[i][r]表示,前i个盒子已经放完了,手上还拿着r对同色球. 状态转移方程为:dp[i+1][r-a-b] = dp[i][r] ...

  4. [ZJOI2010]贪吃的老鼠

    很不错的一道网络流的题目 二分答案是显然的 首先不考虑每个饼干只能一个老鼠吃 那很显然的建图就是将时间点按照开始结束的点分成2*n-1段 然后对每一段时间建m个老鼠的点,然后s-它限流,再从它到目前可 ...

  5. <构建之法>阅读笔记6

    第九章:项目经理 是讲项目经理的作用功能和重要性,书里面主要讲的是微软的PM(Programe Manager)和其他团队PM(Project Manager)的区别,还介绍了PM的能力要求以及人物, ...

  6. 【noip模拟赛5】细菌

    描述 近期,农场出现了D(1<=D<=15)种细菌.John要从他的 N(1<=N<=1,000)头奶牛中尽可能多地选些产奶.但是如果选中的奶牛携带了超过 K (1<=K ...

  7. Hat’s Words HDU1247

    一个很经典的字典树题目 先建树 再拆单词进行判断是否都在树内 因为爆内存错了很久 如果一个四十万的数组  用mamset的话会直接爆几十万的内存 所以要:用多少 初始化多少才对!( 修改了两条初始化语 ...

  8. 【Java】 剑指offer(55-2) 平衡二叉树

      本文参考自<剑指offer>一书,代码采用Java语言. 更多:<剑指Offer>Java实现合集   题目 输入一棵二叉树的根结点,判断该树是不是平衡二叉树.如果某二叉树 ...

  9. Redis数据结构之字符串

    学习阶段分成两个部分,一个是redis客户端,一个是java客户端操作 一:在redis客户端操作 1.先删除里面的几个key 2.set与get与getset 3.数值的增减 值递增1,或者减一 如 ...

  10. [OpenCV-Python] OpenCV 中图像特征提取与描述 部分 V (二)

    部分 V图像特征提取与描述 OpenCV-Python 中文教程(搬运)目录 34 角点检测的 FAST 算法 目标 • 理解 FAST 算法的基础 • 使用 OpenCV 中的 FAST 算法相关函 ...