Bootstrap

【北京迅为】iTOP-4412全能版使用手册-第七部分 Android入门教程

iTOP-4412全能版采用四核Cortex-A9,主频为1.4GHz-1.6GHz,配备S5M8767 电源管理,集成USB HUB,选用高品质板对板连接器稳定可靠,大厂生产,做工精良。接口一应俱全,开发更简单,搭载全网通4G、支持WIFI、蓝牙、陀螺仪、CAN总线、RS485总线、500万摄像头等模块,稳定运行Android 4.0.3/Android 4.4操作,系统通用Linux-3.0.15+Qt操作系统(QT支持5.7版本),Ubuntu版本:12.04,接口智能分配 方便好用。


第七部分 Android入门教程

介绍了和视频配套的安卓开发过程中常用的JNI技术,以及安卓应用界面基础知识,提供例程供大家学习和参考。Android系统进阶开发请参考本手中“Android应用开发”和“Android系统开发”章节

视频资源:

ITOP-4412开发板视频教程\11-迅为电子Android开发相关视频\

第八十六章 Android系统架构及剖析

本章侧重理论讲解,力图把 Android 的架构以及关键概念帮大家理清楚,当然,Android 架构比较复杂,需要多次理解和学习才能够掌握,如果仍然有困难,可以和下一章穿插着来学习,下一章主要着重 Android 系统和应用程序的具体开发步骤。通过理论与实践相结合来学习,会加快对 Android 系统的学习进程。

86.1 Android系统介绍

86.1.1 定义

Android(安卓)是一种以 Linux 为基础的开放源码操作系统,主要使用于便携设备。

Android 早期由”Android 之父”之称的 Andy Rubin 创办,2005 年由 Google 收购注资,并联合多家制造商组成开放手机联盟开发改良,逐渐扩展到到平板电脑及其他领域上。

Android 一词的本义指“机器人”,同时也是 Google 于 2007 年 11 月 5 日宣布的基于Linux 平台的开源手机操作系统的名称,该平台由操作系统、中间件、用户界面和应用软件组成。

86.1.2 特点

Google 公司在 2008 年,Patrick Brady 于 Google I/O 演讲”Anatomy & Physiology of an Android”,并提出的 Android HAL 架构图。HAL 以*.so 档的形式存在,可以把Android framework 与 Linux kernel 隔开。Android 拥有功能强大的 API,出色的文档,茁壮成长的开发人员社区,而且不需要为开发或发布支付费用。随着移动设备的日益普及,不管在怎样的开发背景下,使用 Android 软件开发新颖的手机应用程序都是一个令人为之振奋的良机。

应用程序框架支持组件的重用与替换 ·Dalvik 虚拟机专门为移动设备做了优化 内部集成浏览器,该浏览器基于开源的 WebKit 引擎 优化的图形库 包括 2D 和 3D 图形库,3D 图形库基于 OpenGL ES 1.0 (硬件加速可选) 。SQLite 用作结构化的数据存储·多媒体支持包括常见的音频、视频和静态印象文件格式(如 MPEG4, H.264, MP3, AAC, AMR, JPG,PNG, GIF) ·GSM 电话 (依赖于硬件) ·蓝牙 Bluetooth, EDGE, 3G, and WiFi (依赖于硬件) ·照相机,GPS,指南针,和加速度计 (依赖于硬件) ·丰富的开发环境 包括设备模拟器,

调试工具,内存及性能分析图表,和 Eclipse 集成开发环境插件。

Android 是运行于 Linux kernel 之上,但并不是 GNU/Linux。因为在一般 GNU/Linux里支持的功能,Android 大都没有支持,包括 Cairo、X11、Alsa、FFmpeg、GTK、Pango及 Glibc 等都被移除掉了。Android 又以 bionic 取代 Glibc、以 Skia 取代 Cairo、再以opencore 取代 FFmpeg 等等。Android 为了达到商业应用,必须移除被 GNU GPL 授权证所约束的部份,例如 Android 将驱动程序移到 userspace,使得 Linux driver 与 Linux kernel 彻底分开。bionic/libc/kernel/ 并非标准的 kernel header files。Android 的kernel header 是利用工具由 Linux kernel header 所产生的,这样做是为了保留常数、数据结构与宏。

Android 的 Linux kernel 控制包括安全(Security),存储器管理(Memory Managemeat),程序管理(Process Management),网络堆栈(Network Stack),驱动程序模型(Driver Model)等。下载 Android 源码之前,先要安装其构建工具 Repo 来初始化源码。Repo 是 Android 用来辅助 Git 工作的一个工具。

86.1.3 发展与演进

Android 在中国的前景十分广阔,首先是有成熟的消费者,在国内,Android 社区十分红火,这些社区为 Android 在中国的普及做了很好的推广作用。国内厂商和运营商也纷纷加入了 Android 阵营,包括中国移动,中国联通,中兴通讯,华为通讯,联想等大企业,同时不仅仅局限于手机,国内厂家也陆续推出了采用 Android 系统的 MID 产品。

Android 操作系统 2008 年才上市,随后迅速成为主导性的智能手机平台,市场份额达到了 2/3。与此同时,在快速发展的平板电脑市场,Android 份额也位居第二。虽然 Android是一款免费软件,但它却推动了谷歌核心的搜索业务增长。

下面我们介绍一下 Android 系统的发展历程:

2003 年 10 月,Andy Rubin 团队创办 Android 公司

2005 年 8 月,谷歌收购 Android 公司,Andy Rubin 担任谷歌工程部副总裁继续负责Android 项目;

2008 年 9 月,谷歌正式发布 Android 1.0 系统;

2011 年 1 月,Android 系统设备的用户总数达到了 1.35 亿,成为智能手机领域占有量第一的系统;

2011 年 8 月,Android 手机占据全球智能机市场 48%份额,并在亚太地区市场占据统治地位,终结了 Symbian 系统的霸主地位,跃居全球第一;

2012 年 1 月,谷歌 Android Market 已有 10 万开发者,推出超过 40 万应用;

2013 年 11 月,Android 4.4 正式发布,系统更智能、UI 更现代;

2013 年到 2018 年,这个阶段安卓进入飞速发展期,被升级的有摄像头、内存、机身、芯片等,原来的 3.5 寸小屏已退出历史舞台,全面屏、刘海屏、水滴屏已成为当下主流屏幕方案。

每个 Android 大版本的更新迭代前行,历经 10 余年,在用户体验、流畅性、续航、安全、隐私、机器学习等方面都取得较大的改进,下面我们来看一下 Android 的系统演进:

从 Android 1.0 发展到 Android 4.0,系统各项功能和特性迭代到一个较完善的阶段;

Android 4.1 系统,Google 开展了黄油计划(Project Butter),为了让 Android 系统摆脱 UI 交互上的严重滞后感,希望能像“黄油”一样顺滑。 核心原理是系统框架中的渲染和动画统一采用垂直同步技术(VSYNC),以及三重缓冲技术(Triple Buffer),让滑动、翻页等操作更加一致与顺滑。

Android 4.4 系统,Google 开展了瘦身计划(Project Svelte),力求降低安卓系统的内存使用,解决低端机型升级难的问题,让 Android 4.4 可正常运行在所有 Android 手机,从而减少安卓系统继续碎片化。UI 设计上,支持新的“沉浸式模式”,用户界面由过去的黑色与蓝色为主的色调转向带有透明度的浅色系,视觉语言变得更加明亮与现代化。

Android 5.0 系统,Google 开展了伏特计划(Project Volta),力求提升续航能力,这方面 Google 落后于业界厂商,厂商直面用户对续航尤为迫切,往往系统资源管控更为严格。

另外,系统采用全新的 ART,抛弃 Dalvik 虚拟机,大幅提升运行效率。UI 设计上,使用全新的扁平化 Material Design 设计风格,更加清新与质感的设计,统一 Android 设备的外观和使用体验。

Android 6.0 系统,Google 引入新的运行时权限,让用户能够更好地了解和控制权限;引入了 Doze 模式,进一步提升电池续航能力。UI 设计上,新增夜间模式,大幅改进通知栏,让通知更简洁。

Android 7.0 系统,引入新的 JIT 编译器,对 AOT 编译器的补充,可节省存储空间和加快更新速度;进一步优化 Doze 唤醒机制;UI 设计上,支持分屏功能;

Android 8.0 系统,Google 开展了计划(Project Treble),重新架构 Android,将安卓系统框架与 Vendor 层解耦,力求彻底解决安卓碎片化这一老大难的问题,这是安卓系统架构最大的变化。系统层面加强对后台服务、广播、位置的管控限制。UI 设计上,改进通知栏,智能文本选择和自动填充功能。

Android 9.0 系统,引入神经网络 API,采用机器学习的思路来预测用户使用习惯来做省电优化,继续强化 Treble 计划;文件系统(sdcardf/F2FS)持续提升;私有 API 的限制进一步规范化 Android 生态,强化隐私和安全,硬件安全性模块以及统一生物识别身份验证界面。UI 设计上,新的手势导航,加强支持刘海屏,UI 搜索界面使用到机器学习,AI 正在逐步强化Android 系统。

Android 10.0 系统,Google 开展了主线计划(Project Mainline),相关模块(Modules)不允许厂商直接修改,只能由 Google 应用商店来更新升级,强化用户隐私、系统安全与兼容性。支持脸部生物识别。

需要说明的是,到目前为止,无论 Android 系统如何演进,系统架构并没有什么大的变化,这间接也证明了 Android 系统设计上的远见和优秀。

86.1.4 优点

(1)应用程序框架支持组件的重用与替换。这样我们可以把系统中不喜欢的应用程序删除,安装我们喜欢的应用程序。

(2)Dalvik 虚拟机专门为移动设备进行了优化。Android 应用程序将由 Java 编写、编译的类文件通过 DX 工具转换成一种后缀名为.dex 的文件来执行。Dalvik 虚拟机是基于寄存器的,相对于 Java 虚拟机速度要快很多。

(3)内部集成浏览器基于开源的 WebKit 引擎。有了内置的浏览器,这将意味着 WAP应用的时代即将结束,真正的移动互联网时代已经来临,手机就是一台“小电脑”,可以在网上随意遨游。

(4)优化的图形库包括 2D 和 3D 图形库,3D 图形库基于 OpenGL ES 1.0。强大的图形库给游戏开发带来福音。在 3G/4G 最为重要的的应用莫过于手机上网和手机游戏。

(5)SQLite 用作结构化的数据存储。

(6)多媒体支持包括常见的音频、视频和静态印象文件格式如 MPEG4、H.264、MP3、AAC、AMR、JGP、PNG、GIF。

(7)GSM 电话(依赖于硬件)。

(8)蓝牙(Bluetooth)、EDGE、3G、WiFi(依赖于硬件)。

(9)照相机、GPS、指南针和加速度计(依赖于硬件)。

(10)丰富的开发环境包括设备模拟器、调试工具、内存及性能分析图表和 Eclipse 集成的开发环境插件。

(11)Google 提供了 Android 开发包 SDK,其中包含了大量的类库和开发工具,并且针对 Eclipse 的可视化开发插件 ADT。

Android 的优点包括开放性,丰富的硬件,方便开发等。

Android 开发的平台允许任何移动终端厂商加入到 Android 联盟中来。显著的开放性可以使其拥有更多的开发者,随着用户和应用的日益丰富,一个崭新的平台也将很快走向成熟。

开发性对于 Android 的发展而言,有利于积累人气,这里的人气包括消费者和厂商,而对于消费者来讲,最大的受益正是丰富的软件资源。开放的平台也会带来更大竞争,如此一来,消费者将可以用更低的价位购得心仪的手机。

Android 包含丰富的硬件。这一点还是与 Android 平台的开放性相关,由于 Android 的开放性,众多的厂商会推出千奇百怪,功能特色各具的多种产品。功能上的差异和特色,却不会影响到数据同步、甚至软件的兼容,如同从诺基亚 Symbian 风格手机一下改用苹果iPhone,同时还可将 Symbian 中优秀的软件带到 iPhone 上使用、联系人等资料更是可以方便地转移。

Android 方便开发。Android 平台提供给第三方开发商一个十分宽泛、自由的环境,不会受到各种条条框框的阻扰,可想而知,会有多少新颖别致的软件会诞生。

86.1.5 就业前景和行业需求

随着移动互联网的到来和迅猛发展,移动互联网开发人员的需求也是与日俱增。比如说android 市场,国外 Android 市场正在如日中天的扩展,据市场研究公司 IDC 发布研究报告称,预计中国智能手机市场在全球市场上所占份额将会从 18.3%上升至 26.5%,而美国市场所占份额则将从 21.3%下降至 17.8%。相信在不久的将来会有更多的用户选择 Android 系统的手机或是无线终端设备。

目前中国拥有世界上最大的手机用户群,再加上 3G 的推出对整个行业的巨大推动作用,全世界所有大中小型手机制造商几乎都在招聘 Android 工程师。然而每天有超过 16 万台的Android 设备出货,一年后全球可能会有超过 1 亿台的 Android 设备,以后将超越 iphone成为智能手机平台的旗舰。

86.2 Android系统架构

本节讲解 Android 的架构,Android 系统非常庞大且错综复杂,完全理解需要下很大的功夫,不过它的整体架构设计清晰。Android 底层内核空间以 Linux Kernel 作为基石,上层用户空间由 Native 系统库、虚拟机运行环境、框架层组成,通过系统调用(Syscall)连通系统的内核空间与用户空间。对于用户空间主要采用 C++和 Java 代码编写,通过 JNI 技术打通用户空间的 Java 层和 Native 层(C++/C),从而连通整个系统。

86.2.1 架构图

为了能让大家从整体上大致了解 Android 系统涉及的知识点,我们先来看一张 Google 官方提供的经典分层架构图,从下往上依次分为 Linux 内核、HAL、系统 Native 库和Android 运行时环境、Java 框架层以及应用层这 5 层架构,其中每一层都包含大量的子模块或子系统,下面这张图展示了 Android 系统的主要组成部分:

 

中文方式的架构图: 

可以很明显看出,Android 系统架构由 5 部 分组成,分别是:Linux Kernel、Android Runtime、Libraries、Application Framework、Applications。下面将详细介绍这 5 个部分。

86.2.2 Linux Kernel

Android 基于 Linux 提供核心系统服务,例如:安全、内存管理、进程管理、网络堆栈、驱动模型。除了标准的 Linux 内核外,Android 还增加了内核的驱动程序,如 Binder(IPC) 驱动、显示驱动、输入设备驱动、音频系统驱动、摄像头驱动、WiFi 驱动、蓝牙驱动、电源管理。

Linux Kernel 也作为硬件和软件之间的抽象层,它隐藏具体硬件细节而为上层提供统一的服务。

如果你学过计算机网络知道 OSI/RM,就会 知道分层的好处就是使用下层提供的服务而为上层提供统一的服务,屏蔽本层及以下层的差异,当本层及以下层发生了变化不会影响到上层。也就是说各层各司其职,各层提供固定的 SAP(Service Access Point) ,专业点可以说是高内聚、低耦合。

如果你只是做应用开发,就不需要深入了解 Linux Kernel 层。

86.2.3 Android Runtime

Android 包含一个核心库的集合,提供大部分在 Java 编程语言核心类库中可用的功能。每一个 Android 应用程序是 Dalvik 虚拟机中的实例,运行在他们自己的进程中。Dalvik 虚拟机设计成,在一个设备可以高效地运行多个虚拟机。 Dalvik 虚拟机可执行文件格式是.dex,dex 格式是专为 Dalvik 设计的一种压缩格式,适合内存和处理器速度有限的系统。

大多数虚拟机包括 JVM 都是基于栈的,而 Dalvik 虚拟机则是基于寄存器的。两种架构各有优劣,一般而言,基于栈的机器需要更多指令,而基于寄存器的机器 指令更大。dx 是一套工具,可以將 Java .class 转换成 .dex 格式。一个 dex 文件通常会有多个.class。由于 dex 有時必须进行最佳化,会使文件大小增加 1-4 倍,以 ODEX 结尾。

Dalvik 虚拟机依赖于 Linux 内核提供基本功能,如线程和底层内存管理。

86.2.4 Libraries(本地库)

Android 包含一个 C/C++库的集合,供 Android 系统的各个组件使用。这些功能通过Android 的应用程序框架 (application framework)暴露给开发者。下面列出一些核心库:

系统 C 库 —— 标准 C 系统库(libc)的 BSD 衍生,调整为基于嵌入式 Linux 设备

媒体库 ——基于 PacketVideo 的 OpenCORE。这些库支持播放和录制许多流行的音频和视频格式,以及静态图像文件,包括 MPEG4、 H.264、 MP3、 AAC、 AMR、JPG、PNG

界面管理 ——管理访问显示子系统和无缝组 合多个应用程序的二维和三维图形层

LibWebCore ——新式的 Web 浏览器引擎,驱动 Android 浏览器和内嵌的 web 视图

SGL ——基本的 2D 图形引擎

3D库 ——基于 OpenGL ES 1.0 APIs 的实现。库使用硬件 3D 加速或包含高度优化的3D 软件光栅

FreeType ——位图和矢量字体渲染

SQLite ——所有应用程序都可以使用的强大而轻量级的关系数据库引擎

86.5.5 Application Framework

通过提供开放的开发平台,Android 使开发者能够编制极其丰富和新颖的应用程序。开发者可以自由地利用设备硬件优势、访问位置信息、运行后台服务、设置闹钟、向状态栏添加通知等等,很多很多。

开发者可以完全使用核心应用程序所使用的框架 APIs。应用程序的体系结构旨在简化组件的重用,任何应用程序都能发布他的功能且任何其他应用程序可以使用这些功 能(需要服从框架执行的安全限制)。这一机制允许用户替换组件。

所有的应用程序其实是一组服务和系统,包括:

视图(View) ——丰富的、可扩展的视图集合,可用于构建一个应用程序。包括包括列表、网格、文本框、按 钮,甚至是内嵌的网页浏览器

内容提供者(Content Providers) ——使应 用程序能访问其他应用程序(如通讯录) 的数据,或共享自己的数据

资源管理器(Resource Manager) ——提供访问非代码资源,如本地化字符串、图形和布局文件

通知管理器(Notification Manager ) ——使所有的应用程序能够在状态栏显示自定义警告

活动管理器(Activity Manager ) ——管理应用程序生命周期,提供通用的导航回退功能

86.5.6 Applications

Android 装配一个核心应用程序集合,包括电子邮件客户端、SMS 程序、日历、地图、浏览器、联系人和其他设置。所有应用程序都是用 Java 编程语言写的。更加丰富 的应用程序有待我们去开发!

源码结构

Google 提供的 Android 包含了原始 Android 的目标机代码,主机编译工具、仿真环境,代码包经过解压缩后,第一级别的目录和文件如下所示:

.

|-- Makefile(全局的 Makefile)

|-- bionic(Bionic 含义为仿生,这里面是一些基础的库的源代码)

|-- bootloader(引导加载器)

|-- build (build 目录中的内容不是目标所用的代码,而是编译和配置所需要的脚本和工具)

|-- dalvik(JAVA 虚拟机)

|-- development (程序开发所需要的模板和工具)

|-- external(目标机器使用的一些库)

|-- frameworks(应用程序的框架层)

|-- hardware(与硬件相关的库)

|-- kernel(Linux 的源代码)

|-- packages(Android 的各种应用程序)

|-- prebuilt(Android 在各种平台下编译的预置脚本)

|-- recovery(与目标的恢复功能相关)

|-- system(Android 的底层的一些库)

86.3 Android系统的启动过程

下图是 Android 系统启动过程的示意图:

 

从上图可以看到:Android 系统启动是从下往上的一个过程:

Loader->Kernel->Native->Framework->App。

86.3.1 BootLoader 层

Boot Rom:当手机处于关机状态时,长按开机键开机,会引导芯片开始从固化在 Rom 里预设的代码开始执行,然后加载引导程序到 Ram.

Boot Loader:这是启动 Android 系统之前的引导程序,主要是检查 Ram、初始化参数等功能。

86.3.2 Linux Kernel 层

kernel 层指的就是 Android 内核层,到这里才刚刚进入 Android 系统

启动 Kernel 层的 swapper 进程(pid=1),系统初始化过程 Kernel 创建的第一个进程,用于初始化进程管理、内存管理,加载 Display、Camera、Binder 等驱动相关工作。

启动 kthreadd(pid=2),这是 Linux 系统的内核进程,会创建内核工作线程 kworkder、软中断线程 ksoftirqd 和 thermal 等内核守护进程。kthreadd 是所有内核进程的鼻祖。

 

86.3.3 Native层

这里的 Native 层主要包括 init 孵化来的用户空间的守护进程、HAL 层及开机动画等。启动 init 进程(pid=1),是 Linux 系统的用户进程,init 进程是所有用户进程的鼻祖。

init 进程会孵化出 ueventd、logd、healthd、installd、adbd、lmkd 等用户守护进程;

init 进程还会启动 ServiceManager(Binder 服务管家)、bootanim(开机动画)等重要服务。

init 进程孵化出 Zygote 进程,Zygote 进程是 Android 系统第一个 Java 进程(虚拟机进程),zygote 进程是所有 Java 进程的父进程。

86.3.4 Framework层

framework 主要包括 Zygote 进程、SystemServer 进程和 MediaServer 进程`。

Zygote 进 程

Zygote 进程是由 init 进程通过解析 init.rc 文件后 fork 生成的。Zygote 的任务主要包括:

加载 ZygoteInit 类,注册 Zygote Socket 服务端套接字。

加载虚拟机

preloadClassses

preloadResources

System Server进程

System Server 进程是由 Zygote 进程 fork 而来,System Server 是 Zygote 孵化的第一个进程。System Server 负责启动和管理整个 Java Framework,包含 ActivityManager、PowerManager 等服务。

Media Server进程

Media Server 进程由 init 进程 fork 而来,负责管理整个 C++ Framework,包含AudioFlinger、Camera Service 等服务。

86.3.5 App

Zygote 进程孵化出的第一个 App 进程是 Launcher,也就是用户看到的桌面 App。同时Zygote 进程还会创建 Browser、Phone、Email 等 App 进程。也就是说所有的 App 进程都是由 Zygote 进程 fork 生成的。

86.3.6 Syscall和JNI

Native 层和 Kernel 层有一个系统调用层,也就是 Syscall。Java 层和 native 层之间的纽带是 JNI。

86.4 Android四大组件

86.4.1 Activity

一个 activity 通常就是一个单独的页面

activity 之间通过 intent 进行通信

项目中每一个 activity 都必须要在 AndroidManifest.xml 文件中声明 否则系统将不识别也不知行该 activity

我做项目的时候通常是 activity 和 fragment 同时使用

fragment 可以是你能够将 activity 分离成多喝可重用的组件 每个都有他自己的生命周期和 UI     fragment 可以轻松的创建动态灵活的 UI 设计,可以适用不同的屏幕尺寸,从手机到平面电脑 。

fragment 是一个独立的模块,紧紧的与 activity 绑定在一起,可以运行中动态的移除,加入,交换等等

fragment 切换流畅 轻量切换 fragment 代替TabActivity 做导航性能更好

fragment 在 4.2 版本中新增嵌套 fragment 使用方法  能够生成更好的界面结果

86.4.2 service

service 用于后台完成操作 service 分为两种:

1.started(启动):当应用程序组件(如 activity)调用 startService()方法启动服务时 服务处于 started 状态

2.bound(绑定):当应用程序调用 bindService()方法绑定到服务时,服务处于 bound 状态。

startService()和 bindService()的区别:

1.started service(启动服务) 是由其他组件调用 startService()方法启动的 这导致服务的 onStartCommand 方法被调用 当服务是 started 状态时  其生命周期与启动他的组件无关  并且可以在后台无限期运行  记事启动服务器的组件已经被销毁 因此 服务需要在完成任务后调用 stopSelf()方法停止或者由其他组件调用 stopService()方法停止

2.使用 bindService()方法启动服务 调用者与被服务者绑定在了一起,调用者一旦退出,服务也就终止但求同年同月同日生,也求同年同月同日死。

开发人员需要在应用程序的 AndroidManifest.xml 配置文件中生命全部的,service 使用<service></service>标签

Service 通常位于后台运行,他一般不需要与用户交互,因此 service 组件没有图形界面 Service 组件需要集成 Service 基类 Service 组件通常用于为其他组件提供后台服务或者监控其他组件的运行状态。

86.4.3 content provider

android 平台提供了 Content Provider 使用一个应用程序的指定数据集提供给其他应用程序, 其他应用也可以通过 ContentResolver 类从该内容提供者中获取或者存入数据。

只有需要多个应用程序之间共享数据的时候才是需要内容提供者。例如,通讯录数据被多个应用程序使用,且必须存储在一个内容提供者中。它的好处是统一数据访问方式。

ContentProvider 实现数据共享 ContentProvider 用于保存和获取数据,并使用对所有的应用程序可见  这是不同应用程序之间共享数据的一种方式,因为 Android 没有提供所有应用程序之间共同访问的公共储存区

开发人员不会直接使用 ContentProvider 的对象,大多是通过 ContentResolver 对象实现对 ContentProvider 的操作。

ContentProvider 使用 URI 来唯一标识其数据集 这里的 URI 以 content://作为前缀标识该数据由 ContentProvider 来管理。

86.4.4 Broadcast Receiver

你的应用可以使用他对外部时间进行过滤,只对感兴趣的外部事件(如打电话,或者数据网络可用时) 进行接受并且做出响应, 广播接收器没有用户界面 ,跟他们可以启动一个 activity 或者 service 来响应她们接收到的信息 或者用 NotificationManager 来通知用户。

广播接收者的注册方法有两种 ,分别是动态注册和 AndroidManifest 文件进行静态注册。

动态注册广播接收器特点是当用来注册的 Activity 关掉后,广播也就失效了。

静态注册无需担忧广播接收器是否被关闭,只要设备是开启状态,广播接收器也是打开着的。也就是说哪怕 app 本身未启动,该 app 订阅的广播在触发时也会对它起作用。

86.4.5 四大组件的注册

四大基本组件都需要注册才能使用,每个 Activity、service、Content Provider 都需要在 AndroidManifest 文件中进行配置。AndroidManifest 文件中未进行声明的 activity、服务以及内容提供者将不为系统所见,从而也就不可用。而 broadcast receiver 广播接收者的注册分静态注册(在 AndroidManifest 文件中进行配置)和通过代码动态创建并以调用Context.registerReceiver()的方式注册至系统。需要注意的是在 AndroidManifest 文件中进行配置的广播接收者会随系统的启动而一直处于活跃状态,只要接收到感兴趣的广播就会触发(即使程序未运行)。

86.4.6 四大组件的激活

内容提供者的激活:当接收到 ContentResolver 发出的请求后,内容提供者被激活。而其它三种组件 activity、服务和广播接收器被一种叫做 intent 的异步消息所激活。

86.4.7 四大组件的关闭

内容提供者仅在响应 ContentResolver 提出请求的时候激活。而一个广播接收器仅在响应广播信息的时候激活。所以,没有必要去显式的关闭这些组件。Activity 关闭:可以通过调用它的 finish()方法来关闭一个 activity。服务关闭:对于通过 startService()方法启动的服务要调用 Context.stopService()方法关闭服务,使用 bindService()方法启动的服务要调用Contex.unbindService()方法关闭服务。

86.4.8 Android 中的任务(activity栈)

(a)任务其实就是 activity 的栈,它由一个或多个 Activity 组成,共同完成一个完整的用户体验。栈底的是启动整个任务的 Activity,栈顶的是当前运行的用户可以交互的Activity,当一个 activity 启动另外一个的时候,新的 activity 就被压入栈,并成为当前运行的 activity。而前一个 activity 仍保持在栈之中。当用户按下 BACK 键的时候,当前 activity 出栈,而前一个恢复为当前运行的 activity。栈中保存的其实是对象,栈中的 Activity 永远不会重排,只会压入或弹出。

(b)任务中的所有 activity 是作为一个整体进行移动的。整个的任务(即 activity 栈) 可以移到前台,或退至后台。

(c)Android 系统是一个多任务(Multi-Task)的操作系统,可以在用手机听音乐的同时,也执行其他多个程序。每多执行一个应用程序,就会多耗费一些系统内存,当同时执行的程序过多,或是关闭的程序没有正确释放掉内存,系统就会觉得越来越慢,甚至不稳定。为了解决这个问题,Android 引入了一个新的机制,即生命周期(Life Cycle)。

86.5 Android的binder机制

Binder 是 Android 系统进程间通信(IPC)方式之一。Linux 已经拥有的进程间通信 IPC 手段包括(Internet Process Connection): 管道(Pipe)、信号(Signal)和跟踪(Trace)、插口(Socket)、报文队列(Message)、共享内存(Share Memory)和信号量(Semaphore)。本文详细介绍 Binder 作为 Android 主要 IPC 方式的优势。

86.5.1 引言

基于 Client-Server 的通信方式广泛应用于从互联网和数据库访问到嵌入式手持设备内部通信等各个领域。智能手机平台特别是 Android 系统中,为了向应用开发者提供丰富多样的功能,这种通信方式更是无处不在,诸如媒体播放,视音频频捕获,到各种让手机更智能的传感器(加速度,方位,温度,光亮度等)都由不同的 Server 负责管理,应用程序只需做为Client 与这些 Server 建立连接便可以使用这些服务,花很少的时间和精力就能开发出令人眩目的功能。Client-Server 方式的广泛采用对进程间通信(IPC)机制是一个挑战。目前 linux 支持的 IPC 包括传统的管道,System V IPC,即消息队列/共享内存/信号量,以及 socket 中只有 socket 支持 Client-Server 的通信方式。当然也可以在这些底层机制上架设一套协议来实现 Client-Server 通信,但这样增加了系统的复杂性,在手机这种条件复杂,资源稀缺的环境下可靠性也难以保证。

另一方面是传输性能。socket 作为一款通用接口,其传输效率低,开销大,主要用在跨网络的进程间通信和本机上进程间的低速通信。消息队列和管道采用存储-转发方式,即数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存区拷贝到接收方缓存区,至少有两次拷贝过程。共享内存虽然无需拷贝,但控制复杂,难以使用。

IPC

数据拷贝次数

共享内存

0

Binder

1

Socket/管道/ 消息队列

2

还有一点是出于安全性考虑。终端用户不希望从网上下载的程序在不知情的情况下偷窥隐私数据,连接无线网络,长期操作底层设备导致电池很快耗尽等等。传统 IPC 没有任何安全措施,完全依赖上层协议来确保。首先传统 IPC 的接收方无法获得对方进程可靠的 UID 和 PID(用户 ID 进程 ID),从而无法鉴别对方身份。Android 为每个安装好的应用程序分配了自己的 UID,故进程的 UID 是鉴别进程身份的重要标志。使用传统 IPC 只能由用户在数据包里填入 UID 和 PID,但这样不可靠,容易被恶意程序利用。可靠的身份标记只有由 IPC 机制本身在内核中添加。其次传统 IPC 访问接入点是开放的,无法建立私有通道。比如命名管道的名称,systemV 的键值,socket 的 ip 地址或文件名都是开放的,只要知道这些接入点的程序都可以和对端建立连接,不管怎样都无法阻止恶意程序通过猜测接收方地址获得连接。

基于以上原因,Android 需要建立一套新的 IPC 机制来满足系统对通信方式,传输性能和安全性的要求,这就是 Binder。Binder 基于 Client-Server 通信模式,传输过程只需一次拷贝,为发送发添加 UID/PID 身份,既支持实名 Binder 也支持匿名 Binder,安全性高。

86.5.2 面向对象的 Binder IPC

Binder 使用 Client-Server 通信方式:一个进程作为 Server 提供诸如视频/音频解码,视频捕获,地址本查询,网络连接等服务;多个进程作为 Client 向 Server 发起服务请求,获得所需要的服务。要想实现 Client-Server 通信据必须实现以下两点:一是 server 必须有确定的访问接入点或者说地址来接受 Client 的请求,并且 Client 可以通过某种途径获知 Server 的地址;二是制定 Command-Reply 协议来传输数据。例如在网络通信中 Server 的访问接入点就是 Server 主机的 IP 地址+端口号,传输协议为 TCP 协议。对 Binder 而言,Binder 可以看成Server 提供的实现某个特定服务的访问接入点, Client 通过这个‘地址’向 Server 发送请求来使用该服务;对 Client 而言,Binder 可以看成是通向 Server 的管道入口,要想和某个Server 通信首先必须建立这个管道并获得管道入口。

与其它 IPC 不同,Binder 使用了面向对象的思想来描述作为访问接入点的 Binder 及其在Client 中的入口:Binder 是一个实体位于 Server 中的对象,该对象提供了一套方法用以实现对服务的请求,就象类的成员函数。遍布于 client 中的入口可以看成指向这个 binder 对象的‘指针’,一旦获得了这个‘指针’就可以调用该对象的方法访问 server。在 Client 看来, 通过 Binder‘指针’调用其提供的方法和通过指针调用其它任何本地对象的方法并无区别, 尽管前者的实体位于远端 Server 中,而后者实体位于本地内存中。‘指针’是 C++的术语, 而更通常的说法是引用,即 Client 通过 Binder 的引用访问 Server。而软件领域另一个术语‘句柄’也可以用来表述 Binder 在 Client 中的存在方式。从通信的角度看,Client 中的Binder 也可以看作是 Server Binder 的‘代理’,在本地代表远端 Server 为 Client 提供服务。本文中会使用‘引用’或‘句柄’这个两广泛使用的术语。

面向对象思想的引入将进程间通信转化为通过对某个 Binder 对象的引用调用该对象的方法,而其独特之处在于 Binder 对象是一个可以跨进程引用的对象,它的实体位于一个进程中,而它的引用却遍布于系统的各个进程之中。最诱人的是,这个引用和 java 里引用一样既可以是强类型,也可以是弱类型,而且可以从一个进程传给其它进程,让大家都能访问同一Server,就象将一个对象或引用赋值给另一个引用一样。Binder 模糊了进程边界,淡化了进程间通信过程,整个系统仿佛运行于同一个面向对象的程序之中。形形色色的 Binder 对象以及星罗棋布的引用仿佛粘接各个应用程序的胶水,这也是 Binder 在英文里的原意。

当然面向对象只是针对应用程序而言,对于 Binder 驱动和内核其它模块一样使用 C 语言实现,没有类和对象的概念。Binder 驱动为面向对象的进程间通信提供底层支持。

86.5.3 Binder通信模型

Binder 框架定义了四个角色:Server,Client,ServiceManager(以后简称 SMgr)以及 Binder 驱动。其中 Server,Client,SMgr 运行于用户空间,驱动运行于内核空间。这四个角色的关系和互联网类似:Server 是服务器,Client 是客户终端,SMgr 是域名服务器(DNS),驱动是路由器。

Binder 驱动

和路由器一样,Binder 驱动虽然默默无闻,却是通信的核心。尽管名叫‘驱动’,实际上和硬件设备没有任何关系,只是实现方式和设备驱动程序是一样的。它工作于内核态,驱动负责进程之间 Binder 通信的建立,Binder 在进程之间的传递,Binder 引用计数管理,数据包在进程之间的传递和交互等一系列底层支持。

ServiceManager 与实名 Binder

和 DNS 类似,SMgr 的作用是将字符形式的 Binder 名字转化成 Client 中对该 Binder 的引用,使得 Client 能够通过 Binder 名字获得对 Server 中 Binder 实体的引用。注册了名字的Binder 叫实名 Binder,就象每个网站除了有 IP 地址外还有自己的网址。Server 创建了Binder 实体,为其取一个字符形式,可读易记的名字,将这个 Binder 连同名字以数据包的形式通过 Binder 驱动发送给 SMgr,通知 SMgr 注册一个名叫张三的 Binder,它位于某个Server 中。驱动为这个穿过进程边界的 Binder 创建位于内核中的实体节点以及 SMgr 对实体的引用,将名字及新建的引用打包传递给 SMgr。SMgr 收数据包后,从中取出名字和引用填入一张查找表中。

细心的读者可能会发现其中的蹊跷:SMgr 是一个进程,Server 是另一个进程,Server 向 SMgr 注册 Binder 必然会涉及进程间通信。当前实现的是进程间通信却又要用到进程间通信,这就好象蛋可以孵出鸡前提却是要找只鸡来孵蛋。Binder 的实现比较巧妙:预先创造一只鸡来孵蛋:SMgr 和其它进程同样采用 Binder 通信,SMgr 是 Server 端,有自己的 Binder 对象(实体),其它进程都是 Client,需要通过这个 Binder 的引用来实现 Binder 的注册, 查询和获取。SMgr 提供的 Binder 比较特殊,它没有名字也不需要注册,当一个进程使用BINDER_SET_CONTEXT_MGR 命令将自己注册成 SMgr 时 Binder 驱动会自动为它创建Binder 实体(这就是那只预先造好的鸡)。其次这个 Binder 的引用在所有 Client 中都固定为 0 而无须通过其它手段获得。也就是说,一个 Server 若要向 SMgr 注册自己 Binder 就必需通过 0 这个引用号和 SMgr 的 Binder 通信。类比网络通信,0 号引用就好比域名服务器的地址,你必须预先手工或动态配置好。要注意这里说的 Client 是相对 SMgr 而言的,一个应用程序可能是个提供服务的 Server,但对 SMgr 来说它仍然是个 Client。

Client 获得实名 Binder 的引用

Server 向 SMgr 注册了 Binder 实体及其名字后,Client 就可以通过名字获得该 Binder 的引用了。Client 也利用保留的 0 号引用向 SMgr 请求访问某个 Binder:我申请获得名字叫张三的 Binder 的引用。SMgr 收到这个连接请求,从请求数据包里获得 Binder 的名字,在查找表里找到该名字对应的条目,从条目中取出 Binder 的引用,将该引用作为回复发送给发起请求的 Client。从面向对象的角度,这个 Binder 对象现在有了两个引用:一个位于 SMgr中,一个位于发起请求的 Client 中。如果接下来有更多的 Client 请求该 Binder,系统中就会有更多的引用指向该 Binder,就象 java 里一个对象存在多个引用一样。而且类似的这些指向Binder 的引用是强类型,从而确保只要有引用 Binder 实体就不会被释放掉。通过以上过程可以看出,SMgr 象个火车票代售点,收集了所有火车的车票,可以通过它购买到乘坐各趟火车的票-得到某个 Binder 的引用。

匿名Binder

并不是所有 Binder 都需要注册给 SMgr 广而告之的。Server 端可以通过已经建立的Binder 连接将创建的 Binder 实体传给 Client,当然这条已经建立的 Binder 连接必须是通过实名 Binder 实现。由于这个 Binder 没有向 SMgr 注册名字,所以是个匿名 Binder。Client 将会收到这个匿名 Binder 的引用,通过这个引用向位于 Server 中的实体发送请求。匿名Binder 为通信双方建立一条私密通道,只要 Server 没有把匿名 Binder 发给别的进程,别的进程就无法通过穷举或猜测等任何方式获得该 Binder 的引用,向该 Binder 发送请求。

86.5.4 Binder内存映射和接收缓存区管理

暂且撇开 Binder,考虑一下传统的 IPC 方式中,数据是怎样从发送端到达接收端的呢? 通常的做法是,发送方将准备好的数据存放在缓存区中,调用 API 通过系统调用进入内核中。

内核服务程序在内核空间分配内存,将数据从发送方缓存区复制到内核缓存区中。接收方读数据时也要提供一块缓存区,内核将数据从内核缓存区拷贝到接收方提供的缓存区中并唤醒接收线程,完成一次数据发送。这种存储-转发机制有两个缺陷:首先是效率低下,需要做两次拷贝:用户空间->内核空间->用户空间。Linux 使用 copy_from_user()和 copy_to_user()实现这两个跨空间拷贝,在此过程中如果使用了高端内存(high memory),这种拷贝需要临时建立/取消页面映射,造成性能损失。其次是接收数据的缓存要由接收方提供,可接收方不知道到底要多大的缓存才够用,只能开辟尽量大的空间或先调用 API 接收消息头获得消息体大小,再开辟适当的空间接收消息体。两种做法都有不足,不是浪费空间就是浪费时间。

Binder 采用一种全新策略:由 Binder 驱动负责管理数据接收缓存。我们注意到 Binder 驱动实现了 mmap()系统调用,这对字符设备是比较特殊的,因为 mmap()通常用在有物理存储介质的文件系统上,而象 Binder 这样没有物理介质,纯粹用来通信的字符设备没必要支持mmap()。Binder 驱动当然不是为了在物理介质和用户空间做映射,而是用来创建数据接收的缓存空间。先看 mmap()是如何使用的:

fd = open("/dev/binder", O_RDWR);

mmap(NULL, MAP_SIZE, PROT_READ, MAP_PRIVATE, fd, 0);

这样 Binder 的接收方就有了一片大小为 MAP_SIZE 的接收缓存区。mmap()的返回值是内存映射在用户空间的地址,不过这段空间是由驱动管理,用户不必也不能直接访问(映射类型为 PROT_READ,只读映射)。

接收缓存区映射好后就可以做为缓存池接收和存放数据了。前面说过,接收数据包的结构为 binder_transaction_data,但这只是消息头,真正的有效负荷位于 data.buffer 所指向的内存中。这片内存不需要接收方提供,恰恰是来自 mmap()映射的这片缓存池。在数据从发送方向接收方拷贝时,驱动会根据发送数据包的大小,使用最佳匹配算法从缓存池中找到一块大小合适的空间,将数据从发送缓存区复制过来。要注意的是,存放 binder_transaction_data 结构本身以及表 4 中所有消息的内存空间还是得由接收者提供,但这些数据大小固定,数量也不多,不会给接收方造成不便。映射的缓存池要足够大,因为接收方的线程池可能会同时处理多条并发的交互,每条交互都需要从缓存池中获取目的存储区,一旦缓存池耗竭将产生导致无法预期的后果。

有分配必然有释放。接收方在处理完数据包后,就要通知驱动释放 data.buffer 所指向的内存区。在介绍 Binder 协议时已经提到,这是由命令 BC_FREE_BUFFER 完成的。

通过上面介绍可以看到,驱动为接收方分担了最为繁琐的任务:分配/释放大小不等,难以预测的有效负荷缓存区,而接收方只需要提供缓存来存放大小固定,最大空间可以预测的消息头即可。在效率上,由于 mmap()分配的内存是映射在接收方用户空间里的,所有总体效果就相当于对有效负荷数据做了一次从发送方用户空间到接收方用户空间的直接数据拷贝,省去了内核中暂存这个步骤,提升了一倍的性能。顺便再提一点,Linux 内核实际上没有从一个用户空间到另一个用户空间直接拷贝的函数,需要先用 copy_from_user()拷贝到内核空间,再用 copy_to_user()拷贝到另一个用户空间。为了实现用户空间到用户空间的拷贝,mmap()分配的内存除了映射进了接收方进程里,还映射进了内核空间。所以调用 copy_from_user()将数据拷贝进内核空间也相当于拷贝进了接收方的用户空间,这就是 Binder 只需一次拷贝的“秘密”。

86.5.5 Binder接收线程管理

Binder 通信实际上是位于不同进程中的线程之间的通信。假如进程 S 是 Server 端,提供Binder 实体,线程 T1 从 Client 进程 C1 中通过 Binder 的引用向进程 S 发送请求。S 为了处理这个请求需要启动线程 T2,而此时线程 T1 处于接收返回数据的等待状态。T2 处理完请求就会将处理结果返回给 T1,T1 被唤醒得到处理结果。在这过程中,T2 仿佛 T1 在进程 S 中的代理,代表 T1 执行远程任务,而给 T1 的感觉就是象穿越到 S 中执行一段代码又回到了 C1。为了使这种穿越更加真实,驱动会将 T1 的一些属性赋给 T2,特别是 T1 的优先级 nice,这样T2 会使用和 T1 类似的时间完成任务。很多资料会用‘线程迁移’来形容这种现象,容易让人产生误解。一来线程根本不可能在进程之间跳来跳去,二来 T2 除了和 T1 优先级一样,其它没有相同之处,包括身份,打开文件,栈大小,信号处理,私有数据等。

对于 Server 进程 S,可能会有许多 Client 同时发起请求,为了提高效率往往开辟线程池并发处理收到的请求。怎样使用线程池实现并发处理呢?这和具体的 IPC 机制有关。拿socket 举例,Server 端的 socket 设置为侦听模式,有一个专门的线程使用该 socket 侦听来自 Client 的连接请求,即阻塞在 accept()上。这个 socket 就象一只会生蛋的鸡,一旦收到来自 Client 的请求就会生一个蛋 – 创建新 socket 并从 accept()返回。侦听线程从线程池中启动一个工作线程并将刚下的蛋交给该线程。后续业务处理就由该线程完成并通过这个单与Client 实现交互。

可是对于 Binder 来说,既没有侦听模式也不会下蛋,怎样管理线程池呢?一种简单的做法是,不管三七二十一,先创建一堆线程,每个线程都用 BINDER_WRITE_READ 命令读Binder。这些线程会阻塞在驱动为该 Binder 设置的等待队列上,一旦有来自 Client 的数据驱动会从队列中唤醒一个线程来处理。这样做简单直观,省去了线程池,但一开始就创建一堆线程有点浪费资源。于是 Binder 协议引入了专门命令或消息帮助用户管理线程池,包括:

·INDER_SET_MAX_THREADS

·BC_REGISTER_LOOP

·BC_ENTER_LOOP

·BC_EXIT_LOOP

·BR_SPAWN_LOOPER

首先要管理线程池就要知道池子有多大,应用程序通过 INDER_SET_MAX_THREADS 告诉驱动最多可以创建几个线程。以后每个线程在创建,进入主循环,退出主循环时都要分别使用 BC_REGISTER_LOOP,BC_ENTER_LOOP,BC_EXIT_LOOP 告知驱动,以便驱动收集和记录当前线程池的状态。每当驱动接收完数据包返回读 Binder 的线程时,都要检查一下是不是已经没有闲置线程了。如果是,而且线程总数不会超出线程池最大线程数,就会在当前读出的数据包后面再追加一条 BR_SPAWN_LOOPER 消息,告诉用户线程即将不够用了,请再启动一些,否则下一个请求可能不能及时响应。新线程一启动又会通过 BC_xxx_LOOP 告知驱动更新状态。这样只要线程没有耗尽,总是有空闲线程在等待队列中随时待命,及时处理请求。

关于工作线程的启动,Binder 驱动还做了一点小小的优化。当进程 P1 的线程 T1 向进程P2 发送请求时,驱动会先查看一下线程 T1 是否也正在处理来自 P2 某个线程请求但尚未完成(没有发送回复)。这种情况通常发生在两个进程都有 Binder 实体并互相对发时请求时。假如驱动在进程 P2 中发现了这样的线程,比如说 T2,就会要求 T2 来处理 T1 的这次请求。因为 T2 既然向 T1 发送了请求尚未得到返回包,说明 T2 肯定(或将会)阻塞在读取返回包的状态。这时候可以让 T2 顺便做点事情,总比等在那里闲着好。而且如果 T2 不是线程池中的线程还可以为线程池分担部分工作,减少线程池使用率。

86.5.6 数据包接收队列与(线程)等待队列管理

通常数据传输的接收端有两个队列:数据包接收队列和(线程)等待队列,用以缓解供需矛盾。当超市里的进货(数据包)太多,货物会堆积在仓库里;购物的人(线程)太多,会排队等待在收银台,道理是一样的。在驱动中,每个进程有一个全局的接收队列,也叫 to-do 队列,存放不是发往特定线程的数据包;相应地有一个全局等待队列,所有等待从全局接收队列里收数据的线程在该队列里排队。每个线程有自己私有的 to-do 队列,存放发送给该线程的数据包;相应的每个线程都有各自私有等待队列,专门用于本线程等待接收自己 to-do 队列里的数据。虽然名叫队列,其实线程私有等待队列中最多只有一个线程,即它自己。

由于发送时没有特别标记,驱动怎么判断哪些数据包该送入全局 to-do 队列,哪些数据包该送入特定线程的 to-do 队列呢?这里有两条规则。规则 1:Client 发给 Server 的请求数据包都提交到 Server 进程的全局 to-do 队列。不过有个特例,就是上节谈到的 Binder 对工作线程启动的优化。经过优化,来自 T1 的请求不是提交给 P2 的全局 to-do 队列,而是送入了 T2 的私有 to-do 队列。规则 2:对同步请求的返回数据包(由 BC_REPLY 发送的包)都发送到发起请求的线程的私有 to-do 队列中。如上面的例子,如果进程 P1 的线程 T1 发给进程P2 的线程 T2 的是同步请求,那么 T2 返回的数据包将送进 T1 的私有 to-do 队列而不会提交到 P1 的全局 to-do 队列。

数据包进入接收队列的潜规则也就决定了线程进入等待队列的潜规则,即一个线程只要不接收返回数据包则应该在全局等待队列中等待新任务,否则就应该在其私有等待队列中等待Server 的返回数据。还是上面的例子,T1 在向 T2 发送同步请求后就必须等待在它私有等待队列中,而不是在 P1 的全局等待队列中排队,否则将得不到 T2 的返回的数据包。

这些潜规则是驱动对 Binder 通信双方施加的限制条件,体现在应用程序上就是同步请求交互过程中的线程一致性:1) Client 端,等待返回包的线程必须是发送请求的线程,而不能由一个线程发送请求包,另一个线程等待接收包,否则将收不到返回包;2) Server 端,发送对应返回数据包的线程必须是收到请求数据包的线程,否则返回的数据包将无法送交发送请求的线程。这是因为返回数据包的目的 Binder 不是用户指定的,而是驱动记录在收到请求数据包的线程里,如果发送返回包的线程不是收到请求包的线程驱动将无从知晓返回包将送往何处。

接下来探讨一下 Binder 驱动是如何递交同步交互和异步交互的。我们知道,同步交互和异步交互的区别是同步交互的请求端(client)在发出请求数据包后须要等待应答端

(Server)的返回数据包,而异步交互的发送端发出请求数据包后交互即结束。对于这两种交互的请求数据包,驱动可以不管三七二十一,统统丢到接收端的 to-do 队列中一个个处理。但驱动并没有这样做,而是对异步交互做了限流,令其为同步交互让路,具体做法是:对于某个 Binder 实体,只要有一个异步交互没有处理完毕,例如正在被某个线程处理或还在任意一条 to-do 队列中排队,那么接下来发给该实体的异步交互包将不再投递到 to-do 队列中,而是阻塞在驱动为该实体开辟的异步交互接收队列(Binder 节点的 async_todo 域)中,但这期间同步交互依旧不受限制直接进入 to-do 队列获得处理。一直到该异步交互处理完毕下一个异步交互方可以脱离异步交互队列进入 to-do 队列中。之所以要这么做是因为同步交互的请求端需要等待返回包,必须迅速处理完毕以免影响请求端的响应速度,而异步交互属于‘发射后不管’,稍微延时一点不会阻塞其它线程。所以用专门队列将过多的异步交互暂存起来, 以免突发大量异步交互挤占 Server 端的处理能力或耗尽线程池里的线程,进而阻塞同步交互。

86.5.7 总结

Binder 使用 Client-Server 通信方式,安全性好,简单高效,再加上其面向对象的设计思想,独特的接收缓存管理和线程池管理方式,成为 Android 进程间通信的中流砥柱。

86.6 Android HAL实例解析

此实例很经典,认真分析一下对我们理解 HAL 很有帮助,这里推荐给大家。

本文希望通过分析台湾的 Jollen 的 mokoid 工程代码,和在 s5pc100 平台上实现过程种遇到的问题,解析 Andorid HAL 的开发方法。

86.6.1 HAL介绍

现有 HAL 架构由 Patrick Brady (Google) 在 2008 Google I/O 演讲中提出的。

Android 的 HAL 是为了保护一些硬件提供商的知识产权而提出的,是为了避开 linux 的 GPL 束缚。思路是把控制硬件的动作都放到了 Android HAL 中,而 linux driver 仅仅完成一些简单的数据交互作用,甚至把硬件寄存器空间直接映射到 user space。而 Android 是基于Aparch 的 license,因此硬件厂商可以只提供二进制代码,所以说 Android 只是一个开放的平台,并不是一个开源的平台。也许也正是因为 Android 不遵从 GPL,所以 Greg Kroah- Hartman 才在 2.6.33 内核将 Andorid 驱动从 linux 中删除。GPL 和硬件厂商目前还是有着无法弥合的裂痕。Android 想要把这个问题处理好也是不容易的。

总结下来,Android HAL 存在的原因主要有:

1.并不是所有的硬件设备都有标准的 linux kernel 的接口

2.KERNEL DRIVER 涉及到 GPL 的版权。某些设备制造商并不原因公开硬件驱动,所以才去用 HAL 方 式绕过GPL。

3.针对某些硬件,An 有一些特殊的需求。

86.6.2 HAL内容

HAL主要的储存于以下目录(注意:HAL 在其它目录下也可以正常编译):

●libhardware_legacy/ - 旧的架构、采取链接库模块的观念进行

●libhardware/ - 新架构、调整为 HAL stub 的观念

●ril/ - Radio Interface Layer

●msm7k QUAL 平台相关

主要包含以下一些模块:Gps、Vibrator、Wifi、Copybit、Audio、Camera、Lights、Ril、Overlay 等。

两种 HAL 架构比较

目前存在两种 HAL 架构,位于 libhardware_legacy 目录下的“旧 HAL 架构”和位于libhardware 目录下的“新 HAL 架构”。两种框架如下图所示:

旧HAL架构:

 新HAL架构:

 

libhardware_legacy 是将 *.so 文件当作 shared library 来使用,在 runtime(JNI 部份)以 direct function call 使用 HAL module。通过直接函数调用的方式,来操作驱动程序。当然,应用程序也可以不需要通过 JNI 的方式进行,直接加载 *.so (dlopen)的做法调用*.so 里的符号(symbol)也是一种方式。总而言之是没有经过封装,上层可以直接操作硬件。

现在的 libhardware 架构,就有 stub 的味道了。HAL stub 是一种代理人(proxy)的概念,stub 虽然仍是以 *.so 檔的形式存在,但 HAL 已经将 *.so 档隐藏起来了。Stub 向HAL 提供操作函数(operations),而 runtime 则是向 HAL 取得特定模块(stub)的operations,再 callback 这些操作函数。这种以 indirect function call 的架构,让 HAL stub 变成是一种包含关系,即 HAL 里包含了许许多多的 stub(代理人)。Runtime 只要说明类型,即 module ID,就可以取得操作函数。对于目前的 HAL,可以认为 Android 定义了 HAL 层结构框架,通过几个接口访问硬件从而统一了调用方式。

下面结合实例来分析 HAL 编程方法。

86.6.3 mokoid工程代码下载与结构分析

mokid项目概述

modkoid 工程提供了一个 LedTest 示例程序,是台湾的 Jollen 用于培训的。对于理解android 层次结构、Hal 编程方法都非常有意义。

下载方法

svn checkout http://mokoid.googlecode.com/svn/trunk/mokoid-read-only

结构分析

 

Android 的 HAL 的实现需要通过 JNI(Java Native Interface),JNI 简单来说就是 java 程序可以调用 C/C++写的动态链接库,这样的话,HAL 可以使用 C/C++语言编写,效率更高。在 Android 下访问 HAL 大致有以下两种方式:

(1)Android 的 app 可以直接通过 service 调用.so 格式的 jni

(2)经过 Manager 调用 service 

 

上面两种方法应该说是各有优缺点,第一种方法简单高效,但不正规。第二种方法实现起来比较复杂,但更符合目前的 Android 框架。第二种方法中,LegManager 和 LedService(java)在两个进程中,需要通过进程通讯的方式来通讯。

mokoid 工程中实现了上述两种方法。下面将详细介绍这两种方法的实现原理。

第一种方法:直接调用 service 方法的实现过程,下面分析第一种方法中,各层的关键代码。

(1)HAL 层

一般来说 HAL moudle 需要涉及的是三个关键结构体:

struct hw_module_t;
struct hw_module_methods_t; 
struct hw_device_t;

下面结合代码说明这 3 个结构的用法。部分代码经过修改,后面的章节会给出修改的原因。

文件:mokoid-read-only/hardware/modules/include/mokoid/led.h

/***************************************************************************/

struct led_module_t {
struct hw_module_t common;
};
//HAL 规定不能直接使用 hw_module_t 结构,因此需要做这么一个继承。

struct led_control_device_t {
//自定义的一个针对 Led 控制的结构,包含 hw_device_t 和支持的 API 操作

struct hw_device_t common;
/* attributes */
int fd; //可用于具体的设备描述符
/* supporting control APIs go here */
int (*set_on)(struct led_control_device_t *dev, int32_t led); 
int (*set_off)(struct led_control_device_t *dev, 	int32_t led);
};
#define LED_HARDWARE_MODULE_ID "led"
//定义一个 MODULE_ID,HAL 层可以根据这个 ID 找到我们这个 HAL stub 

文件:mokoid-read-only/hardware/modules/led/led.c

(2)JNI 层

文 件 :mokoid-read- only/frameworks/base/service/jni/com_mokoid_server_LedService.cpp

 

 

 

(3)service (属于 Framework 层) 

 

(4)APP 测试程序 (属于 APP 层)

文件:apps/LedClient/src/com/mokoid/LedClient/LedClient.java

 

第二种方法:经过 Manager 调用 service,HAL、JNI 两层和第一种方法一样,所以后面只分析其他的层次。

(1)Manager (属于 Framework 层) APP 通过这个 Manager 和 service 通讯。

文件:mokoid-read-only/frameworks/base/core/java/mokoid/hardware/LedManager.java

 

因为 LedService 和 LedManager 在不同的进程,所以要考虑到进程通讯的问题。Manager 通过增加一个 aidl 文件来描述通讯接口 

文 件 :mokoid-read- only/frameworks/base/core/java/mokoid/hardware/ILedService.aidl

 

SystemServer (属于 APP 层)

文件:mokoid-read-only/apps/LedTest/src/com/mokoid/LedTest/LedSystemServer.java

 

(3)APP 测试程序(属于 APP 层)

文件:mokoid-read-only/apps/LedTest/src/com/mokoid/LedTest/LedTest.java

 

86.7 Android语言特点

Android 应用的开发语言用的是 Java 语言,并且在 Android 中也用到了 Java 核心类库的大量的类,因此,在学习 Android 开发之前,可以先把 Java 基本语法学习一下是有必要的。Android 应用程序开发是以 Java 语言为基础的,当然 Android 的中间层和底层同样也是C/C++。

Java 简述

Android 应用使用的是 Java 语言,所以有必要在开始 Android 学习之前对 Java 有一定的了解。

Java 的历史

在这个世界上,熟悉 Java 历史的人非常多,如果要问一个人 Java 是从哪年诞生的,也许大多数人都会回答是 1995 年(这个非常好记,因为微软的 Windows95 也是在这一年发布的)。但事实上 Java 早在上个世纪 90 年代初就开始酝酿了。

1991 年 4 月,Sun 公司的 James Gosling 领导的绿色计划(Green Project)开始着力发展一种分布式系统结构,使其能够在各种消费性电子产品上运行。而 Green 项目组的成员一开始使用 C++语言来完成这个项目,由于 Green 项目组的成员都具有 C++背景,所以他们首先把目光锁定了 C++编译器,Gosling 首先改写了 C++编译器,但很快他就感到 C++的很多不足,需要研发一种新的语言 Java 来替代它,一杯飘香的咖啡成为了它的标志。

以下是 Java 大事记:

1995 年 5 月 23 日,Java 语言诞生 。

1996 年 1 月,第一个 JDK-JDK1.0 诞生 ;1996 年 4 月,10 个最主要的操作系统供应商申明将在其产品中嵌入 JAVA 技术 ;1996 年 9 月,约 8.3 万个网页应用了 JAVA 技术来制作。

1997 年 2 月 18 日,JDK1.1 发布 ;1997 年 4 月 2 日,JavaOne 会议召开,参与者逾一万人,创当时全球同类会议规模之纪录;1997 年 9 月,JavaDeveloperConnection 社区成员超过十万 。

1998 年 2 月,JDK1.1 被下载超过 2,000,000 次 ;1998 年 12 月 8 日,JAVA2 企业平台 J2EE 发布 。

1999 年 6 月,SUN 公司发布 Java 的三个版本:标准版、企业版和微型版(J2SE、J2EE、J2ME)。

2000 年 5 月 8 日,JDK1.3 发布 ;2000 年 5 月 29 日,JDK1.4 发布。

2001 年 6 月 5 日,NOKIA 宣布,到 2003 年将出售 1 亿部支持 Java 的手机 ;2001 年9 月 24 日,J2EE1.3 发布。

2002 年 2 月 26 日,J2SE1.4 发布,自此 Java 的计算能力有了大幅提升。

2004 年 9 月 30 日 18:00PM,J2SE1.5 发布,是 Java 语言的发展史上的又一里程碑事件。为了表示这个版本的重要性,J2SE1.5 更名为 J2SE5.0。

2005 年 6 月,JavaOne 大会召开,SUN 公司公开 Java SE 6。此时,Java 的各种版本已经更名以取消其中的数字“2”:J2EE 更名为 Java EE, J2SE 更名为 Java SE,J2ME 更名为 Java ME。

2006 年 11 月 13 日,SUN 公司宣布 Java 全线采纳 GNU General Public LicenseVersion 2,从而公开了 Java 的源代码。

在 Java 发展的二十几年的时间里,经历了无数的风风雨雨。现在 Java 已经成为一种相当成熟的语言了。在这 10 年的发展中,Java 被用于 Android,更是吸引了数百万的开发者,在网络计算遍及全球的今天,更是有 20 亿台设备使用了 Java 技术。作为 Java 技术的基础,J2SE 功不可没,让我们期望 J2SE 伴随 Java 平台一路走好!

关于 Java 有一些基本概念需要先了解,这对于 Java 后续学习很有帮助。

Java 语言规范

Java 语言规范,是对语言的技术定义。包括为 Java 程序设计的语法和语义。

应用程序接口 API

应用程序接口,包括为开发 Java 程序预先定义的类和接口。Java 的语言规范变化很少,但是 API 一直在更新,一直在扩展。

Java 的版本

Java 主要有三个版本,包括:Java 标准版本,Java SE;Java 企业版本,Java EE;Java 微型版本,Java ME。

Java 开发包 JDK

JDK 开发包,主要指的是 Sun 公司发布 Java 各个版本都带有的 Java 开发工具包。它是有一套独立程序构成的集合。每个程序都可以从命令行中调用,例如 Windows 下的cmd.exe,Ubuntu 下的控制台。还可以使用一些 Java 开发工具,例如 NetBeans,Eclipse等等,Java 开发工具是为了快速开发 Java 程序而提供的集成开发环境 IDE。

Java 接口的概念

Java 中的接口是一系列方法的声明,是一些方法特征的集合。可以类比 C 语言中,头文件中的函数。

Java 虚拟机 JVM

Java 的程序都是在 Java 虚拟机上运行的。

Java 的代码和在任何平台上运行都不需要做改变,只要这些平台支持 Java 虚拟机。

就像用户的系统是 32 位电脑装了 xp 系统,64 位电脑装了 win7 或者 win8,只要装了虚拟机,都可以运行 Ubuntu 操作系统(操作系统可以也看做一个程序),Ubuntu 系统和具体的平台没有任何关系,只要这个平台支持虚拟机即可。

学习 Android 需要的 Java 知识

Android 应用程序是以 Java 语言为基础的,所以需要有一定的 Java 基础。

下标中列出了 Java 语法基础,在学习 Android 之前对下表中的知识需要有一定的了解。

开发环境 Java SDK 下载和安装

环境变量的配置(path 和classpath)

编程基础 标识符命名规范

运算符

分支语句(if,switch)

循环语句(for,while)

函数的定义方法

面向对象基础

面向对象基本思想

类的定义方法

对象和类的关系

对象的创建方法

通过对象使用成员变量和成员函数的方法

构造函数的作用

函数的重载

static 的作用

this 的作用

继承

继承的作用

继承的语法特点

super 的使用方法

多态

对象的向上转型和向下转型

final 关键字的作用

Java 当中异常的定义

异常的分类以及各自的特点

try…catch…finally 结构的处理方法

throw 和 throws 的使用方法

自定义异常的使用方法

内部类的定义方法

匿名内部类的定义方法

内部类的常见使用方法

线程 线程的基本定义

线程运行状态介绍

线程间通信的方法

线程同步

线程死锁

IO 基本概念

输入流和输出流的作用

Java 当中 I O 流的分类方法

常见 IO

类 库 类

日期相关类的使用方法

数据库关系型数据库的基本概念

;