《移动互联网技术》 第十章 系统与通信: 掌握Android系统的分层架构设计思想和基于组件的设计模式

《移动互联网技术》课程简介

《移动互联网技术》课程是软件工程、电子信息等专业的专业课,主要介绍移动互联网系统及应用开发技术。课程内容主要包括移动互联网概述、无线网络技术、无线定位技术、Android应用开发和移动应用项目实践等五个部分。移动互联网概述主要介绍移动互联网的概况和发展,以及移动计算的特点。无线网络技术部分主要介绍移动通信网络(包括2G/3G/4G/5G技术)、无线传感器网络、Ad hoc网络、各种移动通信协议,以及移动IP技术。无线定位技术部分主要介绍无线定位的基本原理、定位方法、定位业务、数据采集等相关技术。Android应用开发部分主要介绍移动应用的开发环境、应用开发框架和各种功能组件以及常用的开发工具。移动应用项目实践部分主要介绍移动应用开发过程、移动应用客户端开发、以及应用开发实例。 课程的教学培养目标如下: 1.培养学生综合运用多门课程知识以解决工程领域问题的能力,能够理解各种移动通信方法,完成移动定位算法的设计。 2.培养学生移动应用编程能力,能够编写Andorid应用的主要功能模块,并掌握移动应用的开发流程。 3. 培养工程实践能力和创新能力。  通过本课程的学习应达到以下目的: 1.掌握移动互联网的基本概念和原理; 2.掌握移动应用系统的设计原则; 3.掌握Android应用软件的基本编程方法; 4.能正确使用常用的移动应用开发工具和测试工具。

第十章 系统与通信

本章小结:

1**、本单元学习目的**

通过学习Android的系统架构和系统底层的进程通信,重点掌握Android提供的多种进程间通信方式,包括:IPC-Binder、AIDL、网络编程等技术。

2**、本单元学习要求**

(1) 掌握Android系统的分层架构设计思想和基于组件的设计模式;

(2) 掌握Android系统组件之间的交互方式与信息传递;

3**、本单元学习方法**

结合教材以及Android Studio软件,对进程间通信进行编程练习,运行调试,并在模拟器中观察运行情况。

4**、本单元重点难点分析**

重点

(1) Android****系统架构

Android 是基于Linux平台的开源手机操作系统。Android的系统架构和其操作系统一样,采用了分层架构。系统架构一共有四个层次,从高到低,分别是应用层、应用程序框架层、系统运行库层和内核层

  1. 应用层

Android平台不仅仅是操作系统,它也包含了许多应用程序,比如:电话拨号程序、SMS短信程序、联系人、电子邮件、日历、浏览器、照片、媒体播放器、闹钟等等。这些应用程序都是用Java语言编写,没有固化在系统内部,可以被开发人员开发的其他应用程序所代替,因此更加灵活和个性化。

  1. 应用程序框架层

应用程序框架是Android开发的基础,它采用组件的方式,以方便开发者重用。开发人员可以直接使用这些系统提供的组件,也可以通过继承来扩展功能。应用程序框架层包含的组件。

应用程序框架包括一系列的服务和管理器:Activity Manager(活动管理器)管理各个活动的生命周期以及操作功能;Content Provider(内容提供器)让不同应用程序可以分享数据;Package Manager(包管理器)用来获取Android系统中应用程序的信息;Resource Manager(资源管理器)提供应用程序使用的各种资源;View System(视图系统)包括列表(lists)、网格(grids)、文本框(text boxes)、按钮(buttons)等界面元素;此外,还有很多针对不同功能的管理模块为上层应用程序提供API接口,包括系统级服务进程的实现等等。它们一般由Java语言编写。

  1. 系统运行库层

系统运行库层大多采用C/C++实现,它包括两个部分:一个是程序库,另一个是Android 运行时环境。程序库主要包括基本的C库、多媒体库、2D和3D图形引擎、浏览器引擎、本地数据库等等。Android Runtime(运行时)分为核心库和Dalvik 虚拟机。核心库包含了Android的一些核心API,如Android.os、Android.net、Android.media等等。Dalvik 虚拟机负责解释和执行生成的Dalvik格式的字节码。采用Dalvik 虚拟机,每一个 Android 应用都拥有一个自己的 Dalvik 虚拟机实例。相较于传统的虚拟机,它是一种基于寄存器的Java虚拟机,并且针对移动设备做了优化处理。Android 4.4以后系统开始使用ART虚拟机。Dalvik采用即时编译技术,而ART虚拟机采用预编译技术。ART虚拟机极大的提升了应用的启动速度。

  1. 硬件抽象层和内核层

硬件抽象层是对Linux内核驱动程序的封装,用以屏蔽低层的实现细节。硬件设备厂商从商业的角度考虑不希望公布源代码,而只提供驱动程序的二进制代码。Linux内核源代码版权遵循GNU License,它要求在发布产品时必须公布源代码。因此,Android把对硬件支持的所有代码都放在硬件抽象层HAL。Android系统不再依赖于某一个具体的硬件驱动,而是依赖于HAL代码,这样第三方厂商将自己不开源的代码封装在HAL层,这样就只需要提供二进制代码。通过这种方式,使Android系统和底层的驱动分离,通过抽象层向上提供接口屏蔽了低层的实现细节。

Android 的核心依赖于 Linux 2.6 内核,核心系统服务,比如内存管理、进程管理、网路协议、驱动模型以及安全性都依赖于Linux内核。 此外,在内核层中还有电源管理、键盘驱动等等。在进程通信方面,虽然Linux提供了多种进程间通信方式,但是Android采用Binder来实现自己的进程间通信。把以上四个层次放在一起就构建了 Andriod 的系统架构。

(2) 进程间通信方法

进程间通信或者跨进程通信(Inter-Process Communication,IPC)是指两个进程之间进行数据交换的过程。操作系统都有相应的IPC机制,比如Windows操作系统通过剪贴板、管道等进行进程间通信;Linux操作系统的进程间通信方式包括:管道(Pipe)、信号(Signal)和跟踪(Trace)、消息队列(Message)、共享内存(Share Memory)和信号量(Semaphore)等等。

Android是基于Linux内核的移动操作系统。在Linux操作系统中,每个进程都运行在一个独立的内存中,并在其中完成各自的任务,进程之间不允许直接访问对方的数据。但是,有时进程间也需要传递数据或者委托完成某些任务,这时必须通过系统的底层操作来完成间接通信。

Android的进程间通信方式并不完全来自于Linux,它构建了自己的进程间通信方式。Android系统提供的进程间通信方式包括:Binder、AIDL(Android Interface definition language)、Bundle、文件共享、Messenger、ContentProvider和Socket等等。

Binder中文翻译为粘合剂,在Android系统中是一种进程间的通讯方式(IPC),用于“粘合”两个不同的进程。从Android Framework角度来看,Binder是ServiceManager连接各种Manager和相应ManagerService的桥梁。从Android应用层来说,Binder是客户端和服务端进行通信的媒介。当调用BindService时,服务端会返回一个Binder对象,通过这个对象,客户端可以获取服务端提供的服务或者数据,服务包括普通服务和基于AIDL的服务。

AIDL是Android内部进程通信接口的描述语言。通过AIDL可以处理服务器端接收的大量并发请求,也可以实现跨进程的方法调用。AIDL支持一对多并发通信,并且支持实时通信,但实现较为复杂。

Android系统的四大组件中Activity、Service、BroadcastReceiver都可以通过Intent传递Bundle数据。Bundle实现了Parcelable接口,可以在不同的进程间传输数据。Bundle简单易用,适用于四大组件的进程间通信。

采用共享文件方式通信,两个进程可以通过读/写同一个文件交换数据,例如:P进程把数据写入文件,Q进程通过读取这个文件来获取数据。由于 Android 系统的并发读/写没有限制,当多个进程对同一个文件进行写操作时,会导致数据异常。共享文件方式适用于交换简单的数据,不适合高并发场景,并且无法实现进程间即时通信。

Messenger是一种轻量级的IPC实现方式,它的底层通过AIDL来实现。由于Messenger采用串行处理方式,一次处理一个请求,因此在服务端不需要考虑线程同步的问题,而服务端也不能执行并发操作。Messenger就像邮递员,通过它在不同进程中传递“信件”(Message对象),只要将需要传递的数据放入Message对象,即可实现数据的进程间传输。Messenger常用于低并发的一对多即时通信,但是不能很好地处理高并发情况,不支持远程过程调用(RPC)。

ContentProvider用于不同应用间的数据共享,其底层实现使用了Binder。因为系统对共享做了封装,让开发人员不需要关心实现细节即可完成进程间通信,使得ContentProvider的使用方式比AIDL更简单。ContentProvider主要以表格的形式组织数据,对底层的数据存储方式没有任何要求,既可以使用SQlite数据库,也可以使用文件,甚至可以使用内存中的对象来存储。

Socket套接字是一种常用的网络通信机制,包括流式套接字(SOCK_STREAM)和数据报套接字(SOCK_DGRAM)两种,它们分别对应网络传输层中的TCP协议和UDP协议。Socket主要用于跨网络和本机上的进程间低速通信,其特点是开销较大,传输效率较低。Socket实现细节比较繁琐,不支持直接的远程过程调用。

Bundle用于传递数据,它保存的数据以键值对(key-value)的形式存在。四大组件中的三大组件(Activity、Service、BroadcastReceiver)都支持 Intent 中传递 Bundle 数据。此外,由于 Bundle 实现了 Parcelable 接口,因此它可以在进程间传输数据。

Bundle位于android.os包中,是一个final类,因此Bundle不能被继承。使用Bundle在Activity之间传递数据,传递的数据可以是boolean、byte、int、long、float、double、string等基本类型或它们对应的数组,也可以是对象或对象数组。当Bundle传递的是对象或对象数组时,必须实现Serializable 或Parcelable接口。

Bundle还提供clear函数,该方法用于移除Bundle中的所有数据。在Bundle内部维护了一个ArrayMap对象,它以键值对的方式存储数据,具体定义在其父类BaseBundle中:

// Invariant - exactly one of mMap / mParcelledData will be null

// (except inside a call to unparcel)

ArrayMap<String, Object> mMap = null;

用Bundle在Activity之间传递数据,首先使用Intent的putExtra函数来存放附加信息,通过它将Quiz类的参数信息放置到Bundle实例中。

Intent Intent = new Intent(“pers.cnzdy.tutorial.ACTION_QUIZ”);

Bundle bundle = new Bundle();

bundle.putInt(“id”, 1);

bundle.putString(“statement”, " Service是一个可以与用户交互的Android应用组件。");

Intent.putExtras(bundle);

startActivity(Intent);

在接收端,Activity将Bundle中的数据提取出来。

Intent intent = getIntent();

Bundle bundle = Intent.getExtras();

int id = bundle.getInt(“id”);

String name = bundle.getString(“statement”);

一个实体(用类表示),比如知识点类KPoint,如果要封装到Bundle消息中,需要实现Parcelable接口或者Serializable接口。在用Bundle传递对象时,KPoint类需要实现Serializable接口,然后用putSerializable(String key, Serializable value)来保存数据;在接收数据时,用Serializable getSerizlizble(String key)取出数据。

Bundle是一个数据容器,本身比较简单,没有复杂的生命周期。对开发者来说,只需要了解Bundle的基本功能,使用场景并掌握常用的数据存取方法。

难点

(1) 通信接口描述语言

AIDL(Android Interface Definition Language)是Android系统自定义的接口描述语言,用于定义服务端和客户端之间的通信接口,它可以生成用于进程间通信的代码。AIDL为避免开发人员重复编写代码,通过类似模板的方式来生成IInterface的实例代码。

AIDL语法与Java语言类似,其中AIDL文件以 .aidl 为后缀名。AIDL支持的基本数据类型包括:byte、char、short、int、long、float、double、boolean;其他支持的类型有String、CharSequence、实现Parcelable接口的数据类型、List 类型、以及Map类型。List和Map中存放的数据类型必须是AIDL支持的类型,或者是其它声明的AIDL对象。

AIDL文件分为两类:一类用来声明实现了Parcelable接口的数据类型,以供其他AIDL文件使用非默认支持的数据类型;另一类用来定义接口方法,声明提供服务的接口,以便客户端调用。在接口中,定向Tag用来标注这些方法的参数值,它表示跨进程通信中数据的流向,分为 in、out、inout 三种方式。in 表示数据只能由客户端流向服务端;out表示数据只能由服务端流向客户端,服务端获取到传递的数据后,对该数据的任何操作,会同步到客户端;而 inout表示数据可在服务端与客户端之间双向流通。基本数据类型、String、CharSequence或者其他AIDL文件定义的方法接口参数值的定向 Tag 默认是in;除了这些类型以外,其他参数值都需要明确标注使用哪种定向Tag。另外,在AIDL文件中需要明确标明引用到的数据类型所在的包名,即使两个文件处在同一个包名下。

下面通过Quiz示例来说明AIDL的使用方式。Quiz示例的服务端接收客户端请求,并提供查询Quiz列表的信息,同时可以让客户端添加新的题目。

首先实现服务端。第一步编写AIDL文件,将客户端的请求抽象成接口;然后编写一个 Service来实现接口功能,以处理客户端请求,并且返回Binder;最后,还需要在AndroidManifest中配置Service,以便将Service告知系统。

创建工程IPCServer,包名定义为 pers.cnzdy.ipcserver。在服务端创建一个 Quiz 类。因为Quiz类在服务端和客户端都要用到,所以需要在AIDL文件中声明Quiz类。创建时,需要先建立 Quiz AIDL 文件,然后再创建 Quiz类,以避免出现类名重复导致无法创建文件的问题。在包名上点击右键,新建一个AIDL文件,命名为Quiz。

系统会默认创建一个aidl文件夹,它的子文件夹是工程的包名pers.cnzdy.ipcserver,包名下面是Quiz.aidl文件。在Quiz.aidl文件中会生成一个默认函数basicTypes,将它删除;并且修改Quiz.aidl,将它改为声明Parcelable数据类型的AIDL文件。Quiz.aidl文件中的代码如下:

pers.cnzdy.ipcserver

parcelable Quiz;

现在定义Quiz类,并且实现 Parcelable 接口。Quiz类中包含一个 statement(题干)属性。

public class Quiz implements Parcelable { private String statement;

代码语言:javascript
复制
   public Quiz(String statement) {
     this.statement = statement;
   }

protected Quiz(Parcel in) {
statement = in.readString();
}

@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(statement);
}

@Override
public int describeContents() {
return 0;
}

public static final Creator<Quiz> CREATOR = new Creator<Quiz>() {
@Override
public Quiz createFromParcel(Parcel in) {
return new Quiz(in);
}

@Override
public Quiz[] newArray(int size) {
return new Quiz[size];
}
};

public String getStatement() {
return statement;
}

public void setStatement(String statement) {
this.statement = statement;
}

@Override
public String toString() {
return "Quiz statement:" + statement;
}

public void readFromParcel(Parcel dest) {
statement = dest.readString();
}

}

服务端要给客户端提供服务,包括获取Quiz列表和增加新Quiz两个函数,这两个函数定义在IQuiz.aidl文件中。

package pers.cnzdy.ipcserver;
import pers.cnzdy.ipcserver.Quiz;

interface IQuiz {
List getQuizList();
void addQuiz(inout Quiz quiz);
}

编译代码,系统将根据AIDL文件来生成代码,可以在generated目录中查看系统生成的文件。在进程通信中会用到生成的静态抽象类 Stub。接下来创建QuizService类供客户端远程绑定。

public class QuizService extends Service {
private final String TAG = “Server”;
private List QuizList;

public QuizService() {
}

@Override
public void onCreate() {
super.onCreate();
QuizList = new ArrayList<>();
initData();
}

private void initData() {
Quiz Quiz1 = new Quiz(“简述Intent过滤器的定义和功能。”);
Quiz Quiz2 = new Quiz(“说明Handler异步消息处理的流程。”);
Quiz Quiz3 = new Quiz(“简述Service的基本原理和用途。”);
Quiz Quiz4 = new Quiz(“简述移动计算的主要特点。”);
Quiz Quiz5 = new Quiz(“什么是多路复用?频分、时分和码分多路复用?”);
Quiz Quiz6 = new Quiz(“简述竞争信道的工作过程。”);

代码语言:javascript
复制
 QuizList.add(Quiz1);
QuizList.add(Quiz2);
QuizList.add(Quiz3);
QuizList.add(Quiz4);
QuizList.add(Quiz5);
QuizList.add(Quiz6);

}

private final IQuiz.Stub stub = new IQuiz.Stub() {
@Override
public List getQuizList() throws RemoteException {
return QuizList;
}

代码语言:javascript
复制
 @Override
public void addQuiz(Quiz quiz) throws RemoteException {
if (quiz != null) {
Log.e(TAG, "服务器新增一道题目");
QuizList.add(quiz);
} else {
Log.e(TAG, "接收到一个空对象");
}
}

};

@Override
public IBinder onBind(Intent intent) {
return stub;
}
}

最后,因为服务端的Service需要客户端远程绑定,所以客户端要能找到这个Service。在AndroidManifest文件中定义Service,并设置intent-filter标签。通过指定包名来设置intent-filter的action值。

代码语言:javascript
复制
<service android:name=".QuizService"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="pers.cnzdy.ipcserver.action" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>

接下来编写客户端代码。首先创建IPCClient工程,包名为pers.cnzdy.ipcclient;然后,拷贝服务端的整个aidl 文件夹,将其粘贴到IPCClient工程目录下,aidl文件夹和java文件夹同级。

另外,把服务端的Quiz类也复制到IPCClient工程:先在IPCClient中创建一个新的包,包名与服务端Quiz类所在的包名相同,然后将Quiz类复制到这个包中。

修改activity_client.xml布局文件,添加两个按钮。

代码语言:javascript
复制
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">

<Button
android:id="@+id/btn_getQuizList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="查询题目列表" />

<Button
android:id="@+id/btn_addQuiz"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="增加新的题目" />
</LinearLayout>

ClientActivity通过 Intent 的方式来启动服务端的Service。Service 的“pers.cnzdy.ipcserver.action”启动后,将服务端返回的 Binder 保存下来并转换成对应的实例quizOperator。如果要和服务端通信,直接通过quizOperator即可调用服务端的函数。

import pers.cnzdy.ipcserver.IQuiz;
import pers.cnzdy.ipcserver.Quiz;

public class ClientActivity extends AppCompatActivity {
private final String TAG = “Client”;

private IQuiz quizOperator;
private boolean connected;
private List quizList;

private ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
quizOperator = IQuiz.Stub.asInterface(service);
connected = true;
}

代码语言:javascript
复制
 @Override
public void onServiceDisconnected(ComponentName name) {
connected = false;
}

};

private View.OnClickListener clickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_getQuizList:
if (connected) {
try {
quizList = quizOperator.getQuizList();
} catch (RemoteException e) {
e.printStackTrace();
}
log();
}
break;
case R.id.btn_addQuiz:
if (connected) {
Quiz quiz = new Quiz(“简述RTS/CTS握手协议。”);
try {
quizOperator.addQuiz(quiz);
Log.e(TAG, “向服务端以InOut方式新增一道题目。”);
Log.e(TAG, “题目:” + quiz.getStatement());
} catch (RemoteException e) {
e.printStackTrace();
}
}
break;
}
}
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn_getQuizList).setOnClickListener(clickListener);
findViewById(R.id.btn_addQuiz).setOnClickListener(clickListener);
bindService();
}

@Override
protected void onDestroy() {
super.onDestroy();
if (connected) {
unbindService(serviceConnection);
}
}

private void bindService() {
Intent intent = new Intent();
intent.setPackage(“pers.cnzdy.ipcserver”);
intent.setAction(“pers.cnzdy.ipcserver.action”);
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
}

private void log() {
for (Quiz quiz : quizList) {
Log.e(TAG, quiz.toString());
}
}
}

运行客户端程序,显示两个按钮分别用于获取服务端的Quiz列表和新增Quiz。以上示例实现了客户端与服务端之间的通信,客户端获取到服务端的数据,同时也向服务端传送数据。

(2) Binder****进程间通信原理

Android建立了一套新的IPC机制——通过Binder机制来满足系统对传输性能、安全性和稳定性的要求。Binder采用面向对象的思想设计,以Client-Server模式进行通信,传输过程中只需要执行一次数据拷贝,在性能上仅次于共享内存。在安全性上,Binder在底层为发送方添加UID/PID身份,既支持实名Binder也支持匿名Binder,提高了通信的安全性。Binder采用Client-Server模式,服务端提供某个特定服务的访问接入点,客户端通过访问接入点(服务地址)向服务端发送请求来使用该服务。对客户端来说,Binder是通向服务端的管道入口,如果要和某个服务端通信,必须建立这个管道并获得管道入口。

Android 应用由多个组件(Activity、Service、Broadcast Receiver 和 Content Provide等)构成。进程A中的BpBinder表示“Binder的代理”,用于向远端发送请求;进程B中的Bbinder表示“Binder响应方”,主要用于提供响应服务。此外,Android 系统对应用层提供的各种服务,比如:ActivityManagerService、PackageManagerService 等都是基于Binder IPC机制来实现。

采用面向对象的实现方式,Android系统将进程间通信转化为:获取某个Binder对象的引用,然后再调用该对象的方法。Binder对象是一个可以跨进程引用的对象,它提供一组函数为客户端提供服务,其实体位于一个进程(Server)中。在客户端,可以通过Binder的引用来访问服务端。客户端的Binder引用就像指向这个Binder对象的“指针”,一旦获取了这个“指针”就可以通过调用该对象的函数来访问服务端。对开发者来说,通过Binder引用调用服务端提供的方法和通过指针调用任何本地对象的方法一样,只是前者的实体位于远端服务器中,而后者的实体位于本地内存。Binder引用可以从一个进程传给其它进程,就像把一个对象的引用赋值给另一个引用一样,这样多个进程可以通过Binder引用访问同一个服务端。而对Binder对象来说,它的引用将遍布于系统的各个进程之中。

从通信的角度来说,客户端中的Binder可以看作是服务端Binder的“代理”,它在本地代表远端服务器为客户端提供服务。Binder的通信方式消除了进程间的边界,淡化了通信过程,整个系统就像运行在一个程序中,Binder对象及其引用就是粘接各个应用的胶水。

从结构上来看,Binder由四个部分组成,分别是:Binder客户端、Binder服务端、Binder驱动和ServiceManager模块。客户端、服务端和Service Manager运行在用户空间,而Binder驱动程序运行在内核空间。进程通信的核心组件是Binder驱动程序。Service Manager提供辅助管理的功能。客户端和服务端在Binder驱动和Service Manager的基础上,完成通信。Android系统实现了Binder驱动程序和ServiceManager,开发者只需要在用户空间编写自己的客户端和服务端代码。

Binder驱动负责建立进程之间的Binder通信,在进程之间传递Binder,管理Binder的引用计数,在进程之间传递数据包和执行交互等一系列底层操作。在Android系统的应用框架层,对Binder的底层操作进行了封装,并且通过 JNI 技术来调用 Native(C/C++)层的 Binder 接口。在Native层,Binder以ioctl(设备驱动程序中管理设备I/O通道的函数)方式与内核层的Binder 驱动进行通信。Binder驱动为穿过进程边界的Binder创建位于内核中的实体节点以及ServiceManager对实体的引用;同时,将Binder的名字以及新建的引用打包传递给ServiceManager。

ServiceManager是一个守护进程,它用于管理系统中的各个服务端,并且向客户端提供查询服务端接口的能力。它将字符形式的Binder名字转换为客户端中该Binder的引用,这样客户端就能够通过Binder的名字来获取Server中Binder实体的引用。服务端向ServiceManager注册了Binder实体及其名字后,客户端就可以通过名字获得该Binder的引用。这时Binder对象有两个引用:一个位于ServiceManager,另一个位于发起请求的客户端。当有其他的客户端请求该Binder时,系统中就会有多个引用指向该Binder,就象Java程序中一个对象有多个引用;同时只要系统中存在Binder引用,就不会释放Binder实体。

ServiceManager与其它进程一样也是采用Binder进行通信。ServiceManager是服务端,它有自己的Binder对象(实体),而其它进程都是客户端,需要通过这个Binder的引用来完成Binder的注册、查询和获取。ServiceManager的Binder比较特殊,它没有名字也不需要注册。这个Binder的引用在所有客户端中都固定为0号引用而无须通过其它方式来获取。如果一个进程的Server向ServiceManager发起注册请求,它的Binder需要通过0引用号与ServiceManager的Binder通信。这时,相对于ServiceManager,进程中提供服务的Server是客户端。而0号引用就像域名服务器的地址,需要预先手工或动态配置。客户端也通过保留的0号引用向ServiceManager请求访问某个Binder。比如:申请获得名字为“IQuiz”的Binder引用。

在Binder工作过程中,为客户端和服务端分别设置一个代理:客户端代理Proxy和服务端代理Stub。进程A中的Proxy和进程B中的Stub通过Binder驱动和ServiceManager进行数据传输,即服务端和客户端直接调用Proxy和Stub的接口传输数据,。

ServiceManager是第一个启动的服务,其他服务端进程启动后,需要在ServiceManager中进行注册。当ServiceManager收到客户端的连接请求,从请求数据包里获得Binder的名字,在查找表里找到该名字对应的条目,从条目中取出Binder引用,将该引用发送给发起请求的客户端。

客户端通过ServiceManager来获取服务端的服务列表。当调用服务器端的函数时,客户端直接调用Proxy。ServiceManager屏蔽了Binder的实现细节,因此客户端不需要知道Binder的具体工作方式。

在Android系统中有两种Binder,分别是:实名Binder和匿名Binder。实名Binder是注册了名字的Binder,就像互联网上的网站除了IP地址外还有自己的网址。服务端创建Binder实体后,给它赋予一个容易记忆的名字,并将这个Binder连同它的名字以数据包的形式通过Binder驱动发送给ServiceManager,通知ServiceManager注册这个带有名字的Binder。ServiceManager收数据包后,从中取出名字和引用填入一张查找表中。

匿名Binder是没有向ServiceManager提交注册的Binder。当已经建立了Binder通信连接,返回Binder引用的时候,Binder驱动保存这个Binder实体的各种数据。服务端可以将Binder的引用传递给某个客户端,客户端收到这个匿名Binder引用,通过该引用向位于服务端中的实体发送请求。由于这个Binder没有向ServiceManager注册名字,所以称为匿名Binder。注意建立的Binder连接必须通过实名Binder来实现。匿名Binder为通信进程建立了一条私密通道,只要服务端不把匿名Binder发给其他进程,其他进程就无法通过猜测或穷举等方式获取该Binder引用,从而无法请求服务。

(3) 网络编程

Android支持TCP/UDP网络通信API,可以使用Socket来建立基于TCP/IP协议的网络通信;也可以使用数据报Socket(DatagramSocket)建立基于UDP协议的网络通信。另外,Android也支持URL、URLConnection等网络通信API。

Socket是基于TCP/IP通信协议是一种网络通信方式,通信双方通过Socket可以在通信两端建立起一个网络虚拟链路,两端的程序可以通过虚拟链路进行通信。下面创建一个Android应用客户端,通过Socket连接一个用Python程序实现一个服务端程序。Android应用的界面中包含一个按钮和一个文本框,按钮发起连接,文本框用于输入发送的数据和显示从服务器端接收到的字符串数据。要连接服务端程序,需要知道对方的IP地址和端口号。在连接服务器函数中,开启一个线程,在线程中调用tcpClient函数实现Socket通信。

public class SocketActivity extends AppCompatActivity {
private static final String TAG = “SocketActivity”;
TextView textView;

代码语言:javascript
复制
  // serverIp服务端的IP

private static final String serverIp = “192.168.1.105”;
private static final int port = 12345; //connection**另一端的端口

public void connectServerSocket(View view){
Thread thread = new Thread(){
@Override
public void run(){ tcpClient();}
};
thread.start();
}

首先,创建一个指定IP和端口的Socket对象,然后,构建一个缓冲读取器,读取的数据来自于Socket接收的数据。Socket的getInputStream函数,返回该Socket对象对应的输入流,通过该输入流可以从Socket中取出服务端发送的数据。getOutputStream函数类似,它返回的输出流可以向Socket中输出数据,也就是向服务端发送数据。

private void tcpClient(){
try{
Socket socketClient = new Socket(serverIp, port);

代码语言:javascript
复制
 Log.i(TAG,"新建套接字...");

BufferedReader in = new BufferedReader(
new InputStreamReader(
socketClient.getInputStream()));

BufferedWriter out = new BufferedWriter(
new OutputStreamWriter(
socketClient.getOutputStream()));

String outMsg = “TCP 连接端口:” + port + " Hello Server!"; *

  • Log.i(TAG,“开始发送数据…”);
    out.write(outMsg); *//发送数据
  • out.flush();

    代码语言:javascript
    复制
     Log.i(TAG, "Tcp客户端发送: " + outMsg);
    Log.i(TAG,"开始接收数据...");
    String inMsg = in.readLine();
    showResponse(inMsg);
    socketClient.close();

    } catch (UnknownHostException e) {
    e.printStackTrace();
    }
    catch (IOException e){ e.printStackTrace();}
    }

    通过输出缓冲对象out,可以向socket中写入发送给服务端的数据,服务端在接收到数据以后,会发送响应消息给Android应用,通过输入缓冲对象in,可以读取Socket传送过来的信息,调用showResponse函数将服务端的信息显示在界面上。注意完成通信以后,需要关闭Socket。在传输过程中,可能出现各种异常,比如服务端没有启动,网络中断等,因此,通过异常处理机制,能够处理各种通信异常情况。

    通常应用的网络通信在后台运行,这样就不会影响前台界面的用户操作。前后台要通信,比如将网络传输的数据显示在应用界面上,不能直接操作界面控件。在这里通过开启一个线程将数据传给文本显示控件。更标准的交互方式是通过handler等异步消息机制来实现。由于Socket通信需要访问互联网,因此还需要为该Android应用赋予访问互联网的权限。

    通过使用URL来访问一个指定的网址,并获取网页中的图片,在界面上设置了一个按钮和一个ImageView控件。点击按钮将访问指定的网址并下载图像显示在imageView控件上。在URL活动中,定义了一个URL对象和一个线程对象。与Socket通信一样,在线程中通过URL实现网络通信,为了显示网络传输的图片,通过handler来设置imageView控件上显示的图片。

    public class URLActivity extends AppCompatActivity {
    public static final int SHOWIMAGEMSG = 1;
    private String strURL;
    private URL url;

    private Bitmap bitmap;
    private String fileName;
    private ImageView imageView;

    Thread thread;
    Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
    if (msg.what == SHOWIMAGEMSG) {
    imageView.setImageBitmap(bitmap); } }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    … …
    imageView = findViewById(R.id.image_view_url_get);
    strURL = “https://edu-image.nosdn.127.net/” +
    “BB7D91BE8141EE86A261E3857BAFE188.png?” +
    “imageView&thumbnail=510y288&quality=100”;
    fileName = “mobilecomputing.png”;

    thread = new Thread() {
    public void run() {
    getImage();
    }
    };
    }

    在活动的onCreate函数中,指定了应用访问的URL地址,这里是一个网页中的图片。在线程中,调用getImage函数来下载该图片。下载图片时,首先根据网址创建url对象,调用openStream函数打开传输流,接着调用得出decodeStream函数将流数据解码生成bitmap对象,并通过handler发送“显示图像”消息,在界面上显示图片;最后,将下载的图片通过OutputStream保存到移动设备的data\data\ 目录下。Filename是指定的图像文件名。

    private void getImage() {
    try {
    url = new URL(strURL);
    InputStream is = url.openStream();
    bitmap = BitmapFactory.decodeStream(is);
    handler.sendEmptyMessage(SHOWIMAGEMSG);

    代码语言:javascript
    复制
     OutputStream os = openFileOutput(fileName, *MODE_PRIVATE*);
    byte[] buff = new byte[1024];
    int hasRead = 0;

    * while ((hasRead = is.read(buff)) > 0) {
    os.write(buff, 0, hasRead);
    }
    is.close();
    os.close();
    } catch (Exception e) { e.printStackTrace(); }
    }

    本章习题:

    1**、本单元考核点**

    Android系统的分层架构,每一层的功能模块。

    Android系统的各种进程间通信方式。

    2**、本单元课后习题**

    1、AIDL的全称是什么?如何工作?能处理哪些类型的数据? 
    答案:AIDL:Android Interface Definition Language,即Android接口描述语言。Android系统中的进程之间不能共享内存,因此,需要提供一些机制在不同进程之间进行数据通信。
    为了使其他的应用程序也可以访问本应用程序提供的服务,Android系统采用了远程过程调用( Remote Procedure Call,RPC) 方式来实现。与很多其他的基于RPC 的解决方案一样,Android 使用一种接口定义语言(Interface Definition Language,IDL)来公开服务的接口。因此,可以将这种可以跨进程访问的服务称为AIDL( Android Interface Definition Language) 服务。
    如果需要在一个Activity中,访问另一个Service中的某个对象,需要先将对象转化成AIDL可识别的参数(可能是多个参数),然后使用AIDL来传递这些参数,在消息的接收端,使用这些参数组装成自己需要的对象
    AIDL支持的数据类型:
    (1) 不需要import声明的简单Java编程语言类型(int, boolean等);
    (2) String、CharSequence不需要特殊声明;
    (3) List、Map和Parcelables类型,这些类型内所包含的数据成员也只能是简单数据类型,String等其他支持的类型。
    2、什么是嵌入式实时操作系统,Android操作系统属于实时操作系统吗?
    答案:嵌入式实时操作系统是指当外界事件或数据产生时,能够接受并以足够快的速度予以处理,其处理的结果又能在规定的时间之内来控制生产过程或对处理系统作出快速响应,并控制所有实时任务协调一致运行的嵌入式操作系统。主要用于工业控制、军事设备、航空航天等领域,对系统的响应时间有苛刻的要求,这就需要使用实时系统。又可分为软实时和硬实时两种,而android是基于linux内核的,因此属于软实时。
    3、谈谈Android的IPC机制。
    答案:IPC是内部进程通信的简称,是共享"命名管道"的资源。Android中的IPC机制是为了让Activity和Service之间可以随时的进行交互,故在Android中,该机制只适用于Activity 和Service之间的通信,类似于远程方法调用,类似于C/S模式的访问。通过定义AIDL接口文件来定义IPC接口。Servier端实现IPC接口, Client 端调用IPC接口本地代理。

    参考资源:

    1、林学森著.深入理解Android内核设计思想.北京:人民邮电出版社,2014.

    2、任玉刚著.Android开发艺术探索.北京:电子工业出版社,2015.