深入理解Android中的线程及线程间通信

来源:互联网 发布:雷神3知乎 编辑:程序博客网 时间:2024/05/22 17:15

Android应用启动时会创建哪些线程

从一个问题开始本文,当启动一个应用时,会创建哪些线程?UI线程是肯定有的,那么还有没有其他线程呢?

在Android Studio中通过adb shell命令可以查看应用的进程与线程信息,操作之前,先明确几个概念:

  • UID——User ID,即用户id,在Android中,每个应用就代表一个用户,用户id在应用安装后就会分配。
  • PID——Process identifier,即进程id。
  • PPID——Parent process identifier,即父进程id,我们知道每个应用进程都派生自另一个进程,而Android中所有应用进程都派生自Zygote进程,也就是说Zygote进程是所有应用的父进程,待会儿我们也会证明这一事实。

针对个人的一个包名为me.geed.planner的应用,使用adb命令进行如下操作:

  1. 先查看Zygote的进程情况
  2. 再来看me.geed.planner应用的进程情况
  3. 最后打印me.geed.planner应用的线程信息

上述三步操作结果如下图所示:

这里写图片描述

其中第一列代表UID,第二列为PID,第三列为PPID,最后一列为Name。因此我们可以发现Zygote进程的PID为297。继续往下看,me.geed.planner应用的UID为u0_a228,其PID为22819,PPID为297,这就证明了Zygote是该应用的父进程。当然我这里只是打印了一个应用的信息,你也可以打印所有应用的进程信息,它们的父进程id都为297。再接着看图中应用的线程信息,第一个实际上是UI线程,可以发现后续几个线程都是UI线程的子线程,都是由UI线程派生出来的,由PPID都是22819可以证明。这里有一点需要理解即Linux中的进程与线程并没有严格意义上的区别,因此UI线程跟该应用的进程信息是一样的。

可以发现,应用启动时除了UI线程(UI线程在应用启动时创建并一直到进程结束),还创建了其他几个线程,如GC线程、Signal Catcher线程、Compiler线程及Binder线程等。Binder线程比较重要,是用于进程间通信的,Activity、Service等组件的启动均需要跟Binder线程打交道。

我们经常讨论到工作线程则是需要自己来创建启动的,而并非系统创建。

线程的调度

在Android中,应用的线程并不是由虚拟机来调度的,而是由Linux的标准调度器(Linux CFS)来调度的,因此线程不仅和自己应用中的线程竞争,也与其他app中的线程竞争。Linux CFS是指在内核2.6.23版本中推出的Completely Fair Scheduler,这款调度器不依赖于运行队列而是使用红黑树实现任务管理,更多资料可以参考这里。

Android中对线程的调度通常有优先级和线程组两种方式。

优先级

  • Java线程优先级

java.lang.Thread.setPriority(int priority);

参数取值范围为[0,10],优先级从低到高,默认优先级为5。

  • Linux线程优先级

android.os.Process.setThreadPriority(int priority)

android.os.Process.setThreadPriority(int threadId, int priority)

参数取值范围为[-20,19],优先级从高到低,默认优先级为0。

当然建议使用Android的线程优先级。

线程组

Android中主要的线程组有:Backgroud Group与Foreground Group。当app在前台运行,一般是在前台线程组,如果按下home键,app被切换到后台,则相应线程被切换到后台线程组。

应用中创建的线程默认跟UI线程在同一线程组并具有相同的优先级,UI线程的优先级为0。虽然UI线程更重要,但调度器不知道哪个是UI线程,哪个是我们创建的工作线程,因此UI线程在调度上并没有任何优势。所以,我们应该适当降低工作线程的优先级,使其归入到Backgroud Group。可利用Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)将线程归入后台线程组,以提高UI线程的性能。

利用adb命令查看线程组情况,如下图:

这里写图片描述

可以发现UI线程为fg,即表示前台线程组。

线程间的通信

Pipes

Pipes位于java.io包中,是java中的概念,它允许在同一进程中的两个线程之间进行单向通信。该机制遵循生产-消费模型。假设两个Thread分别为Producer线程和Consumer线程,则Producer负责写入数据,而Consumer线程负责读取数据,对应关系如下图:

这里写图片描述

pipe实质上是内存中分配的一块Buffer,只有通信的两个线程可以访问,其他线程不可以访问,并且利用pipe通信是单向的,因此能够保证线程安全。此Buffer的默认大小为1024kb,也可以自己配置。

利用Piles进行线程通讯只能传递二进制数据或字符数据。对于二进制数据的传输,Producer线程端借助PipedOutputStream、Consumer线程端借助PipedInputStream来完成;对于字符数据,Producer线程端借助PipedWriter、Consumer线程端借助PipedReader实现。

管道通信方式的生命周期起于读或写任一方发起的连接操作,当连接关闭时则终止。

由于此种通信方式实现了生产-消费模型,因此会产生阻塞,当pipe已经写满数据,此时Producer线程发生阻塞,不可再写入数据;当pipe中没有任何数据时,Consumer线程则发生阻塞,直到pipe里面被写入内容后才可以继续读取。因此,此种通信方式最好不要用在UI线程中,否则ANR就会成为你的噩梦。

Shared Memory

共享内存的通信方式就比较容易理解了,应用中的多个线程可以访问相同的一块地址空间,如果一个线程在共享内存中写入了数据,那么该数据就可以被其他线程读取,如下图所示:

这里写图片描述

Signaling

信号通知的机制比较抽象一些,但信号通知机制比共享内存的性能更高,它的原理是当状态值发生变化时,让某个线程通知其他线程。

举例来说明一下,例如当线程A需要等待线程B到达某个状态时才继续执行,此时线程A调用wait()/wait(timeout)或等价的await()/await(timeout),timeout参数即调用线程的等待时间。当线程B满足了需要的状态,则会调用notify()/notifyAll()或者等价的signal()/singalAll()。通过接收到B线程发出的信号,原来等待的A线程继续执行。

Blocking Queue

虽然信号机制可以满足大多数的线程通信需求,但这种方式容易出错,于是基于信号通知机制,Java抽象出一种更通用的单向通信机制,即利用阻塞队列来实现Producer线程与Consumer线程的通信。其原理如下图所示:

这里写图片描述

BlockingQueue作为两个线程之间的协调者,消费者线程按顺序读取阻塞队列中的数据。同样,当队列中数据已满时,生产者线程就不能写入数据了,当队列中没有数据时,消费者线程也不能读取数据。

Android Message mechanism

Android消息机制应该是Android开发中最常用的通信机制了,它利用Handler、Looper、MessageQueue等完美实现线程间的切换及数据的传递,在这里就不展开细说了,可以参考之前写的Android消息机制源码解析系列文章:

  • Android消息机制源码解析(一)——消息的载体Message
  • Android消息机制源码解析(二)——消息的执行者Handler
  • Android消息机制源码解析(三)——消息循环器Looper
  • Android消息机制源码解析(四)——消息队列MessageQueue

参考文献

《Efficient Android Threading》

0 0
原创粉丝点击