Efficient Android Threading(第三章:Android中的多线程)

每个Android应用程序都包含了多个线程,这些线程依附于Linux进程,并用Dalvik虚拟机管理其内部执行。应用程序的线程类型有UI线程、Binder线程和后台线程。在本章中,我们将了解Android平台上线程的相关内容:
•UI线程、Binder线程和后台线程之间的异同。
•Linux线程耦合。
•线程调度如何受应用程序进程级别的影响。
•运行的Linux线程

Android应用程序线程

所有的应用程序线程都基于Linux中的原生pthreads,在Java中用Thread表示,但是Android平台为这些线程添加了特别属性,把它们分为不同类型:UI线程、Binder线程和后台线程。

UI线程

UI线程在应用程序启动时启动,并在Linux进程的整个生命周期内存活。UI线程是应用程序的主线程,用于执行Android组件和更新屏幕上的UI元素。如果平台检测到任何非UI线程尝试更新UI,则立即抛出CalledFromWrongThreadException异常通知应用程序。因为Android UI Toolkit不是线程安全的,所以只允许从UI线程访问UI元素。

UI线程是一个顺序事件处理线程,它可以处理来自平台中任何其他线程发送的事件。如果UI线程正在处理前一个事件,那么后续的事件将排队等待被依次处理。任何事件都可以被post到UI线程,但是如果发送的事件没有显式地要求在UI线程执行,则UI关键事件可能在被处理前和应用响应减慢之前都必须在队列中等待。更多关于事件传递的详细讲解请见后续章节中的“Android消息传递”。

Binder线程

Binder线程用于在不同进程的线程之间进行通信。每个进程都维护一组叫线程池的线程,这些线程永远不会终止或重新创建,但可以响应本进程内另一个线程的请求并执行任务,也可以处理从其他进程传入的请求(如system services、intents、content providers和services)。必要时,系统会创建一个Binder线程来处理从其他进程传入的请求。如果应用程序提供Service供其他进程通过AIDL接口绑定,那么应用程序需要关心Binder线程。其它的大多数情况下,应用程序不必关心Binder线程,因为平台通常优先使用UI线程来处理从其它进程传入的请求。有关Binder线程的详细讨论请见“第五章”。

后台线程

应用程序显式创建的所有线程都是后台线程。后台线程是从UI线程创建的,因此它们继承了UI线程的属性,如优先级。默认情况下,新创建的进程不包含任何后台线程。后台线程总是在被需要时由应用程序自己创建。本书的Part II部分将描述怎样创建后台线程。

用ps –t查看在应用程序中创建的后台线程,输出如下信息:

u0_a72 4283 4257 320304 34540 ffffffff 00000000 S Thread-12412

在应用程序中,UI线程和后台线程的使用场景是不同的,但是在Linux中,它们都是普通的Linux原生线程,并且被处理的方式都是相同的。对UI线程的约束(只能在UI线程更新UI元素)是由Application Framework中的Window Manager实现的,而不是由Linux实现的。

Linux进程和线程

在Android中,后台线程上的耗时操作可以用多种方式实现,但是不管它们是如何实现的,线程最终在操作系统级别上总是一样的。Android平台是一个基于Linux的操作系统,每个应用程序都是在操作系统中作为Linux应用程序执行的。Android应用程序及其线程都遵循Linux执行环境。所以,对Linux环境的了解有助于我们了解和研究应用程序的执行情况,同时也可以提高应用程序的性能。

每个正在运行的应用程序都有一个底层的Linux进程,这个进程从Zygote孵化出来,具有以下特性:
用户ID (UID):
每个进程有一个唯一的用户标识,用于表示Linux系统上的一个用户。Linux是一个多用户系统,在Android上,每个应用程序都表示该系统中的一个用户。应用程序被安装时,它被分配一个用户ID。
进程ID(PID):
进程的唯一标识符。
父进程ID(PPID):
系统启动后,每个进程都是从另一个进程创建的。当前系统中正在运行的进程形成一个树状层次结构。因此,每个应用程序进程都有一个父进程。对于Android,所有进程的父进程都是Zygote。
Stack:
本地函数指针和变量。
Heap:
分配给进程的地址空间。地址空间是本进程私有的,不能被其他进程访问。

您可以从ADB shell调用ps(进程状态)命令查看运行中的应用程序的进程信息。Android ps命令就像在任何Linux系统上一样检索进程信息。然而,其选项不同于传统的Linux系统的PS:
-t:
显示进程中的线程信息。
-x:
显示在用户代码和系统代码所花的时间,通常以10ms为单位。
-p:
显示优先级。
-P:
显示调度策略,通常指示应用程序是否在前台或后台执行。
-c:
显示哪个CPU正在执行进程。
pid:
对应用程序的pid进行筛选。你也可以通过grep命令过滤。例如,用ps名称查询进程名为com.eat的进程的信息:

$ adb shell ps | grep com.eat
USER PID PPID VSIZE RSS WCHAN PS NAME
u0_a72 4257 144 320304 34540 ffffffff 00000000 S com.eat

从这个输出,我们可以提取com.eat应用的相关信息:
•UID:u0_a72
•PID:4257
•PPID:144(父进程的PID,在Android应用程序中总是Zygote)

另一种查查看进程和线程信息的方法是使用Android Tools中的DDMS查看。

应用程序创建和启动的所有线程都是Linux原生线程,它们都由POSIX标准定义。线程属于创建它们的进程,同一进程内的每个线程的父进程都是这个进程。线程和进程非常相似,它们之间的差异在于资源共享的差异。与其它进程相比,一个进程单独运行在一个沙盒环境中,进程内的所有线程可以共享资源。进程和线程之间的一个重要区别是进程不共享地址空间,但线程共享进程内的地址空间。这种内存共享使线程之间的通信比进程之间的通信要快得多,进程通信需要占用更多远程调用的开销。第四章将讲解线程间通信,第五章将讲解进程间通信。

进程启动时,将自动创建一个线程。一个进程总是包含至少一个线程来处理它的执行。在Android中,进程中自动创建的线程就是是我们已知的UI线程。下面看下包名为com.eat的进程创建的线程:

$ adb shell ps -t | grep u0_a72
USER PID PPID VSIZE RSS WCHAN PS NAME
u0_a72 4257 144 320304 34540 ffffffff 00000000 S com.eat
u0_a72 4259 4257 320304 34540 ffffffff 00000000 S GC
u0_a72 4262 4257 320304 34540 ffffffff 00000000 S Signal Catcher
u0_a72 4263 4257 320304 34540 ffffffff 00000000 S JDWP
u0_a72 4264 4257 320304 34540 ffffffff 00000000 S Compiler
u0_a72 4265 4257 320304 34540 ffffffff 00000000 S ReferenceQueueDemon
u0_a72 4266 4257 320304 34540 ffffffff 00000000 S FinalizerDaemon
u0_a72 4267 4257 320304 34540 ffffffff 00000000 S FinalizerWatchdogDaemon
u0_a72 4268 4257 320304 34540 ffffffff 00000000 S Binder_1
u0_a72 4269 4257 320304 34540 ffffffff 00000000 S Binder_2

在应用程序启动时,进程中启动的线程不少于10个。当应用启动时,第一个被默认启动的是名字为com.eat的线程。因此,这就是应用程序的UI线程。所有线程都是从UI线程派生出来的,因为它们的PPID是UI线程的PID。

大部分线程是Dalvik内部线程,用于处理垃圾回收、调试连接、Finalizer等,从应用角度,我们不需要关注这些线程。我们需要关注下面的线程:

u0_a72 4257 144 320304 34540 ffffffff 00000000 S com.eat
u0_a72 4268 4257 320304 34540 ffffffff 00000000 S Binder_1
u0_a72 4269 4257 320304 34540 ffffffff 00000000 S Binder_2

调度器

Linux以线程而不是进程作为执行的基本单元。因此,Android上的调度器关注的是线程而不是进程。调度器为线程分配执行时间。应用程序中执行的每个线程都在与应用程序中的所有其他线程竞争执行时间。调度器在选择将要执行的新线程和context切换之前,要先判断将执行哪些线程以及允许执行多长时间。调度器根据某些线程属性(如线程优先级)选择下一个要执行的线程,但是不同类型的调度器根据的线程属性可能不同。在Android中,应用程序的线程调度使用的是Linux内核的标准调度规则,而不是Dalvik虚拟机的调度规则。这意味着不仅应用程序中的线程相互竞争,而且不同应用程序中的所有线程相互竞争。

Linux内核调度器被称为完全公平的调度器(CFS)。这个“公平”表现在:它不仅基于线程的优先级,而且通过跟踪一个线程已执行的时间来平衡各线程的执行。如果一个线程先前对处理器有较低访问权,那么它将被允许在更高优先级的线程之前执行。如果线程不使用已分配的时间来执行,CFS将降低其优先级,从而减少这个线程在将来的执行时间。

平台主要有两种方式影响线程调度:
Priority:
更改Linux线程优先级。
Control group:
更改线程所属控制组。

线程优先级

应用程序中的每个线程都有一个优先级,优先级用于指示每次进行context切换时调度器应该把执行时间优先分配给哪个线程。在Linux中,线程的优先级被称为亲和值,这表示一个线程对待其他线程的亲和力。因此,低亲和值对应高优先级。在Android中,Linux的亲和值从-20(最高优先级)到19(最低优先级),默认值为0。一个线程的优先级继承于它的父线程,并一直保留这个优先级,除非被应用程序显式地改变了。应用程序可以用下面两个类更改线程的优先级:
java.lang.Thread

setPriority(int priority);

基于Java优先级范围设置优先级,值从0(最低优先级)到10(最高优先级)。
android.os.Process

Process.setThreadPriority(int priority); // Calling thread.
Process.setThreadPriority(int threadId, int priority); // Thread with
// specific id

使用Linux亲和值设置优先级,值从-20(最高优先级)到19(最低优先级)。

Java优先级对比Linux亲和值
Thread.setPriority()是平台独立的,它是底层平台对线程优先级的抽象。这个抽象的优先级值与Linux亲和值的对应关系如下:
这里写图片描述
Java优先级是底层平台对线程优先级的抽象,不同平台上,上述Java优先级与Linux亲和值的映射关系可能不同。上述的映射关系是Android的Jelly Bean版本定义的。

线程控制组

Android不仅依赖Linux CFS实现线程的调度,而且在所有线程中使用线程控制组。线程控制组是Linux容器,用于管理一个容器中所有线程的处理器时间分配。同一个应用程序中创建的所有线程都属于线程控制组中的一个。

Android定义了多个控制组,但应用程序最重要的控制组是前台组和后台组。Android平台定义了执行约束,以便为不同的控制组中的线程在处理器上分配不同的执行时间。前台组中的线程比后台组中的线程被分配更多的执行时间,Android利用这一点确保屏幕上可见的应用程序比屏幕上不可见的应用程序获得更多的处理器分配。屏幕上的可见性与进程级别有关,如图3-1所示。
这里写图片描述
图3-1:线程控制组
如果应用程序运行在前台或可见级别的进程,则由该应用程序创建的线程将属于前台组,并被分配处理器的大部分时间,处理器的其余时间将在其他应用程序中的线程中进行分配。用ps命名查看前台线程的信息显示如下(注意值为fg的那一列):

$ adb shell ps -P | grep u0_a72
u0_a72 4257 144 320304 34504 fg ffffffff 00000000 S com.eat

如果用户将应用程序切换到后台,如按下Home按钮,则应用程序中的所有线程将切换到后台组,并接收更少的处理器资源分配。用ps命名查看后台线程的信息显示如下:

$ adb shell ps -P | grep u0_a72
u0_a72 4257 144 318700 32164 bg ffffffff 00000000 S com.eat

当应用程序再次出现在屏幕上时,线程将移回前台组。线程在线程控制组间的切换是随着应用程序的进程级别改变而切换的。控制组的使用提高了屏幕上应用程序的性能,减少了后台应用程序干扰用户实际看到的应用程序的风险,从而提高了用户体验。

尽管控制组确保后台应用程序尽可能少地干扰可见应用程序的性能,但应用程序仍然可以创建许多与UI线程竞争的线程。默认情况下,应用程序创建的线程与UI线程具有相同的优先级和在同一控制组,因此它们以相同的条件竞争处理器资源。因此,尽管创建后台线程的目的是提高应用程序的性能,但是创建大量后台线程可能会降低UI线程的性能。为了解决这个问题,可以将后台线程与应用程序线程默认所在的控制组解耦。可以把后台线程设置为较低优先级,即使应用程序可见,后台线程也始终属于后台组,以实现后台线程与应用程序默认控制组解耦。

用Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)不仅降低了一个线程的优先级,并且使这个线程与进程的默认控制组解耦,和使这个线程一直在后台组中。

总结

Android中所有的线程类型(UI线程、Binder线程、后台线程)都是Linux Posix线程。当进程启动时,应用程序有一个UI线程和多个Binder线程,但是应用程序必须自己创建后台线程。所有的Android组件默认在UI线程上执行,但是耗时的任务需要在后台线程中执行,以防止减慢UI渲染和引起ANR问题。UI线程是最重要的线程,但调度器不知道哪个线程是UI线程,因此UI线程与其他线程相比没有特殊的调度优势。应用程序可以通过降低后台线程的优先级和把不重要的后台线程放入后台组,以防止后台线程过多的干扰UI线程。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值