在讲解多线程之前,我们要先介绍下Android平台、应用架构、应用执行原理。本章讲述了本书后续部分所述线程相关的基本知识。Android平台相关的详尽资料请参考Android官方文档,或世面上最流行的Android编程相关书籍。

Android软件堆栈

APP运行在以Linux kernel、native C/C++库、Runtime为基础的软件堆栈上。如图1-1。

Android 在堆栈移出 安卓堆栈软件_Android多线程


图1-1:Android软件堆栈

Android软件堆栈的主要组成有:

Applications:用Java实现的Android应用程序,依赖Java和Android框架库。

**Core Java:**Android应用程序和Android应用框架使用的核心Java库。它不完全兼容Java SE 或 ME,但是基于Apache Harmony(基于Java 5)的子集实现。它提供了基本的Jav线程机制:java.lang.Thread类和java.util.concurrent包。

Application framework:处理窗口系统、UI toolkit、资源等,提供Android应用程序Java实现部分所需的一切。Framework定义和管理Android组件的生命周期和组件的通信机制。此外,它还定义了一套Android特有的同步机制:HandlerThread, AsyncTask, IntentService, AsyncQueryHandler, 和 Loaders。APP可用这些机制简化线程的管理。所有这些机制将在本书中讲到。

Native libraries:处理图形,媒体,数据库,字体,OpenGL等的C / C ++库。因为Framework已为Native代码提供Java封装,所以APP通常不会直接与Native库交互。

Runtime:沙盒运行时环境,在虚拟机中执行编译的Android应用程序代码,并具有内部字节码表示形式。每个应用程序都在自己的runtime内执行,即Dalvik或ART(Android Runtime)。ART在KitKat(API等级19)中被添加为可由用户启用的可选runtime,但Dalvik是写入时的默认runtime时。

Linux kernel:基础操作系统允许应用程序使用设备的硬件功能:声音,网络,摄像头等。它还管理进程和线程。每个APP启动一个进程,每个进程都拥有一个APP的runtime。在一个进程中,可以有多个线程执行APP的代码。内核通过调度器为进程及其线程分割可用的CPU执行时间。

应用架构

APP的基础是Application对象和Android四大组件:Activity,Service,BroadcastReceiver和ContentProvider。

Application

运行中的APP用android.app.Application对象表示。该对象在APP启动时被实例化,在APP停止时销毁,即Application类的实例与APP的Linux进程有相同的生命周期。当进程终止并重新启动时,将会创建一个新的Application实例。

四大组件

Android应用程序的基本组成部分是由runtime管理的四大组件:Activity,Service,BroadcastReceiver和ContentProvider。这些组件的配置和交互定义了应用程序的行为。它们有不同的功能和生命周期,但都可以做应用程序启动的入口点。在整个应用程序的生命周期内,一个已启动的组件可以触发启动另一个组件。在一个应用中或两个应用间,可以用Intent启动一个组件。例如,Intent指定receiver要接收的Action(如发送电子邮件或拍照),并且还可以将数据从发送者传送到接收者。Intent分为显式的和隐式的。
显式Intent:显式定义组件的名称,在编译时应用程序就能识别此名称。
隐式Intent:运行时绑定到组件,并通过IntentFilter定义一些特性。如果Intent与组件的IntentFilter中的特性匹配,则可以启动该组件。

组件及其生命周期是Android特定的术语,它们不直接与基础Java对象进行匹配。Java对象可以扩展其组件,并且runtime可以包含与同一个组件相关的多个Java对象。正如我们将会在第六章看到的,这会引起内存泄漏的风险。APP通过继承组件来实现组件,并且APP中的所有组件必须要在AndroidManifest.xml中注册。

Activity

Activity是向用户展示的界面,用于展示信息、处理用户输入等。它包含屏幕上显示的UI组件:按钮、文本、图像等等,并用所有的视图实例将对象引用保存到视图层次结构中。因此,一个Activity将占用较大的内存。

当用户在界面之间切换,Activity实例形成一个堆栈。进入一个新界面会将一个Activity实例push到堆栈中,而点击back键返回会弹出对应的Activity实例。

在图1-2中,用户已经启动了一个Activity A,并在A销毁后跳转到B,然后转到C和D。A、B和C是全屏的,但D只覆盖了屏幕的一部分。因此,A销毁了,B完全被遮蔽,C部分被显示,D在栈顶被完全显示。因此,D能获取焦点并能响应用户的输入。每个Activity在堆栈中的位置决定它的状态:

Activity D:在前台,且是活跃的。

Activity C:暂停,且部分可见。

Activity B:停止,且不可见。

Activity A:销毁的。

Android 在堆栈移出 安卓堆栈软件_应用程序_02


图1-2:Activity堆栈

一个APP的最顶层的Activity的状态对应用程序的系统优先级(也被称为进程优先级)有影响,这反过来会影响应用程序的终止时机(见本章后续部分“应用程序终止”)和APP线程的执行时间(见第3章)。一个Activity在用户点击返回按钮或明确调用finish()时结束它的生命周期。

Service

服务可以在后台执行,而不需要直接与用户交互。当执行的任务超过组件的生命周期时,通常就用Service来实现这种耗时的任务。Service的启动模式分为start和bind两种。
Started Service:该服务是通过显式或隐式的Intent调用Context.startService(Intent)开始,通过调用Context.stopService(Intent)终止。
Bound Service:多个组件可以通过具有显式或隐式Intent参数的Context.bindService(Intent,ServiceConnection,int)绑定到一个服务。绑定成功后,组件可以通过服务接口与服务进行交互,并通过Context.unbindService(ServiceConnection)解除与服务的绑定。当最后一个组件与服务取消绑定时,服务将被销毁。

ContentProvider

应用程序可以使用ContentProvider在应用程序内或应用程序之间共享大量数据。它可以提供对任何数据源的访问,但最常用于与应用程序私有的SQLite数据库协作。通过使用ContentProvider,应用程序可以将数据提供给在远程进程中执行的应用程序。

BroadcastReceiver

该组件具有非常有限的功能:它监听从应用程序、远程应用程序或平台发送的Intent。它会过滤传入的内容,过滤哪些Intent被发送到当前BroadcastReceiver。您应该在开始监听Intent时动态注册BroadcastReceiver,并在停止监听时取消注册。如果在AndroidManifest中静态注册广播接收器,则在安装应用程序时就开始监听Intent。因此,如果一个Intent与过滤器匹配,那么BroadcastReceiver可以启动其关联的应用程序。

应用程序执行

Android是一个多用户多任务系统,可以同时运行多个应用程序,用户可以在应用程序之间切换,而感觉不到延迟。Linux内核处理多任务,并且应用程序的执行基于Linux进程。

Linux进程

Linux为每个用户分配唯一的用户ID,用以区分用户。每个用户都可以访问受权限保护的私有资源,并且没有用户(除root,超级用户,这里不涉及)可以访问另一个用户的私有资源。因此,系统会创建沙盒以隔离用户。在Android中,每个应用包都有唯一的用户ID。 例如,Android中的应用程序对应于Linux中的唯一用户,无法访问其他应用程序的资源。对于每个应用程序的实例,Android为每个进程添加一个runtime执行环境,即Dalvik虚拟机。图1-3显示了Linux进程模型,VM和应用程序之间的关系。

Android 在堆栈移出 安卓堆栈软件_Android多线程_03


图1-3:应用程序在不同的进程和虚拟机中执行

默认情况下,应用程序和进程具有一对一的关系,但是如果需要,应用程序可以在多个进程中运行,也可以让多个应用程序运行在同一个进程中。

生命周期

应用程序生命周期封装在其Linux进程中,它在Java中用android.app.Application类表示。当runtime调用应用程序的onCreate()方法时,其Application对象将会启动。理想情况下,应用程序终止于runtime对其onTerminate()的调用,但应用程序不能依赖此操作。底层Linux进程可能在runtime调用onTerminate()之前就已被杀死。Application对象是在进程中第一个实例化的组件,也是最后一个被销毁的。

应用程序启动

当应用程序的一个组件初始化并运行了,那么这个应用程序就启动了。任何组件都可以是应用程序的入口点,一旦触发启动第一个组件,就会启动一个Linux进程(除非它已经在运行了),导致以下启动顺序:
1、 启动Linux进程
2、 创建runtime
3、 创建Application实例
4、 为应用程序创建入口点组件。
建立一个新的Linux进程和runtime并不是一个即时的操作。它可能会降低性能并对用户体验产生明显的影响。因此,在系统启动时创建一个称为Zygote的特殊进程来缩短Android应用程序的启动时间。Zygote拥有整套预加载好的核心库。新的应用程序进程从Zygote进程孵化出来,不复制核心库,而是所有应用程序共享核心库。

应用程序终止

进程在应用程序启动时创建,并在系统要释放资源时结束。因为用户可能在任何时候请求应用程序,所以直到实际应用程序数量导致整个系统的资源短缺时runtime才销毁其所有资源。因此,即使应用程序的所有组件已被销毁,应用程序也不会自动终止。当系统资源不足时,由runtime决定哪个进程应该被杀死。系统根据应用程序的可见性和当前执行的组件对每个进程进行排名,以判断应该杀掉哪个进程。排名在最后的进程在排名较前的进程之前被优先杀掉。进程的排名如下:
Foreground:应用程序在前台有一个可见的组件、服务绑定到一个在前台的Activity,或BroadcastReceiver正在运行。
Visible:应用程序具有部分可见的组件。
Service:服务在后台执行,没有绑定到可见组件。
**Background:**Activity不可见。 大多数应用程序都处于这个进程状态。
Empty:没有活动组件的进程。保持空的进程可以提高应用程序启动时间,但是当系统回收资源时,它们是第一个被终止的进程。排名系统确保平台在资源耗尽时不会终止可见的应用程序。

例子:两个互相交互的应用程序的生命周期

此示例说明了以典型方式进行交互的两个进程P1和P2的生命周期(图1-4)。Client程序P1调用了Service程序 P2。Client进程P1由广播Intent触发启动。在启动时,该进程启动了BroadcastReceiver和Application实例。过了一段时间,一个Activity启动了,此时P1有最高的进程排名:Foreground。

Android 在堆栈移出 安卓堆栈软件_Android多线程_04


图1-4。 客户端应用程序启动在其他进程中的服务

Activity调用在进程P2中运行的服务,这会启动服务和与服务相关的Application实例。因此,任务在两个不同的进程中执行。 Activity P1销毁了,但Service P2可以继续运行。当所有组件都销毁后(用户在Activity P1点击back键,并且Service P2被其他进程或runtime终止),两个进程都处于Empty状态,系统需要资源时将考虑终止这两个进程。整个执行过程中详细的进程排序列表如表1-1所示。

Android 在堆栈移出 安卓堆栈软件_Android多线程_05


应该注意的是,由Linux进程定义的实际应用程序生命周期与感知到的应用程序生命周期之间存在差异。即使用户感知到某应用程序已终止,但它的进程可能仍然运行在系统中。如果系统资源允许,空的进程会一直运行,用以缩短应用重新启动时的启动时间。

构建高性能应用程序

Android设备是可以同时运行多个操作的多处理器系统,但是由每个应用程序确保操作被划分为并行执行的多个子任务,以优化应用程序性能。如果应用程序不分割操作,而是将所有操作都运行为一个长操作,那么程序只能利用一个CPU,导致性能不佳。未分割的操作必须同步运行,而分割的操作可以异步运行。通过异步操作,系统可以共享多个CPU之间的执行,从而提高吞吐量。

具有多个独立任务的应用程序应结构化以采用异步执行机制。一种方法是将应用程序执行分解为多个进程,因为这些进程可以同时运行。然而,每个进程为其大量资源分配内存,因此应用程序在多个进程中执行将比在一个进程中执行使用更多的内存。此外,进程之间的启动和通信速度很慢,所以这不是实现异步执行的有效方式。多个进程仍然是有效的设计,但它应该用来实现多个应用程序异步运行。为了实现更高的吞吐量和更好的性能,应用程序应该在每个进程中使用多个线程。

用线程实现应用程序的响应性

应用程序可以以高吞吐量在多个CPU上异步执行,但这不能保证应用程序的响应性。响应性是用户在交互过程中感知应用程序的方式:用户界面快速响应按钮点击,动画平滑等。从用户体验的角度来看,性能基本上取决于应用程序更新UI组件的速度。更新UI组件的线程叫UI线程,且整个系统中只能通过UI线程更新UI组件。

为了保证应用程序的响应性,应该确保不在UI线程上执行耗时操作。如果在UI线程执行耗时操作,UI线程上的所有其他执行都将被阻塞。在UI线程上执行耗时操作引起的常见问题是UI不响应,因为它不允许更新屏幕或接受用户按钮点击事件。如果应用程序的UI线程阻塞时间过长(通常5-10秒),runtime就会显示一个“应用程序无响应”(ANR)对话框给用户,让用户选择是否关闭应用程序。显然,你不希望出现这种情况。事实上,runtime会禁止某些在UI线程上的耗时操作,比如网络下载。

所以耗时操作必须在后台线程中进行。常见的耗时操作包括:
1、网络通信
2、读写文件
3、对数据库进行增、删、改、查。
4、读写 SharedPreferences
5、图片处理
6、文本解析

什么是耗时操作?对于耗时操作没有一个明确的定义,且耗时/非耗时操作之间也没有明确的界限,也没有明确的标准规定某个操作必须要在后台线程中执行。但是一旦用户能感知到UI卡顿(如按钮反馈慢、动画不流畅),这就意味着任务耗时太长以至于不能在UI线程上运行。通常情况下,动画对UI线程耗时操作要比按钮点击要敏感得多,因为人脑对于屏幕触摸的实际反馈有点模糊。因此,我们用对动画效果的简单分析来做个严苛的例子。

动画在事件循环中更新,其中每个事件用一个帧(即一个绘制周期)更新动画。每秒内执行的绘制周期越多,动画被感知的效果越好。如果目标是每秒执行60次绘制周期(fps),那么每帧必须要16ms内绘制完成。如果另一项任务也同时在UI线程上运行,则绘制周期和任务必须在16毫秒内完成,以避免动画卡顿。因此,一个耗时小于16ms的任务仍然是耗时的。这只是个粗略的示例计算,这说明一个应用程序的响应能力不仅会受几秒钟的网络连接的影响,还可能会受看起来短时间任务的影响。任何地方都可能会存在影响程序响应性的耗时任务。

Android应用程序中的线程与任何组件一样重要。所有的Android组件和系统回调(除非特别说明的)都运行在UI线程,并且应该使用后台线程执行耗时操作。

小结

Android应用程序运行在Dalvik虚拟机的Linux操作系统上。Android对每个正在运行的应用程序按照优先级进行进程排序,以确保优先终止最不重要的进程。应用程序应该使用多个线程并发执行代码,以提高程序的性能。每个Linux进程都有一个用于更新UI的UI线程。所有耗时的操作都不能在UI线程上执行,而应该在后台线程上执行。