本文概述
- Android组件和多处理需求
- Android软件堆栈
- 应用架构
- 应用执行
- 构建应用程序以提高性能
- 总结
Android组件和多处理需求
多核处理器是当今手持设备中的标准配置, 应用程序应利用这种机会并行处理数据。在大多数情况下, 并行数据处理可带来更好的性能和响应能力, 因为更多的处理器可以积极参与运行应用程序。但是另一方面, 执行变得更加复杂, 这会导致不确定的行为和与计时相关的错误, 这些错误可能很难重现。每个应用程序开发人员都需要管理一个多线程编程模型, 以及如何避免它引起的问题。
本书侧重于Android平台上的应用程序线程, 以及多线程如何与Android编程模型配合使用。它提供了对底层执行机制以及如何有效利用异步技术的理解。多CPU硬件为应用程序和开发人员带来许多好处:应用程序代码可以在多个CPU上同时执行以提高性能。并发执行和响应能力是应用程序成功的关键因素。活泼的应用程序对于保留本来不错的应用程序的用户体验至关重要。但是本书并不仅限于提高应用程序的速度-它应该以最小的实现工作量, 良好的代码设计和最小的意外错误风险(在并发世界中并不罕见)来实现快速发展。为正确的用例选择正确的异步机制对于通过一致的代码设计实现简单而健壮的代码执行至关重要。
但是在沉浸于线程世界之前, 我们将首先介绍Android平台, 应用程序体系结构及其执行。本章提供了本书其余部分中有效讨论线程所需的知识基础, 但是有关Android平台的完整信息可以在官方文档或市场上众多的Android编程书籍中找到。
Android软件堆栈
应用程序运行在基于Linux内核, 本机C / C ++库和执行应用程序代码的运行时的软件堆栈之上(图1-1)。元素是:
图1-1。 Android软件堆栈
- 应用领域
- Java应用程序的顶层, 使用Core Java库和Android Application Framework类。
- 核心Java
- 应用程序和应用程序框架使用的核心Java库。它不是完全兼容的Java SE或ME实现, 而是基于Java 5的已淘汰Apache Harmony实现的子集。它提供了基本的Java线程机制, 如java.lang.Thread-class和java.util.concurrent-。包。
- 应用框架
- 处理窗口系统, UI工具包, 资源等的Android类-基本上是用Java编写Android应用程序所需的所有内容。该框架定义和管理Android组件及其相互通信的生命周期。此外, 它定义了一组Android特定的异步机制, 应用程序可用来简化线程管理:HandlerThread, AsyncTask, IntentService, AsyncQueryHandler和Loaders。所有这些机制将在本书中进行描述。
- 本机库
- 处理图形, 媒体, 数据库, 字体, OpenGL等的C / C ++库。Java应用程序不直接与本机库交互, 因为Application框架提供了本机代码的Java包装器。
- 运行
- Dalvik虚拟机为每个Java应用程序提供一个沙箱, 并执行已编译的Android应用程序代码, 其内部字节码表示形式存储在Dalvik Executable(.dex)文件中。每个应用程序都在其自己的Dalvik运行时中运行。
- Linux内核
- 允许应用程序使用设备的硬件功能(例如声音, 网络, 摄像机等)的基础操作系统。它还管理进程和线程。将为每个应用程序启动一个进程, 并且每个进程都具有运行中的应用程序的Dalvik运行时。在该过程中, 多个线程可以执行应用程序代码。内核通过调度为进程及其线程分配可用的CPU执行时间。
应用架构
应用程序的基础是Application对象和Android组件:Activity, Service, BroadcastReceiver和ContentProvider。
应用
Java中正在执行的应用程序的表示形式是android.app.Application对象, 该对象在应用程序启动时实例化, 并在应用程序停止时销毁, 即Application类的实例在该应用程序的Linux进程的生命周期内持续存在。当该过程终止并重新启动时, 将创建一个新的Application实例。
组件
Android应用程序的基本组成部分是由Dalvik运行时管理的组件:Activity, Service, BroadcastReceiver和ContentProvider。这些组件的配置和交互定义了应用程序的行为。这些实体具有不同的职责和生命周期, 但它们都代表可以启动应用程序的应用程序入口点。启动组件后, 它可以在应用程序的整个生命周期中触发另一个组件, 依此类推。
组件通过Intent在应用程序内部或应用程序之间触发另一个组件的启动。Intent指定了接收者要采取的行动(例如, 发送电子邮件或拍照), 并且还可以将数据从发送者提供给接收者。一个Intent可以是显式的或隐式的:
- 明确Intent
- 定义组件的完全分类的名称, 该名称在应用程序中在编译时是已知的。
- 内隐Intent
- 与已在IntentFilter中定义了一组特征的组件的运行时绑定。如果Intent与组件的IntentFilter的特征匹配, 则可以启动该组件。
组件及其生命周期是Android专有的术语, 它们与底层Java对象没有直接匹配。一个Java对象可以超过其组件的寿命, 并且运行时可以包含与同一个实时组件相关的多个Java对象。这是造成混乱的根源, 正如我们将在第6章中看到的那样, 它存在内存泄漏的风险。
应用程序通过子类化实现组件, 并且应用程序中的所有组件都必须在AndroidManifest.xml文件中注册。
Activity
Activity是向用户显示的屏幕, 几乎总是占据设备的全屏。它显示信息, 处理用户输入等。它包含屏幕上显示的所有UI组件(按钮, 文本, 图像等), 并包含对具有所有View实例的视图层次结构的对象引用。因此, Activity的内存占用量可能会增大。
当用户在屏幕之间导航时, “Activity”实例形成一个堆栈。导航到新屏幕会将“Activity”推入堆栈, 而向后导航会导致相应的弹出窗口。
在图1-2中, 用户已经启动了初始ActivityA, 并在A完成时导航到B, 然后导航到C和D。A, B和C是全屏显示, 但D仅覆盖了一部分显示。因此, 在堆栈顶部, A被破坏, B被完全遮盖, C被部分显示, D被完全显示。因此, D具有焦点并接收用户输入。堆栈中的位置确定每个Activity的状态:
图1-2。Activity堆栈
- 活跃在前台:D
- 已暂停且部分可见:C
- 静止不可见:B
- 无效且已销毁:A
应用程序最高Activity的状态会影响应用程序的系统优先级(也称为a)。进程等级-依次影响终止应用程序的机会(应用程序终止)和应用程序线程的计划执行时间(第3章)。
当用户向后导航时(即, 按下“后退”按钮-或“Activity”明确调用finish()时。
服务
服务可以在后台执行, 而无需用户直接交互。当操作的寿命超过使用寿命时, 通常用于从其他组件上卸载执行。可以在启动或绑定模式下执行服务:
- 开始服务
- 通过使用显式或隐式Intent调用Context.startService(Intent)来启动服务。当调用Context.stopService(Intent)时终止。
- 绑定服务
- 多个组件可以通过带有显式或隐式Intent参数的Context.bindService(Intent, ServiceConnection, int)绑定到服务。绑定之后, 组件可以通过ServiceConnection接口与Service进行交互, 并且可以通过Context.unbindService(ServiceConnection)与Service解除绑定。当最后一个组件与服务解除绑定时, 它将被销毁。
内容提供商
想要在应用程序之内或之间共享大量数据的应用程序可以利用ContentProvider。它可以提供对任何数据源的访问, 但是最常与SQLite数据库配合使用, 而SQLite数据库始终是应用程序专用的。在ContentProvider的帮助下, 应用程序可以将该数据发布到在远程进程中执行的应用程序。
广播接收器
该组件的功能非常受限制:它侦听从应用程序, 远程应用程序或平台内部发送的Intent。它过滤传入的Intent, 以确定将哪些Intent发送到BroadcastReceiver。当你想开始监听Intent时, 应该动态注册BroadcastReceiver, 而当它停止监听时, 应该取消注册。如果它是在AndroidManifest中静态注册的, 则在安装应用程序时会监听Intent。因此, 如果Intent与过滤器匹配, 则BroadcastReceiver可以启动其关联的应用程序。
应用执行
Android是一个多用户, 多任务系统, 可以同时运行多个应用程序, 并且允许用户在应用程序之间切换而不会引起明显的延迟。多任务由Linux内核处理, 应用程序执行基于Linux进程。
Linux进程
Linux为每个用户分配一个唯一的用户ID, 基本上是OS跟踪的一个数字, 以使用户分开。每个用户都可以访问受权限保护的私有资源, 并且任何用户(除了root, 超级用户, 在这里都不涉及我们)都不能访问其他用户的私有资源。因此, 创建沙箱来隔离用户。在Android中, 每个应用程序包都有唯一的用户ID;也就是说, Android中的应用程序对应于Linux中的唯一用户, 并且无法访问其他应用程序的资源。
Android为每个流程添加的是针对应用程序每个实例的运行时执行环境Dalvik虚拟机。图1-3显示了Linux流程模型, VM和应用程序之间的关系。
图1-3。应用程序在不同的进程和VM中执行
默认情况下, 应用程序和进程具有一对一的关系, 但是如果需要, 一个应用程序可以在多个进程中运行, 或者多个应用程序可以在同一进程中运行。
生命周期
应用程序生命周期封装在其Linux进程中, 该进程在Java中映射到android.app.Application类。运行时调用其onCreate()方法时, 将启动每个应用程序的Application对象。理想情况下, 应用程序在运行时通过对其onTerminate()的调用而终止, 但是应用程序不能依赖于此。在运行时有机会调用onTerminate()之前, 底层Linux进程可能已经被杀死。 Application对象是要在流程中实例化的第一个组件, 最后一个要销毁的组件。
申请开始
当启动应用程序的组件之一以启动该应用程序时, 将启动该应用程序。任何组件都可以成为应用程序的入口点, 一旦触发第一个组件以启动Linux进程(除非它已经在运行), 就会启动它, 这将导致以下启动顺序:
- 启动Linux进程。
- 创建Dalvik虚拟机。
- 创建应用程序实例。
- 为应用程序创建入口点组件。
设置新的Linux进程和运行时不是瞬时操作。它会降低性能, 并对用户体验产生明显影响。因此, 系统尝试通过在系统启动时启动一个称为Zygote的特殊过程来缩短Android应用程序的启动时间。 Zygote已预加载了整套核心库。从Zygote流程派生出新的应用程序流程, 而无需复制在所有应用程序之间共享的核心库。
申请终止
在应用程序的开始处创建一个过程, 并在系统要释放资源时结束该过程。由于用户可以在以后的任何时间请求应用程序, 因此运行时避免了销毁其所有资源, 直到Activity应用程序的数量导致整个系统的实际资源短缺为止。因此, 即使应用程序的所有组件都已销毁, 它也不会自动终止。
当系统资源不足时, 由运行时决定应终止哪个进程。为了做出此决定, 系统会根据应用程序的可见性和当前正在执行的组件, 对每个进程进行排名。在下面的排名中, 排名最低的进程被迫在排名较高的进程之前退出。进程排名是(最高的)。
- 前台
- 应用程序在前面有一个可见的组件, 服务绑定到了远程进程中前面的Activity或BroadcastReceiver正在运行。
- 可见
- 应用程序具有可见的组件, 但部分被遮盖。
- 服务
- 服务在后台执行, 并且未绑定到可见组件。
- 后台
- 不可见的Activity。这是包含大多数应用程序的过程级别。
- 空的
- 没有Activity组件的过程。空进程被保留以缩短启动时间, 但是当系统回收资源时, 它们是第一个终止的进程。
实际上, 排名系统可确保平台耗尽资源时, 不会终止任何可见的应用程序。
两个交互应用程序的生命周期。
此示例说明了以典型方式交互的两个进程P1和P2的生命周期(图1-4)。 P1是一个客户端应用程序, 它在服务器应用程序P2中调用服务。客户端进程P1由广播的Intent触发时开始。在启动时, 该过程将同时启动BroadcastReceiver和Application实例。一段时间后, 将启动一个Activity, 并且在所有这段时间中, P1具有最高的进程等级:前台。
图1-4。客户端应用程序启动
服务
在其他过程中。
Activity将工作转移到在流程P2中运行的服务, 该服务启动服务和关联的应用程序实例。因此, 应用程序将工作分为两个不同的过程。当P2服务继续运行时, P1Activity可以终止。
一旦所有组件完成(用户从P1中的“Activity”导航返回, 并且其他进程或运行时要求P2中的“服务”停止), 这两个进程均被列为“空”, 从而使它们有可能被系统终止需要资源时。
表1-1中显示了执行过程中进程等级的详细列表。
表1-1。流程等级转换。
应用状态 | P1流程等级 | P2流程等级 |
P1从BroadcastReceiver入口点开始 |
Foreground |
不适用 |
P1开始Activity |
Foreground |
N.A. |
P1在P2中启动服务入口点 |
Foreground |
Foreground |
P1Activity被破坏 |
Empty |
Service |
P2服务已停止 |
Empty |
Empty |
应该注意的是, 实际的应用程序生命周期(由Linux进程定义)与感知的应用程序生命周期之间存在差异。该系统可以运行多个应用程序进程, 即使用户认为它们已终止。如果系统资源允许, 则空进程会持续存在, 以缩短重新启动时的启动时间。
构建应用程序以提高性能
Android设备是可以同时运行多个操作的多处理器系统, 但是要确保每个应用程序可以同时分区和执行以优化应用程序性能, 这取决于每个应用程序。如果应用程序未启用分区操作, 而是希望将所有操作作为一项长时间操作运行, 则它只能利用一个CPU, 从而导致性能欠佳。未分区的操作必须同步运行, 而分区的操作可以异步运行。通过异步操作, 系统可以在多个CPU之间共享执行, 因此可以提高吞吐量。
具有多个独立任务的应用程序应被构造为利用异步执行。一种方法是将应用程序执行分为几个进程, 因为它们可以同时运行。但是, 每个进程都会为其自身的大量资源分配内存, 因此在多个进程中执行一个应用程序将比一个进程中的一个应用程序使用更多的内存。此外, 进程之间的启动和通信很慢, 不是实现异步执行的有效方法。多个过程可能仍然是有效的设计, 但是该决定应独立于性能。为了获得更高的吞吐量和更好的性能, 应用程序应在每个进程中利用多个线程。
通过线程创建响应式应用程序
一个应用程序可以在多个CPU上以高吞吐率利用异步执行, 但这不能保证一个响应式应用程序。响应能力是用户在交互过程中感知应用程序的方式:UI快速响应按钮单击, 平滑动画等。基本上, 从用户体验的角度来看, 性能取决于应用程序更新UI组件的速度。更新UI组件的责任在于UI线程[1], 这是系统允许处理UI更新的唯一线程。
为了使应用程序具有响应能力, 应确保在UI线程上不执行任何长时间运行的任务。如果这样做, 该线程上的所有其他执行将被延迟。通常, 在UI线程上执行长时间运行的任务的第一个症状是UI变得无响应, 因为不允许UI更新屏幕或正确接受用户按钮按下。如果应用程序延迟UI线程的时间过长(通常为5-10秒), 则运行时将向用户显示“应用程序无响应”(ANR)对话框, 为用户提供关闭应用程序的选项。显然, 你想避免这种情况。实际上, 运行时禁止某些耗时的操作(例如网络下载)在UI线程上运行。
因此, 应在后台线程上处理长时间的操作。长期运行的任务通常包括:
- 网络通讯
- 读取或写入文件
- 在数据库中创建, 删除和更新元素
- 读取或写入SharedPreferences
- 图像处理
- 文字解析
什么是长任务?
没有长任务的固定定义, 也没有明确指示何时应在后台线程上执行任务, 但是一旦用户感觉到UI滞后(例如, 缓慢的按钮反馈和断断续续的动画), 便表明任务已完成太长而无法在UI线程上运行。通常, 动画对UI线程上的竞争任务比单击按钮要敏感得多, 因为人脑对于何时真正发生屏幕触摸有些含糊。因此, 让我们对动画作为最苛刻的用例进行一些粗略的推理。
在事件循环中更新动画, 其中每个事件都以一帧(即一个绘制周期)更新动画。每个时间帧可以执行的绘制周期越多, 动画效果就越好。如果目标是每秒进行60个绘图循环-也就是每秒帧数(fps)-每帧必须在16ms内渲染。如果另一个任务同时在UI线程上运行, 则绘图周期和辅助任务都必须在16ms内完成, 以避免出现卡顿动画。因此, 任务可能需要少于16ms的执行时间, 并且仍被认为是很长的时间。
该示例和计算是粗略的, 不仅表示持续数秒钟的网络连接如何影响应用程序的响应能力, 而且乍一看似乎无害的任务也表明了这种影响。应用程序中的瓶颈可能隐藏在任何地方。
Android应用程序中的线程与任何组件构建块一样基本。除非另有说明, 否则所有Android组件和系统回调均在UI线程上运行, 并且在执行较长的任务时应使用后台线程。
总结
Android应用程序在Dalvik运行时中的Linux操作系统之上运行, 该运行时包含在Linux进程中。 Android应用了一个流程排名系统, 该系统优先考虑每个正在运行的应用程序的重要性, 以确保终止的只是优先级最低的应用程序。为了提高性能, 应用程序应在多个线程之间拆分操作, 以便同时执行代码。每个Linux进程都包含一个负责更新UI的特定线程。所有长时间的操作都应远离UI线程, 而应在其他线程上执行。
[1]也称为主线程, 但是在本书中, 我们始终遵循将其称为“ UI线程”的约定。