【Bugly干货分享】Android性能优化典范之多线程篇

本文涉及的内容有:多线程并发的性能问题,介绍了 AsyncTask,HandlerThread,IntentService 与 ThreadPool 分别适合的使用场景以及各自的使用注意事项,这是一篇了解 Android 多线程编程不可多得的基础文章,清楚的了解这些 Android 系统提供的多线程基础组件之间的差别以及优缺点,才可以在项目实战中作出最恰当的选择。android

1. Threading Performance

在程序开发的实践当中,为了让程序表现得更加流畅,咱们确定会须要使用到多线程来提高程序的并发执行性能。可是编写多线程并发的代码一直以来都是一个相对棘手的问题,因此想要得到更佳的程序性能,咱们很是有必要掌握多线程并发编程的基础技能。
众所周知,Android 程序的大多数代码操做都必须执行在主线程,例如系统事件(例如设备屏幕发生旋转),输入事件(例如用户点击滑动等),程序回调服务,UI 绘制以及闹钟事件等等。那么咱们在上述事件或者方法中插入的代码也将执行在主线程。
腾讯bugly
一旦咱们在主线程里面添加了操做复杂的代码,这些代码就极可能阻碍主线程去响应点击/滑动事件,阻碍主线程的 UI 绘制等等。咱们知道,为了让屏幕的刷新帧率达到 60fps,咱们须要确保 16ms 内完成单次刷新的操做。一旦咱们在主线程里面执行的任务过于繁重就可能致使接收到刷新信号的时候由于资源被占用而没法完成此次刷新操做,这样就会产生掉帧的现象,刷新帧率天然也就跟着降低了(一旦刷新帧率降到 20fps 左右,用户就能够明显感知到卡顿不流畅了)。
腾讯bugly
为了不上面提到的掉帧问题,咱们须要使用多线程的技术方案,把那些操做复杂的任务移动到其余线程当中执行,这样就不容易阻塞主线程的操做,也就减少了出现掉帧的可能性。
腾讯bugly
那么问题来了,为主线程减轻负的多线程方案有哪些呢?这些方案分别适合在什么场景下使用?Android 系统为咱们提供了若干组工具类来帮助解决这个问题。编程

  • AsyncTask: 为 UI 线程与工做线程之间进行快速的切换提供一种简单便捷的机制。适用于当下当即须要启动,可是异步执行的生命周期短暂的使用场景。
  • HandlerThread: 为某些回调方法或者等待某些任务的执行设置一个专属的线程,并提供线程任务的调度机制。
  • ThreadPool: 把任务分解成不一样的单元,分发到各个不一样的线程上,进行同时并发处理。
  • IntentService: 适合于执行由 UI 触发的后台 Service 任务,并能够把后台任务执行的状况经过必定的机制反馈给 UI。
    了解这些系统提供的多线程工具类分别适合在什么场景下,能够帮助咱们选择合适的解决方案,避免出现不可预期的麻烦。虽然使用多线程能够提升程序的并发量,可是咱们须要特别注意由于引入多线程而可能伴随而来的内存问题。举个例子,在 Activity 内部定义的一个 AsyncTask,它属于一个内部类,该类自己和外面的 Activity 是有引用关系的,若是 Activity 要销毁的时候,AsyncTask 还仍然在运行,这会致使 Activity 没有办法彻底释放,从而引起内存泄漏。因此说,多线程是提高程序性能的有效手段之一,可是使用多线程却须要十分谨慎当心,若是不了解背后的执行机制以及使用的注意事项,极可能引发严重的问题。

    2. Understanding Android Threading

    一般来讲,一个线程须要经历三个生命阶段:开始,执行,结束。线程会在任务执行完毕以后结束,那么为了确保线程的存活,咱们会在执行阶段给线程赋予不一样的任务,而后在里面添加退出的条件从而确保任务可以执行完毕后退出。
    腾讯bugly
    在不少时候,线程不只仅是线性执行一系列的任务就结束那么简单的,咱们会须要增长一个任务队列,让线程不断的从任务队列中获取任务去进行执行,另外咱们还可能在线程执行的任务过程当中与其余的线程进行协做。若是这些细节都交给咱们本身来处理,这将会是件极其繁琐又容易出错的事情。
    腾讯bugly
    所幸的是,Android 系统为咱们提供了 Looper,Handler,MessageQueue 来帮助实现上面的线程任务模型:
    Looper: 可以确保线程持续存活而且能够不断的从任务队列中获取任务并进行执行。
    腾讯bugly
    Handler: 可以帮助实现队列任务的管理,不只仅可以把任务插入到队列的头部,尾部,还能够按照必定的时间延迟来确保任务从队列中可以来得及被取消掉。
    腾讯bugly
    MessageQueue: 使用 Intent,Message,Runnable 做为任务的载体在不一样的线程之间进行传递。
    腾讯bugly
    把上面三个组件打包到一块儿进行协做,这就是 HandlerThread
    咱们知道,当程序被启动,系统会帮忙建立进程以及相应的主线程,而这个主线程其实就是一个 HandlerThread。这个主线程会须要处理系统事件,输入事件,系统回调的任务,UI绘制等等任务,为了不主线程任务太重,咱们就会须要不断的开启新的工做线程来处理那些子任务。

    3. Memory & Threading

    增长并发的线程数会致使内存消耗的增长,平衡好这二者的关系是很是重要的。咱们知道,多线程并发访问同一块内存区域有可能带来不少问题,例如读写的权限争夺问题,ABA 问题等等。为了解决这些问题,咱们会须要引入锁的概念。
    在 Android 系统中也没法避免由于多线程的引入而致使出现诸如上文提到的种种问题。Android UI 对象的建立,更新,销毁等等操做都默认是执行在主线程,可是若是咱们在非主线程对UI对象进行操做,程序将可能出现异常甚至是崩溃。
    腾讯bugly
    另外,在非 UI 线程中直接持有 UI 对象的引用也极可能出现问题。例如Work线程中持有某个 UI 对象的引用,在 Work 线程执行完毕以前,UI 对象在主线程中被从 ViewHierarchy 中移除了,这个时候 UI 对象的任何属性都已经再也不可用了,另外对这个 UI 对象的更新操做也都没有任何意义了,由于它已经从 ViewHierarchy 中被移除,再也不绘制到画面上了。
    腾讯bugly
    不只如此,View 对象自己对所属的 Activity 是有引用关系的,若是工做线程持续保有 View 的引用,这就可能致使 Activity 没法彻底释放。除了直接显式的引用关系可能致使内存泄露以外,咱们还须要特别留意隐式的引用关系也可能致使泄露。例如一般咱们会看到在 Activity 里面定义的一个 AsyncTask,这种类型的 AsyncTask 与外部的 Activity 是存在隐式引用关系的,只要 Task 没有结束,引用关系就会一直存在,这很容易致使 Activity 的泄漏。更糟糕的状况是,它不只仅发生了内存泄漏,还可能致使程序异常或者崩溃。
    腾讯bugly
    为了解决上面的问题,咱们须要谨记的原则就是:不要在任何非 UI 线程里面去持有 UI 对象的引用。系统为了确保全部的 UI 对象都只会被 UI 线程所进行建立,更新,销毁的操做,特意设计了对应的工做机制(当 Activity 被销毁的时候,由该 Activity 所触发的非 UI 线程都将没法对UI对象进行操做,否者就会抛出程序执行异常的错误)来防止 UI 对象被错误的使用。

    4. Good AsyncTask Hunting

    AsyncTask 是一个让人既爱又恨的组件,它提供了一种简便的异步处理机制,可是它又同时引入了一些使人厌恶的麻烦。一旦对 AsyncTask 使用不当,极可能对程序的性能带来负面影响,同时还可能致使内存泄露。
    举个例子,常遇到的一个典型的使用场景:用户切换到某个界面,触发了界面上的图片的加载操做,由于图片的加载相对来讲耗时比较长,咱们须要在子线程中处理图片的加载,当图片在子线程中处理完成以后,再把处理好的图片返回给主线程,交给 UI 更新到画面上。
    腾讯bugly
    AsyncTask 的出现就是为了快速的实现上面的使用场景,AsyncTask 把在主线程里面的准备工做放到 onPreExecute()方法里面进行执行,doInBackground()方法执行在工做线程中,用来处理那些繁重的任务,一旦任务执行完毕,就会调用 onPostExecute()方法返回到主线程。
    腾讯bugly
    使用 AsyncTask 须要注意的问题有哪些呢?请关注如下几点:
    首先,默认状况下,全部的 AsyncTask 任务都是被线性调度执行的,他们处在同一个任务队列当中,按顺序逐个执行。假设你按照顺序启动20个 AsyncTask,一旦其中的某个 AsyncTask 执行时间过长,队列中的其余剩余 AsyncTask 都处于阻塞状态,必须等到该任务执行完毕以后才可以有机会执行下一个任务。状况以下图所示:
    腾讯bugly
    为了解决上面提到的线性队列等待的问题,咱们可使用 AsyncTask.executeOnExecutor()强制指定 AsyncTask 使用线程池并发调度任务。
    腾讯bugly
    其次,如何才可以真正的取消一个 AsyncTask 的执行呢?咱们知道 AsyncTaks 有提供 cancel()的方法,可是这个方法实际上作了什么事情呢?线程自己并不具有停止正在执行的代码的能力,为了可以让一个线程更早的被销毁,咱们须要在 doInBackground()的代码中不断的添加程序是否被停止的判断逻辑,以下图所示:
    腾讯bugly
    一旦任务被成功停止,AsyncTask 就不会继续调用 onPostExecute(),而是经过调用 onCancelled()的回调方法反馈任务执行取消的结果。咱们能够根据任务回调到哪一个方法(是 onPostExecute 仍是 onCancelled)来决定是对 UI 进行正常的更新仍是把对应的任务所占用的内存进行销毁等。
    最后,使用 AsyncTask 很容易致使内存泄漏,一旦把 AsyncTask 写成 Activity 的内部类的形式就很容易由于 AsyncTask 生命周期的不肯定而致使 Activity 发生泄漏。
    腾讯bugly
    综上所述,AsyncTask 虽然提供了一种简单便捷的异步机制,可是咱们仍是颇有必要特别关注到他的缺点,避免出现由于使用错误而致使的严重系统性能问题。

    5. Getting a HandlerThread

    大多数状况下,AsyncTask 都可以知足多线程并发的场景须要(在工做线程执行任务并返回结果到主线程),可是它并非万能的。例如打开相机以后的预览帧数据是经过 onPreviewFrame()的方法进行回调的,onPreviewFrame()open()相机的方法是执行在同一个线程的。
    腾讯bugly
    若是这个回调方法执行在 UI 线程,那么在 onPreviewFrame()里面将要执行的数据转换操做将和主线程的界面绘制,事件传递等操做争抢系统资源,这就有可能影响到主界面的表现性能。
    腾讯bugly
    咱们须要确保 onPreviewFrame()执行在工做线程。若是使用 AsyncTask,会由于 AsyncTask 默认的线性执行的特性(即便换成并发执行)会致使由于没法把任务及时传递给工做线程而致使任务在主线程中被延迟,直到工做线程空闲,才能够把任务切换到工做线程中进行执行。
    腾讯bugly
    因此咱们须要的是一个执行在工做线程,同时又可以处理队列中的复杂任务的功能,而 HandlerThread 的出现就是为了实现这个功能的,它组合了 Handler,MessageQueue,Looper 实现了一个长时间运行的线程,不断的从队列中获取任务进行执行的功能。
    腾讯bugly
    回到刚才的处理相机回调数据的例子,使用 HandlerThread 咱们能够把 open()操做与 onPreviewFrame()的操做执行在同一个线程,同时还避免了 AsyncTask 的弊端。若是须要在 onPreviewFrame()里面更新 UI,只须要调用 runOnUiThread()方法把任务回调给主线程就够了。
    腾讯bugly
    HandlerThread 比较合适处理那些在工做线程执行,须要花费时间偏长的任务。咱们只须要把任务发送给 HandlerThread,而后就只须要等待任务执行结束的时候通知返回到主线程就行了。
    另外很重要的一点是,一旦咱们使用了 HandlerThread,须要特别注意给 HandlerThread 设置不一样的线程优先级,CPU 会根据设置的不一样线程优先级对全部的线程进行调度优化。
    腾讯bugly
    掌握 HandlerThread 与 AsyncTask 之间的优缺点,能够帮助咱们选择合适的方案。

    6. Swimming in Threadpools

    线程池适合用在把任务进行分解,并发进行执行的场景。一般来讲,系统里面会针对不一样的任务设置一个单独的守护线程用来专门处理这项任务。例如使用 Networking Thread 用来专门处理网络请求的操做,使用 IO Thread 用来专门处理系统的 I\O 操做。针对那些场景,这样设计是没有问题的,由于对应的任务单次执行的时间并不长并且能够是顺序执行的。可是这种专属的单线程并不能知足全部的状况,例如咱们须要一次性 decode 40张图片,每一个线程须要执行 4ms 的时间,若是咱们使用专属单线程的方案,全部图片执行完毕会须要花费 160ms(40*4),可是若是咱们建立10个线程,每一个线程执行4个任务,那么咱们就只须要16ms就可以把全部的图片处理完毕。
    腾讯bugly
    为了可以实现上面的线程池模型,系统为咱们提供了 ThreadPoolExecutor 帮助类来简化实现,剩下须要作的就只是对任务进行分解就行了。
    腾讯bugly
    使用线程池须要特别注意同时并发线程数量的控制,理论上来讲,咱们能够设置任意你想要的并发数量,可是这样作很是的很差。由于 CPU 只能同时执行固定数量的线程数,一旦同时并发的线程数量超过 CPU 可以同时执行的阈值,CPU 就须要花费精力来判断到底哪些线程的优先级比较高,须要在不一样的线程之间进行调度切换。
    腾讯bugly
    一旦同时并发的线程数量达到必定的量级,这个时候 CPU 在不一样线程之间进行调度的时间就可能过长,反而致使性能严重降低。另外须要关注的一点是,每开一个新的线程,都会耗费至少 64K+ 的内存。为了可以方便的对线程数量进行控制,ThreadPoolExecutor 为咱们提供了初始化的并发线程数量,以及最大的并发数量进行设置。
    腾讯bugly
    另外须要关注的一个问题是:Runtime.getRuntime().availableProcesser()方法并不可靠,他返回的值并非真实的 CPU 核心数,由于 CPU 会在某些状况下选择对部分核心进行睡眠处理,在这种状况下,返回的数量就只能是激活的 CPU 核心数。

    7. The Zen of IntentService

    默认的 Service 是执行在主线程的,但是一般状况下,这很容易影响到程序的绘制性能(抢占了主线程的资源)。除了前面介绍过的 AsyncTask 与 HandlerThread,咱们还能够选择使用 IntentService 来实现异步操做。IntentService 继承自普通 Service 同时又在内部建立了一个 HandlerThread,在 onHandlerIntent()的回调里面处理扔到 IntentService 的任务。因此 IntentService 就不只仅具有了异步线程的特性,还同时保留了 Service 不受主页面生命周期影响的特色。
    腾讯bugly
    如此一来,咱们能够在 IntentService 里面经过设置闹钟间隔性的触发异步任务,例如刷新数据,更新缓存的图片或者是分析用户操做行为等等,固然处理这些任务须要当心谨慎。
    使用 IntentService 须要特别留意如下几点:
  • 首先,由于 IntentService 内置的是 HandlerThread 做为异步线程,因此每个交给 IntentService 的任务都将以队列的方式逐个被执行到,一旦队列中有某个任务执行时间过长,那么就会致使后续的任务都会被延迟处理。
  • 其次,一般使用到 IntentService 的时候,咱们会结合使用 BroadcastReceiver 把工做线程的任务执行结果返回给主 UI 线程。使用广播容易引发性能问题,咱们可使用 LocalBroadcastManager 来发送只在程序内部传递的广播,从而提高广播的性能。咱们也可使用 runOnUiThread() 快速回调到主 UI 线程。
  • 最后,包含正在运行的 IntentService 的程序相比起纯粹的后台程序更不容易被系统杀死,该程序的优先级是介于前台程序与纯后台程序之间的。

    8. Threading and Loaders

    当启动工做线程的 Activity 被销毁的时候,咱们应该作点什么呢?为了方便的控制工做线程的启动与结束,Android 为咱们引入了 Loader 来解决这个问题。咱们知道 Activity 有可能由于用户的主动切换而频繁的被建立与销毁,也有多是由于相似屏幕发生旋转等被动缘由而销毁再重建。在 Activity 不停的建立与销毁的过程中,颇有可能由于工做线程持有 Activity 的 View 而致使内存泄漏(由于工做线程极可能持有 View 的强引用,另外工做线程的生命周期还没法保证和 Activity 的生命周期一致,这样就容易发生内存泄漏了)。除了可能引发内存泄漏以外,在 Activity 被销毁以后,工做线程还继续更新视图是没有意义的,由于此时视图已经不在界面上显示了。
    腾讯bugly
    Loader 的出现就是为了确保工做线程可以和 Activity 的生命周期保持一致,同时避免出现前面提到的问题。
    腾讯bugly
    LoaderManager 会对查询的操做进行缓存,只要对应 Cursor 上的数据源没有发生变化,在配置信息发生改变的时候(例如屏幕的旋转),Loader 能够直接把缓存的数据回调到 onLoadFinished(),从而避免从新查询数据。另外系统会在 Loader 再也不须要使用到的时候(例如使用 Back 按钮退出当前页面)回调 onLoaderReset()方法,咱们能够在这里作数据的清除等等操做。
    在 Activity 或者 Fragment 中使用 Loader 能够方便的实现异步加载的框架,Loader 有诸多优势。可是实现 Loader 的这套代码仍是稍微有点点复杂,Android 官方为咱们提供了使用 Loader 的示例代码进行参考学习。

    9. The Importance of Thread Priority

    理论上来讲,咱们的程序能够建立出很是多的子线程一块儿并发执行的,但是基于 CPU 时间片轮转调度的机制,不可能全部的线程均可以同时被调度执行,CPU 须要根据线程的优先级赋予不一样的时间片。
    腾讯bugly
    Android 系统会根据当前运行的可见的程序和不可见的后台程序对线程进行归类,划分为 forground 的那部分线程会大体占用掉 CPU 的90%左右的时间片,background 的那部分线程就总共只能分享到5%-10%左右的时间片。之因此设计成这样是由于 forground 的程序自己的优先级就更高,理应获得更多的执行时间。
    腾讯bugly
    默认状况下,新建立的线程的优先级默认和建立它的母线程保持一致。若是主 UI 线程建立出了几十个工做线程,这些工做线程的优先级就默认和主线程保持一致了,为了避免让新建立的工做线程和主线程抢占 CPU 资源,须要把这些线程的优先级进行下降处理,这样才能给帮组 CPU 识别主次,提升主线程所能获得的系统资源。
    腾讯bugly
    在 Android 系统里面,咱们能够经过 android.os.Process.setThreadPriority(int) 设置线程的优先级,参数范围从-20到19,数值越小优先级越高。Android 系统还为咱们提供了如下的一些预设值,咱们能够经过给不一样的工做线程设置不一样数值的优先级来达到更细粒度的控制。
    腾讯bugly
    大多数状况下,新建立的线程优先级会被设置为默认的0,主线程设置为0的时候,新建立的线程还能够利用 THREAD_PRIORITY_LESS_FAVORABLE 或者 THREAD_PRIORITY_MORE_FAVORABLE 来控制线程的优先级。
    腾讯bugly
    Android 系统里面的 AsyncTask 与 IntentService已经默认帮助咱们设置线程的优先级,可是对于那些非官方提供的多线程工具类,咱们须要特别留意根据须要本身手动来设置线程的优先级。
    腾讯bugly
    腾讯bugly

    10. Profile GPU Rendering : M Update

    从 Android M 系统开始,系统更新了 GPU Profiling 的工具来帮助咱们定位 UI 的渲染性能问题。早期的 CPU Profiling 工具只能粗略的显示出 Process,Execute,Update 三大步骤的时间耗费状况。
    腾讯bugly
    可是仅仅显示三大步骤的时间耗费状况,仍是不太可以清晰帮助咱们定位具体的程序代码问题,因此在 Android M 版本开始,GPU Profiling 工具把渲染操做拆解成以下8个详细的步骤进行显示。
    腾讯bugly
    旧版本中提到的 Proces,Execute,Update 仍是继续获得了保留,他们的对应关系以下:
    腾讯bugly
    接下去咱们看下其余五个步骤分别表明了什么含义:
  • Sync & Upload:一般表示的是准备当前界面上有待绘制的图片所耗费的时间,为了减小该段区域的执行时间,咱们能够减小屏幕上的图片数量或者是缩小图片自己的大小。
  • Measure & Layout:这里表示的是布局的 onMeasure 与 onLayout 所花费的时间,一旦时间过长,就须要仔细检查本身的布局是否是存在严重的性能问题。
  • Animation:表示的是计算执行动画所须要花费的时间,包含的动画有 ObjectAnimator,ViewPropertyAnimator,Transition 等等。一旦这里的执行时间过长,就须要检查是否是使用了非官方的动画工具或者是检查动画执行的过程当中是否是触发了读写操做等等。
  • Input Handling:表示的是系统处理输入事件所耗费的时间,粗略等于对于的事件处理方法所执行的时间。一旦执行时间过长,意味着在处理用户的输入事件的地方执行了复杂的操做。
  • Misc/Vsync Delay:若是稍加注意,咱们能够在开发应用的 Log 日志里面看到这样一行提示:I/Choreographer(691): Skipped XXX frames! The application may be doing too much work on its main thread。这意味着咱们在主线程执行了太多的任务,致使 UI 渲染跟不上 vSync 的信号而出现掉帧的状况。
    上面八种不一样的颜色区分了不一样的操做所耗费的时间,为了便于咱们迅速找出那些有问题的步骤,GPU Profiling 工具会显示 16ms 的阈值线,这样就很容易找出那些不合理的性能问题,再仔细看对应具体哪一个步骤相对来讲耗费时间比例更大,结合上面介绍的细化步骤,从而快速定位问题,修复问题。

若是你以为内容意犹未尽,若是你想了解更多相关信息,请扫描如下二维码,关注咱们的公众帐号,能够获取更多技术类干货,还有精彩活动与你分享~缓存

腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的状况以及解决方案。智能合并功能帮助开发同窗把天天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同窗定位到出问题的代码行,实时上报能够在发布后快速的了解应用的质量状况,适配最新的 iOS, Android 官方操做系统,鹅厂的工程师都在使用,快来加入咱们吧!markdown

相关文章
相关标签/搜索