Android性能优化典范 - 第5季

这是Android性能优化典范第5季的课程学习笔记,拖拖拉拉好久,记录分享给你们,请多多包涵担待指正!文章共10个段落,涉及的内容有:多线程并发的性能问题,介绍了AsyncTask,HandlerThread,IntentService与ThreadPool分别适合的使用场景以及各自的使用注意事项,这是一篇了解Android多线程编程不可多得的基础文章,清楚的了解这些Android系统提供的多线程基础组件之间的差别以及优缺点,才可以在项目实战中作出最恰当的选择。html

1)Threading Performance

在程序开发的实践当中,为了让程序表现得更加流畅,咱们确定会须要使用到多线程来提高程序的并发执行性能。可是编写多线程并发的代码一直以来都是一个相对棘手的问题,因此想要得到更佳的程序性能,咱们很是有必要掌握多线程并发编程的基础技能。android

众所周知,Android程序的大多数代码操做都必须执行在主线程,例如系统事件(例如设备屏幕发生旋转),输入事件(例如用户点击滑动等),程序回调服务,UI绘制以及闹钟事件等等。那么咱们在上述事件或者方法中插入的代码也将执行在主线程。编程

android_perf_5_threading_main_thread

一旦咱们在主线程里面添加了操做复杂的代码,这些代码就极可能阻碍主线程去响应点击/滑动事件,阻碍主线程的UI绘制等等。咱们知道,为了让屏幕的刷新帧率达到60fps,咱们须要确保16ms内完成单次刷新的操做。一旦咱们在主线程里面执行的任务过于繁重就可能致使接收到刷新信号的时候由于资源被占用而没法完成此次刷新操做,这样就会产生掉帧的现象,刷新帧率天然也就跟着降低了(一旦刷新帧率降到20fps左右,用户就能够明显感知到卡顿不流畅了)。缓存

android_perf_5_threading_dropframe

为了不上面提到的掉帧问题,咱们须要使用多线程的技术方案,把那些操做复杂的任务移动到其余线程当中执行,这样就不容易阻塞主线程的操做,也就减少了出现掉帧的可能性。性能优化

android_perf_5_threading_workthread

那么问题来了,为主线程减轻负的多线程方案有哪些呢?这些方案分别适合在什么场景下使用?Android系统为咱们提供了若干组工具类来帮助解决这个问题。网络

  • AsyncTask: 为UI线程与工做线程之间进行快速的切换提供一种简单便捷的机制。适用于当下当即须要启动,可是异步执行的生命周期短暂的使用场景。
  • HandlerThread: 为某些回调方法或者等待某些任务的执行设置一个专属的线程,并提供线程任务的调度机制。
  • ThreadPool: 把任务分解成不一样的单元,分发到各个不一样的线程上,进行同时并发处理。
  • IntentService: 适合于执行由UI触发的后台Service任务,并能够把后台任务执行的状况经过必定的机制反馈给UI。

了解这些系统提供的多线程工具类分别适合在什么场景下,能够帮助咱们选择合适的解决方案,避免出现不可预期的麻烦。虽然使用多线程能够提升程序的并发量,可是咱们须要特别注意由于引入多线程而可能伴随而来的内存问题。举个例子,在Activity内部定义的一个AsyncTask,它属于一个内部类,该类自己和外面的Activity是有引用关系的,若是Activity要销毁的时候,AsyncTask还仍然在运行,这会致使Activity没有办法彻底释放,从而引起内存泄漏。因此说,多线程是提高程序性能的有效手段之一,可是使用多线程却须要十分谨慎当心,若是不了解背后的执行机制以及使用的注意事项,极可能引发严重的问题。多线程

2)Understanding Android Threading

一般来讲,一个线程须要经历三个生命阶段:开始,执行,结束。线程会在任务执行完毕以后结束,那么为了确保线程的存活,咱们会在执行阶段给线程赋予不一样的任务,而后在里面添加退出的条件从而确保任务可以执行完毕后退出。并发

android_perf_5_thread_lifecycle

在不少时候,线程不只仅是线性执行一系列的任务就结束那么简单的,咱们会须要增长一个任务队列,让线程不断的从任务队列中获取任务去进行执行,另外咱们还可能在线程执行的任务过程当中与其余的线程进行协做。若是这些细节都交给咱们本身来处理,这将会是件极其繁琐又容易出错的事情。app

android_perf_5_thread_thread

所幸的是,Android系统为咱们提供了Looper,Handler,MessageQueue来帮助实现上面的线程任务模型:框架

Looper: 可以确保线程持续存活而且能够不断的从任务队列中获取任务并进行执行。

android_perf_5_thread_looper

Handler: 可以帮助实现队列任务的管理,不只仅可以把任务插入到队列的头部,尾部,还能够按照必定的时间延迟来确保任务从队列中可以来得及被取消掉。

android_perf_5_thread_handler

MessageQueue: 使用Intent,Message,Runnable做为任务的载体在不一样的线程之间进行传递。

android_perf_5_thread_messagequeue

把上面三个组件打包到一块儿进行协做,这就是HandlerThread

android_perf_5_thread_handlerthread

咱们知道,当程序被启动,系统会帮忙建立进程以及相应的主线程,而这个主线程其实就是一个HandlerThread。这个主线程会须要处理系统事件,输入事件,系统回调的任务,UI绘制等等任务,为了不主线程任务太重,咱们就会须要不断的开启新的工做线程来处理那些子任务。

3)Memory & Threading

增长并发的线程数会致使内存消耗的增长,平衡好这二者的关系是很是重要的。咱们知道,多线程并发访问同一块内存区域有可能带来不少问题,例如读写的权限争夺问题,ABA问题等等。为了解决这些问题,咱们会须要引入的概念。

在Android系统中也没法避免由于多线程的引入而致使出现诸如上文提到的种种问题。Android UI对象的建立,更新,销毁等等操做都默认是执行在主线程,可是若是咱们在非主线程对UI对象进行操做,程序将可能出现异常甚至是崩溃。

android_perf_5_memory_thread_update

另外,在非UI线程中直接持有UI对象的引用也极可能出现问题。例如Work线程中持有某个UI对象的引用,在Work线程执行完毕以前,UI对象在主线程中被从ViewHierarchy中移除了,这个时候UI对象的任何属性都已经再也不可用了,另外对这个UI对象的更新操做也都没有任何意义了,由于它已经从ViewHierarchy中被移除,再也不绘制到画面上了。

android_perf_5_memory_view_remove

不只如此,View对象自己对所属的Activity是有引用关系的,若是工做线程持续保有View的引用,这就可能致使Activity没法彻底释放。除了直接显式的引用关系可能致使内存泄露以外,咱们还须要特别留意隐式的引用关系也可能致使泄露。例如一般咱们会看到在Activity里面定义的一个AsyncTask,这种类型的AsyncTask与外部的Activity是存在隐式引用关系的,只要Task没有结束,引用关系就会一直存在,这很容易致使Activity的泄漏。更糟糕的状况是,它不只仅发生了内存泄漏,还可能致使程序异常或者崩溃。

android_perf_5_memory_asynctask

为了解决上面的问题,咱们须要谨记的原则就是:不要在任何非UI线程里面去持有UI对象的引用。系统为了确保全部的UI对象都只会被UI线程所进行建立,更新,销毁的操做,特意设计了对应的工做机制(当Activity被销毁的时候,由该Activity所触发的非UI线程都将没法对UI对象进行操做,否者就会抛出程序执行异常的错误)来防止UI对象被错误的使用。

4)Good AsyncTask Hunting

AsyncTask是一个让人既爱又恨的组件,它提供了一种简便的异步处理机制,可是它又同时引入了一些使人厌恶的麻烦。一旦对AsyncTask使用不当,极可能对程序的性能带来负面影响,同时还可能致使内存泄露。

举个例子,常遇到的一个典型的使用场景:用户切换到某个界面,触发了界面上的图片的加载操做,由于图片的加载相对来讲耗时比较长,咱们须要在子线程中处理图片的加载,当图片在子线程中处理完成以后,再把处理好的图片返回给主线程,交给UI更新到画面上。

android_perf_5_asynctask_main

AsyncTask的出现就是为了快速的实现上面的使用场景,AsyncTask把在主线程里面的准备工做放到onPreExecute()方法里面进行执行,doInBackground()方法执行在工做线程中,用来处理那些繁重的任务,一旦任务执行完毕,就会调用onPostExecute()方法返回到主线程。

android_perf_5_asynctask_mode

使用AsyncTask须要注意的问题有哪些呢?请关注如下几点:

  • 首先,默认状况下,全部的AsyncTask任务都是被线性调度执行的,他们处在同一个任务队列当中,按顺序逐个执行。假设你按照顺序启动20个AsyncTask,一旦其中的某个AsyncTask执行时间过长,队列中的其余剩余AsyncTask都处于阻塞状态,必须等到该任务执行完毕以后才可以有机会执行下一个任务。状况以下图所示:

android_perf_5_asynctask_single_queue

为了解决上面提到的线性队列等待的问题,咱们可使用AsyncTask.executeOnExecutor()强制指定AsyncTask使用线程池并发调度任务。

android_perf_5_asynctask_thread_pool

  • 其次,如何才可以真正的取消一个AsyncTask的执行呢?咱们知道AsyncTaks有提供cancel()的方法,可是这个方法实际上作了什么事情呢?线程自己并不具有停止正在执行的代码的能力,为了可以让一个线程更早的被销毁,咱们须要在doInBackground()的代码中不断的添加程序是否被停止的判断逻辑,以下图所示:

android_perf_5_asynctask_cancel

一旦任务被成功停止,AsyncTask就不会继续调用onPostExecute(),而是经过调用onCancelled()的回调方法反馈任务执行取消的结果。咱们能够根据任务回调到哪一个方法(是onPostExecute仍是onCancelled)来决定是对UI进行正常的更新仍是把对应的任务所占用的内存进行销毁等。

  • 最后,使用AsyncTask很容易致使内存泄漏,一旦把AsyncTask写成Activity的内部类的形式就很容易由于AsyncTask生命周期的不肯定而致使Activity发生泄漏。

android_perf_5_memory_asynctask

综上所述,AsyncTask虽然提供了一种简单便捷的异步机制,可是咱们仍是颇有必要特别关注到他的缺点,避免出现由于使用错误而致使的严重系统性能问题。

5)Getting a HandlerThread

大多数状况下,AsyncTask都可以知足多线程并发的场景须要(在工做线程执行任务并返回结果到主线程),可是它并非万能的。例如打开相机以后的预览帧数据是经过onPreviewFrame()的方法进行回调的,onPreviewFrame()open()相机的方法是执行在同一个线程的。

android_perf_5_handlerthread_camera_open

若是这个回调方法执行在UI线程,那么在onPreviewFrame()里面将要执行的数据转换操做将和主线程的界面绘制,事件传递等操做争抢系统资源,这就有可能影响到主界面的表现性能。

android_perf_5_handlerthread_main_thread2

咱们须要确保onPreviewFrame()执行在工做线程。若是使用AsyncTask,会由于AsyncTask默认的线性执行的特性(即便换成并发执行)会致使由于没法把任务及时传递给工做线程而致使任务在主线程中被延迟,直到工做线程空闲,才能够把任务切换到工做线程中进行执行。

android_perf_5_handlerthread_asynctask

因此咱们须要的是一个执行在工做线程,同时又可以处理队列中的复杂任务的功能,而HandlerThread的出现就是为了实现这个功能的,它组合了Handler,MessageQueue,Looper实现了一个长时间运行的线程,不断的从队列中获取任务进行执行的功能。

android_perf_5_handlerthread_outline

回到刚才的处理相机回调数据的例子,使用HandlerThread咱们能够把open()操做与onPreviewFrame()的操做执行在同一个线程,同时还避免了AsyncTask的弊端。若是须要在onPreviewFrame()里面更新UI,只须要调用runOnUiThread()方法把任务回调给主线程就够了。

android_perf_5_handlerthread_camera

HandlerThread比较合适处理那些在工做线程执行,须要花费时间偏长的任务。咱们只须要把任务发送给HandlerThread,而后就只须要等待任务执行结束的时候通知返回到主线程就行了。

另外很重要的一点是,一旦咱们使用了HandlerThread,须要特别注意给HandlerThread设置不一样的线程优先级,CPU会根据设置的不一样线程优先级对全部的线程进行调度优化。

android_perf_5_handlerthread_priority

掌握HandlerThread与AsyncTask之间的优缺点,能够帮助咱们选择合适的方案。

6)Swimming in Threadpools

线程池适合用在把任务进行分解,并发进行执行的场景。一般来讲,系统里面会针对不一样的任务设置一个单独的守护线程用来专门处理这项任务。例如使用Networking Thread用来专门处理网络请求的操做,使用IO Thread用来专门处理系统的I\O操做。针对那些场景,这样设计是没有问题的,由于对应的任务单次执行的时间并不长并且能够是顺序执行的。可是这种专属的单线程并不能知足全部的状况,例如咱们须要一次性decode 40张图片,每一个线程须要执行4ms的时间,若是咱们使用专属单线程的方案,全部图片执行完毕会须要花费160ms(40*4),可是若是咱们建立10个线程,每一个线程执行4个任务,那么咱们就只须要16ms就可以把全部的图片处理完毕。

android_perf_5_threadpool_1

为了可以实现上面的线程池模型,系统为咱们提供了ThreadPoolExecutor帮助类来简化实现,剩下须要作的就只是对任务进行分解就行了。

android_perf_5_threadpool_2

使用线程池须要特别注意同时并发线程数量的控制,理论上来讲,咱们能够设置任意你想要的并发数量,可是这样作很是的很差。由于CPU只能同时执行固定数量的线程数,一旦同时并发的线程数量超过CPU可以同时执行的阈值,CPU就须要花费精力来判断到底哪些线程的优先级比较高,须要在不一样的线程之间进行调度切换。

android_perf_5_threadpool_3

一旦同时并发的线程数量达到必定的量级,这个时候CPU在不一样线程之间进行调度的时间就可能过长,反而致使性能严重降低。另外须要关注的一点是,每开一个新的线程,都会耗费至少64K+的内存。为了可以方便的对线程数量进行控制,ThreadPoolExecutor为咱们提供了初始化的并发线程数量,以及最大的并发数量进行设置。

android_perf_5_threadpool_4

另外须要关注的一个问题是:Runtime.getRuntime().availableProcesser()方法并不可靠,他返回的值并非真实的CPU核心数,由于CPU会在某些状况下选择对部分核心进行睡眠处理,在这种状况下,返回的数量就只能是激活的CPU核心数。

7)The Zen of IntentService

默认的Service是执行在主线程的,但是一般状况下,这很容易影响到程序的绘制性能(抢占了主线程的资源)。除了前面介绍过的AsyncTask与HandlerThread,咱们还能够选择使用IntentService来实现异步操做。IntentService继承自普通Service同时又在内部建立了一个HandlerThread,在onHandlerIntent()的回调里面处理扔到IntentService的任务。因此IntentService就不只仅具有了异步线程的特性,还同时保留了Service不受主页面生命周期影响的特色。

android_perf_5_intentservice_outline

如此一来,咱们能够在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被销毁以后,工做线程还继续更新视图是没有意义的,由于此时视图已经不在界面上显示了。

android_perf_5_loader_bad

Loader的出现就是为了确保工做线程可以和Activity的生命周期保持一致,同时避免出现前面提到的问题。

android_perf_5_loader_good

LoaderManager会对查询的操做进行缓存,只要对应Cursor上的数据源没有发生变化,在配置信息发生改变的时候(例如屏幕的旋转),Loader能够直接把缓存的数据回调到onLoadFinished(),从而避免从新查询数据。另外系统会在Loader再也不须要使用到的时候(例如使用Back按钮退出当前页面)回调onLoaderReset()方法,咱们能够在这里作数据的清除等等操做。

在Activity或者Fragment中使用Loader能够方便的实现异步加载的框架,Loader有诸多优势。可是实现Loader的这套代码仍是稍微有点点复杂,Android官方为咱们提供了使用Loader的示例代码进行参考学习。

9)The Importance of Thread Priority

理论上来讲,咱们的程序能够建立出很是多的子线程一块儿并发执行的,但是基于CPU时间片轮转调度的机制,不可能全部的线程均可以同时被调度执行,CPU须要根据线程的优先级赋予不一样的时间片。

android_perf_5_threadpriority_CPU

Android系统会根据当前运行的可见的程序和不可见的后台程序对线程进行归类,划分为forground的那部分线程会大体占用掉CPU的90%左右的时间片,background的那部分线程就总共只能分享到5%-10%左右的时间片。之因此设计成这样是由于forground的程序自己的优先级就更高,理应获得更多的执行时间。

android_perf_5_threadpriority_90

默认状况下,新建立的线程的优先级默认和建立它的母线程保持一致。若是主UI线程建立出了几十个工做线程,这些工做线程的优先级就默认和主线程保持一致了,为了避免让新建立的工做线程和主线程抢占CPU资源,须要把这些线程的优先级进行下降处理,这样才能给帮组CPU识别主次,提升主线程所能获得的系统资源。

android_perf_5_threadpriority_less

在Android系统里面,咱们能够经过android.os.Process.setThreadPriority(int)设置线程的优先级,参数范围从-20到24,数值越小优先级越高。Android系统还为咱们提供了如下的一些预设值,咱们能够经过给不一样的工做线程设置不一样数值的优先级来达到更细粒度的控制。

android_perf_5_threadpriority_const

大多数状况下,新建立的线程优先级会被设置为默认的0,主线程设置为0的时候,新建立的线程还能够利用THREAD_PRIORITY_LESS_FAVORABLE或者THREAD_PRIORITY_MORE_FAVORABLE来控制线程的优先级。

android_perf_5_threadpriority_value

Android系统里面的AsyncTask与IntentService已经默认帮助咱们设置线程的优先级,可是对于那些非官方提供的多线程工具类,咱们须要特别留意根据须要本身手动来设置线程的优先级。

android_perf_5_threadpriority_asynctask android_perf_5_threadpriority_intentservice

10)Profile GPU Rendering : M Update

从Android M系统开始,系统更新了GPU Profiling的工具来帮助咱们定位UI的渲染性能问题。早期的CPU Profiling工具只能粗略的显示出Process,Execute,Update三大步骤的时间耗费状况。

android_perf_5_gpu_profiling_old

可是仅仅显示三大步骤的时间耗费状况,仍是不太可以清晰帮助咱们定位具体的程序代码问题,因此在Android M版本开始,GPU Profiling工具把渲染操做拆解成以下8个详细的步骤进行显示。

android_perf_5_gpu_profiling_8steps

旧版本中提到的Proces,Execute,Update仍是继续获得了保留,他们的对应关系以下:

android_perf_5_gpu_profiling_3steps

接下去咱们看下其余五个步骤分别表明了什么含义:

  • 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的阈值线,这样就很容易找出那些不合理的性能问题,再仔细看对应具体哪一个步骤相对来讲耗费时间比例更大,结合上面介绍的细化步骤,从而快速定位问题,修复问题。

相关文章
相关标签/搜索