Skip to content

Latest commit

 

History

History
129 lines (75 loc) · 13.8 KB

Android_ThreadBase.md

File metadata and controls

129 lines (75 loc) · 13.8 KB

为什么需要多线程

Android程序的大多数代码操作都必须执行在主线程, 例如系统事件(例如设备屏幕发生旋转),输入事件(例如用户点击滑动等),程序回调服务,UI绘制以及闹钟事件等等。在上述事件或者方法中插入的代码也将执行在主线程。 一旦我们在主线程里面添加了操作复杂的代码,这些代码就很可能阻碍主线程去响应点击/滑动事件,阻碍主线程的UI绘制等等

为了让屏幕的刷新帧率达到60fps,我们需要确保16ms内完成单次刷新的操作。 一旦我们在主线程里面执行的任务过于繁重就可能导致接收到刷新信号的时候因为资源被占用而无法完成这次刷新操作, 这样就会产生掉帧的现象,刷新帧率自然也就跟着下降了(一旦刷新帧率降到20fps左右,用户就可以明显感知到卡顿不流畅了)。

为了避免上面提到的掉帧问题,我们需要使用多线程的技术方案,把那些操作复杂的任务移动到其他线程当中执行,这样就不容易阻塞主线程的操作,也就减小了出现掉帧的可能性。

Android 中的多线程

那么问题来了,为主线程减负的多线程方案有哪些呢?这些方案分别适合在什么场景下使用?

Android系统为我们提供了若干组工具类来帮助解决这个问题。

  • AsyncTask: 为UI线程与工作线程之间进行 快速的切换 提供一种简单便捷的机制。适用于当下立即需要启动,但是异步执行的生命周期短暂的使用场景。

  • HandlerThread: 为 某些回调方法或者等待某些任务的执行设置一个专属的线程, 并提供线程任务的调度机制。

  • ThreadPool: 把任务分解成不同的单元, 分发到各个不同的线程上进行同时 并发处理

  • IntentService: 适合于执行由 UI 触发的后台 Service 任务,并可以把 后台任务 执行的情况通过一定的机制反馈给UI。

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

通常来说,一个线程需要经历三个生命阶段:开始,执行,结束。线程会在任务执行完毕之后结束,那么为了确保线程的存活,我们会在执行阶段给线程赋予不同的任务, 然后在里面添加退出的条件从而确保任务能够执行完毕后退出。在很多时候,线程不仅仅是线性执行一系列的任务就结束那么简单的,我们会需要增加一个任务队列, 让线程不断的从任务队列中获取任务去进行执行,另外我们还可能在线程执行的任务过程中与其他的线程进行协作。如果这些细节都交给我们自己来处理,这将会是 件极其繁琐又容易出错的事情。

Android 系统为我们提供了 Looper,Handler 和 MessageQueue, 这三个组件来帮助实现上面的线程任务模型:

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

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

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

把上面三个组件打包到一起进行协作,这就是 HandlerThread。

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

AsyncTask

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

常遇到的一个典型的使用场景: 用户切换到某个界面, 触发了界面上的图片的加载操作. 因为图片的加载相对来说耗时比较长, 我们需要在子线程中处理图片的加载.当图片在子线程中处理完成之后, 再把处理好的图片返回给主线程,交给 UI 更新到画面上。

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

使用 AsyncTask 需要注意以下几点问题:

  • 默认情况下, 所有的 AsyncTask 任务都是被线性调度执行的, 它们处在同一个任务队列当中, 按顺序逐个执行。假设你按照顺序启动 20 个 AsyncTask, 一旦其中 的某个 AsyncTask 执行时间过长, 队列中的其它剩余 AsyncTask 都处于阻塞状态, 必须等到该任务执行完毕之后才能够有机会执行下一个任务。为了解决线性队列 等待的问题,我们可以使用 AsyncTask.executeOnExecutor() 强制指定 AsyncTask 使用线程池并发调度任务。

  • 如何才能够真正的取消一个 AsyncTask 的执行呢?我们知道 AsyncTaks 有提供 cancel() 的方法. 但但是线程本身并不具备中止正在执行的代码的能力, 为了能够让一个线程更早的被销毁, 我们需要在 doInBackground() 的代码中不断的添加程序是否被中止的判断逻辑,如下所示:

   protected String doInBackground(Integer... integers) {
        if (isCancelled()){
            return null;
        }
        DelayOperator delayOperator = new DelayOperator();
        int i = 0;
        for (i = 10; i < 100; i++){
            delayOperator.delay();
            publishProgress(i);
        }
        return i + integers[0].intValue() + "";
    }
  • 使用 AsyncTask 很容易导致内存泄漏,一旦把 AsyncTask 写成 Activity 的内部类的形式就很容易因为 AsyncTask 生命周期的不确定而导致 Activity 发生 泄漏。

综上所述,AsyncTask 虽然提供了一种简单便捷的异步机制,但是我们还是很有必要特别关注到它的缺点,避免出现因为使用错误而导致的严重系统性能问题。

HandlerThread

大多数情况下,AsyncTask 都能够满足多线程并发的场景需要(在工作线程执行任务并返回结果到主线程),但是它并不是万能的。例如打开相机之后的预览帧数据是通过 onPreviewFrame()的方法进行回调的,onPreviewFrame() 和 open() 相机的方法是执行在同一个线程的。

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

我们需要确保 onPreviewFrame() 执行在工作线程。如果使用 AsyncTask,会因为 AsyncTask 默认的线性执行的特性(即使换成并发执行)会导致因为无法把任务及时传递给工作线程而导致任务在主线程中被延迟,直到工作线程空闲,才可以把任务切换到工作线程中进行执行。

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

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

一旦我们使用了 HandlerThread,需要特别注意给 HandlerThread 设置不同的线程优先级, CPU 会根据设置的不同线程优先级对所有的线程进行调度优化。

Threadpools

线程池适合用在把任务进行分解, 并发进行执行的场景。

通常来说,系统里面会针对不同的任务设置一个单独的守护线程用来专门处理这项任务。例如使用 Networking Thread 用来专门处理网络请求的操作,使用 IO Thread 用来专门处理系统的I\O操作。针对那些场景,这样设计是没有问题的,因为对应的任务单次执行的时间并不长而且可以是顺序执行的。但是这种专属的单线 程并不能满足所有的情况,例如我们需要一次性 decode 40 张图片, 每个线程需要执行4ms的时间. 如果我们使用专属单线程的方案, 所有图片执行完毕会需要花费160ms(40*4), 但是如果我们创建 10 个线程,每个线程执行 4 个任务, 那么我们就只需要 16ms 就能够把所有的图片处理完毕。

为了能够实现上面的线程池模型,系统为我们提供了 ThreadPoolExecutor 帮助类来简化实现, 剩下需要做的就只是对任务进行分解就好了。

使用线程池需要特别注意同时并发线程数量的控制。理论上来说,我们可以设置任意你想要的并发数量,但是这样做非常的不好。因为CPU只能同时执行固定数量的线程数,一旦同时并发的线程数量超过CPU能够同时执行的阈值,CPU就需要花费精力来判断到底哪些线程的优先级比较高,需要在不同的线程之间进行调度切换,这个时候 CPU 在不同线程之间进行调度的时间就可能过长,反而导致性能严重下降。另外需要关注的一点是,每开一个新的线程,都会耗费至少64K+的内存。为了能够方便的对线程 数量进行控制,ThreadPoolExecutor 为我们提供了初始化的并发线程数量, 以及最大的并发数量进行设置。另外需要关注的一个问题是:Runtime.getRuntime().availableProcesser()方法并不可靠, 它返回的值并不是真实的 CPU 核心数. 因为CPU会在某些情况下选择对部分核心进行睡眠处理,在这种情况下,返回的数量就只能是激活的 CPU 核心数。

IntentService

默认的 Service 是执行在主线程的,可是通常情况下,这很容易影响到程序的绘制性能(抢占了主线程的资源)。IntentService 继承 Service 同时又在内部创建了一个 HandlerThread. 在 onHandlerIntent() 的回调里面处理扔到 IntentService 的任务。所以 IntentService 就不仅仅具备了异步线程的特性,还同时保留了 Service 不受页面生命周期影响的特点。

如此一来,我们可以在 IntentService 里面通过设置闹钟间隔性的触发异步任务,例如刷新数据,更新缓存的图片或者是分析用户操作行为等等,当然处理这些任务需要小心谨慎。

使用 IntentService 需注意以下几点:

  • 因为 IntentService 内置的是 HandlerThread 作为异步线程,所以每一个交给 IntentService 的任务都将以队列的方式逐个被执行到, 一旦队列中有某个任 务执行时间过长,那么就会导致后续的任务都会被延迟处理。

  • 通常使用到 IntentService 的时候,我们会结合 BroadcastReceiver 把工作线程的任务执行结果返回给主 UI 线程。使用广播容易引起性能问题,我们可以使 用 LocalBroadcastManager 来发送只在程序内部传递的广播, 从而提升广播的性能。也可以使用 runOnUiThread() 快速回调到主UI线程。

  • 包含正在运行的 IntentService 的程序相比起纯粹的后台程序更不容易被系统杀死,该程序的优先级是介于前台程序与纯后台程序之间的。

Thread Priority

程序可以创建出非常多的子线程一起并发执行, 可是基于 CPU 时间片轮转调度的机制, 不可能所有的线程都可以同时被调度执行, CPU 需要根据线程的优先级赋予不同的时间片。

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

默认情况下,新创建的线程的优先级默认和创建它的母线程保持一致

如果主UI线程创建出了几十个工作线程,这些工作线程的优先级就默认和主线程保持一致了,为了不让新创建的工作线程和主线程抢占CPU资源,需要把这些线程的优先级进行降低处理, 这样才能给帮助 CPU 识别主次, 提高主线程所能得到的系统资源。

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

Android性能优化典范-第5季 Android Developer