Android 物联网应用开发实例
Android
可以采用 Kotlin、Java、C++ 语言编写应用程序,Android SDK
会将这些代码连同相应的数据和资源文件编译为 Android
软件包,即一个带有.apk后缀的归档文件,也就是 Android
应用程序的安装文件。本质上 Android 系统是一种多用户的 Linux
系统,每个应用程序都运行在独立的 Linux 用户 ID
和进程之下,从而为每个 Android
应用都提供了独立的安全沙盒,体现了最小权限的设计原则。

鉴于 Google 官方提供了完善的文档,本文并不过多过深的涉及 Android SDK 开发的具体知识细节,仅会在简单介绍 Android 开发当中的一些基本概念之后,着重分析经典/低功耗蓝牙、NFC、WIFI、指纹识别、5G 等硬件外设的通信协议概念以及相应的实现步骤,并且展示一些比较典型的应用场景与示例代码,从而为读者在进行物联网相关项目的开发时,在移动设备应用控制端提供即有的现成经验。
Hello Android
打开 Android Studio,鼠标点击【+ Start a new Android Studio project】选项:

在 Create New Project 窗口当中,选择【Empty Activity】,然后点击【Next】:

在 Configure your project 窗口当中,填写如下信息:

经过上述步骤新建的 Hello Android 项目下面有如下 4 个比较重要的源文件:

MainActivity.java
app > java > com.example.myfirstapp > MainActivity,主
Activity 是应用程序的入口点,程序运行时会首先启动该 Activity
的实例并且加载其对应的布局。
1 | package com.example.helloandroid; |
activity_main.xml
app > res > layout > activity_main.xml,用于定义主
Activity 的 XML 界面布局文件。
1 |
|
AndroidManifest.xml
工程清单文件app > manifests > AndroidManifest.xml,用于描述应用程序的基本特性,定义应用程序所使用到的组件。
1 |
|
build.gradle
Gradle Scripts > build.gradle 存在 2
个同名文件,分别针对项目Project: Hello_Android和模块Module: app,Android
应用的每个模块都拥有各自的build.gradle文件,本项目当前仅拥有app一个模块。
build.gradle (Project: Hello_Android)
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. |
build.gradle (Module: app)
1 | apply plugin: 'com.android.application' |
核心概念
基本组件
Android
应用组件拥有活动Activity、服务Service、广播接收器BroadcastReceiver、内容提供器ContentProvider共
4 种基本类型:
- 活动
Activity:是用户交互的入口点,表示的是拥有界面的单个屏幕,通过继承Activity类来实现; - 服务
Service:表示一种运行在后台的组件,并不会提供界面,用于执行长时间运行的操作或者为其它进程执行任务,通过继承Service类来实现; - 广播接收器
BroadcastReceiver:Android 借助该组件向应用传递事件,从而允许应用响应系统范围的广播通知。虽然广播接收器不会显示界面,但是可以创建状态栏通知以提醒用户,通过继承BroadcastReceiver类实现; - 内容提供器
ContentProvider:用于管理共享的应用数据,可以将这些数据存储在文件系统、SQLite 数据库、网络或者应用可访问的其它存储位置。其它应用则可以通过内容提供器查询、修改数据,通过继承ContentProvider类实现;
组件启动
上述 4
种组件类型当中,Activity、Service、BroadcastReceiver三种均可以通过异步消息Intent进行启动,Intent会在运行时将各个组件绑定在一起,因此可以将Intent视为组件间操作的纽带。Intent通过Intent对象进行创建,该对象可以通过定义消息来启动特定组件(显式)或者组件类型(隐式)。
使用Intent可以启动Activity、Service、BroadcastReceiver组件,我们即可以在Intent使用类名显式声明目标组件,也可以使用隐式的Intent来描述所要执行的操作类型与待操作数据。如果当前存在多个可执行Intent描述操作的组件,则由用户通过过滤器选择具体使用哪一个。
对于Activity和Service,Intent会定义所要执行的操作,并且可以指定待操作数据的URI,以及当前正在启动组件所需的信息。对于BroadcastReceiver,Intent仅会定义当前等待广播的通知信息。而ContentProvider并非由Intent启动,它只会在成为内容解析器ContentResolver的请求目标时才会启动。
- 启动
Activity,如果需要让Activity返回结果,可以向startActivity()或者startActivityForResult()传递Intent,或者为其安排其它任务; - 通过向
startService()传递Intent启动Service,或者可以向bindService()传递Intent来绑定到该服务; - 发起广播,可以向
sendBroadcast()、sendOrderedBroadcast()、sendStickyBroadcast()等方法传递Intent; - 为
ContentProvider提供查询服务,可以在ContentResolver上调用query();
工程清单文件
Android
系统启动应用组件之前,必须通过读取工程清单文件AndroidManifest.xml确认组件存在,因此必须在该文件中声明所有组件,且该文件必须位于工程的根目录。除此之外,清单文件还拥有如下的用途:
- 声明应用所需的权限,例如:互联网访问权限或者联系人的读取权限;
- 声明应用所需的最低 API 级别;
- 声明应用所需的软硬件功能,例如:相机、蓝牙、多点触摸屏幕;
- 声明应用链接的 API 库,例如:Google 地图库;
下面代码中,<application>元素的android:icon属性用于标识应用的图标,<activity>元素的android:name属性指定了对应Activity子类的完整类名,android:label属性指定用于Activity的用户可见标签字符串。
1 |
|
活动Activity、服务Service、广播接收器BroadcastReceiver、内容提供器ContentProvider分别对应AndroidManifest.xml当中的<activity>、<service>、<receiver>、<provider>元素。如果没有在工程清单文件中提供这些内容,那么这些组件在系统运行时是不可见的,永远都不会得到执行。
组件 XML 声明
上述的Activity、Service、BroadcastReceiver组件都可以使用Intent进行启动,即可以通过在Intent中显式命名目标组件的类名来使用Intent,还可以使用隐式Intent来描述所要执行的操作类型和数据。通过隐式Intent,Android
可以在设备上查询并启动可以执行该操作的组件。如果存在多个组件可以执行Intent所描述的操作,则由开发人员选择具体使用哪个组件。
通过将接收到的Intent与设备上其它应用的AndroidManifest.xml工程清单文件上的Intent过滤器相比较,Android
系统就可以正确的查询出可以响应该Intent的组件。AndroidManifest.xml当中声明Activity时,可以添加<intent-filter>元素作为Activity的子元素,从而为该组件声明一个Intent过滤器。
例如,构建一个包含撰写新邮件Activity的电子邮件应用程序,可以通过声明Intent过滤器来响应名称为send的Intent来实现发送新邮件的目的:
1 |
|
如果设备上另一个应用创建包含有ACTION_SEND操作的Intent并将其传递给startActivity(),则系统就会启动该Activity完成邮件发送操作。
其它 XML 声明
Android
设备比较碎片化,并非所有设备都提供相同的特性与功能,为了防止将应用安装在缺少相关特性的设备上,必须在AndroidManifest.xml工程清单文件中声明设备与软件要求。Android
操作系统本身并不会读取其中的大部分声明,但是Google Play等应用商店会读取这些信息,从而便于用户搜索应用提供相应的过滤功能。
例如,如果当前设备的应用需要相机功能,并使用Android 2.1(API 级别 7)中引入的
API,则可以在AndroidManifest.xml中声明如下要求:
1 |
|
,没有相机且 Android 版本低于 2.1 的设备将无法从 Google Play 安装您的应用。不过,您可以声明您的应用使用相机,但并不要求必须使用。在此情况下,您的应用必须将 required 属性设置为 false,并在运行时检查设备是否拥有相机,然后根据需要停用任何相机功能。
应用资源
Android 应用当中的图片、音频等资源,Android SDK
都会为其定义唯一的整型ID,在工程当中可以利用该ID引用这些资源。例如,SDK
会为工程res/drawable/目录下的logo.png图像文件生成名为R.drawable.logo的整型资源
ID,然后在代码中就可以通过该 ID 引用图像。
通过代码与资源的分离,可以方便的为不同设备提供对应的备用资源,Android
支持许多不同的备用资源限定符。限定符是资源目录名称中加入的短字符串,用于定义这些资源适用的设备配置。例如:根据设屏幕方向与尺寸为Activity创建不同的布局,当需要更换布局时,可以对每个布局的目录名称采用限定符,这样系统就会根据当前设备的水平和垂直方向自动应用对应的布局。
经典蓝牙
传统蓝牙适用于较为耗电的操作,可用于 Android
设备之间数据流的传输等场景。Android SDK 提供的 Bluetooth API
可以完成蓝牙通信的 4
大任务:设置蓝牙、查找区域内的配对设备或者可用设备、连接设备、在设备之间传输数据。
蓝牙设备之间进行数据传输之前,首先必须通过配对形成通信通道,即将其中一台设备设置为可检测状态,另一台设备通过搜索发现该设备,两个设备配对期间会交换并且缓存安全密钥,以供下次连接使用。配对完成以后,两台设备即可开始进行数据传输。会话完成以后,两台设备仍将维持绑定状态,未来如果需要再次打开连接会话,则需要两个设备在有效通信距离内均未移除绑定,即可自动完成连接。
权限
Android
应用中使用蓝牙功能,必须在工程清单文件AndroidManifest.xml声明如下
3 个权限:
BLUETOOTH权限:需要该权限才能执行蓝牙通信任务;BLUETOOTH_ADMIN权限:用于启动设备发现或者操作设备的蓝牙设置;ACCESS_FINE_LOCATION权限:由于蓝牙扫描可用于收集用户位置信息,此类信息通常来自用户设备或者各种外设蓝牙信标;
1 | <manifest ... > |
蓝牙 Profile
蓝牙 Profile 是适用于蓝牙设备之间不同应用场景的一系列协议栈,Android 的蓝牙 API 为如下 Profile 提供了实现:
- 蓝牙耳机:提供
BluetoothHeadset类控制蓝牙耳机服务代理; - 蓝牙立体声音频传输:提供
BluetoothA2dp类控制蓝牙 A2DP 服务代理; - 健康设备:提供 Bluetooth Health API
控制蓝牙健康设备,包含有
BluetoothHealth、BluetoothHealthCallback、BluetoothHealthAppConfiguration类;
蓝牙 Profile 的基本使用步骤如下所示:
- 获取默认适配器;
- 设置
BluetoothProfile.ServiceListener监听BluetoothProfile客户端,在其连接或者断开服务时向其发送通知; - 使用
getProfileProxy()与 Profile 关联的设备对象建立连接; - 通过
onServiceConnected()获取 Profile 代理对象的句柄; - 获得 Profile 代理对象后,可以监视连接状态,并执行与其相关的其它操作;
下面的示例代码,Profile 的代理对象是 1
个用于控制耳机的BluetoothHeadset实例:
1 | BluetoothHeadset bluetoothHeadset; |
设置蓝牙
如果当前 Android 设备不支持蓝牙,则应该停用应用的蓝牙功能;如果设备支持蓝牙,但是已经停用该功能,则应在不离开应用的同时启用蓝牙。
所有蓝牙相关的Activity都需要使用BluetoothAdapter(代表当前设备的蓝牙适配器),通过调用静态的getDefaultAdapter()方法可以获取BluetoothAdapter对象,如果该方法返回null,则表示设备不支持蓝牙。
1 | BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); |
接下来需要调用isEnabled()判断蓝牙是否启用,该方法返回false表示蓝牙处于停用状态,那么调用startActivityForResult()可以启用蓝牙,并且传入ACTION_REQUEST_ENABLE作为
Intent 参数向系统设置请求启用蓝牙。
1 | if (!bluetoothAdapter.isEnabled()) { |
此时系统显示【请求用户允许启用蓝牙】的对话框,如果选择【Yes】就会启用蓝牙,完成后焦点将会返回应用。
传递给startActivityForResult()的REQUEST_ENABLE_BT常量为局部定义的正整数,Android
系统会将该常量回传至onActivityResult()的requestCode参数。成功启用蓝牙以后,Activity
就会在onActivityResult()回调中接收到RESULT_OK结果代码,如果蓝牙启动失败,则返回的结果代码为RESULT_CANCELED。
注意:除此之外,每当系统蓝牙状态发生变化时,系统都会广播
ACTION_STATE_CHANGED这个 Intent。该广播包含EXTRA_STATE(目前的蓝牙状态)和EXTRA_PREVIOUS_STATE(之前的蓝牙状态)两个额外字段。这些额外字段的取值包含STATE_TURNING_ON、STATE_ON、STATE_TURNING_OFF、STATE_OFF。
查找设备
BluetoothAdapter还可用于扫描附近可被发现的蓝牙设备,这些设备会共享名称、类型、MAC
地址来响应扫描请求。当扫描设备与可被发现设备建立连接以后,Android
系统就会自动向用户显示配对请求。完成配对以后,Android
系统会保存该设备的名称、类型、MAC 地址,借助这些设备的 MAC
地址,可以随时向其发起连接,而无需再次执行发现操作。
调用BluetoothAdapter提供的getBondedDevices(),可以返回一组表示已经配对设备的BluetoothDevice对象,进而可以查询每台设备的名称和
MAC 地址。
1 | Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices(); |
注意:调用
startDiscovery()可以发现附近的蓝牙设备(12 秒),该操作为异步的进程,最后返回一个用于标示发现进程是否成功启动的布尔值。
Android 系统会为每台设备广播ACTION_FOUND这个
Intent,因此必须为其注册一个BroadcastReceiver,以便接收每台被发现设备的相关信息。该
Intent
包含EXTRA_DEVICE(包含BluetoothDevice类)和EXTRA_CLASS(包含BluetoothClass类)两个额外字段。下面代码段展示了发现设备时,如何处理ACTION_FOUND广播:
1 |
|
使用ACTION_REQUEST_DISCOVERABLE这个 Intent
调用startActivityForResult(Intent, int)方法,可以将本机设备设置为可检测状态,这样可以避免进入到设置界面进行相关操作。Android
设备默认处于可检测模式的时间为120秒,通过设置EXTRA_DISCOVERABLE_DURATION这个额外属性定义持续时间,最高设置达3600秒。下面示例将
Android 设备的蓝牙可被检测时间设置为300秒:
1 | Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); |
此时 Android
系统会显示【请求用户允许将设备设为可检测模式】对话框,如果用户选择【Yes】,则设备进入可检测模式,并在指定时间内保持该模式。然后
Activity
将会执行onActivityResult()回调函数。如果用户选择【No】则结果代码为RESULT_CANCELED。
如果希望设备检测状态发生变化时接收通知,则可以考虑为ACTION_SCAN_MODE_CHANGED这个
Intent
注册BroadcastReceiver。该Intent包含EXTRA_SCAN_MODE和EXTRA_PREVIOUS_SCAN_MODE两个额外字段,每个属性可以包含这些值:SCAN_MODE_CONNECTABLE_DISCOVERABLE(设备处于可检测模式)、SCAN_MODE_CONNECTABLE(设备未处于可检测模式,但是仍然能够接收连接)、SCAN_MODE_NONE(设备未处于可检测模式,并且无法接收到连接)。
连接设备
两台连接的设备必须分别同时实现服务端(开放服务器
Socket 服务)和客户端(使用服务器设备的 MAC
地址发起连接),当打开设备之间连接的 RFCOMM
通道以后,通过BluetoothSocket即可完成双向的流式数据传输。
服务端
服务端设备需要保持开放的BluetoothServerSocket监听听传入的连接请求,并在接受到请求后提供已连接的BluetoothSocket(即从BluetoothServerSocket获取BluetoothSocket)。设置
Socket 服务并接受连接,需要依次实现如下步骤:
- 调用
listenUsingRfcommWithServiceRecord(String, UUID)获取BluetoothServerSocket。其中参数String是服务的可识别名称,参数UUID是 128 位的通用唯一标识符,用于对应用的蓝牙服务进行唯一化标识;这 2 个参数都会被写入新服务发现协议 (SDP)的数据库。 - 调用
accept()监听连接请求,该函数为阻塞调用,当服务器接受连接或者发生异常,该调用就会中断返回。仅当客户端发送包含UUID的连接请求,且该UUID与服务端注册的UUID匹配时,服务端才会接受连接。连接成功以后,accept()返回已经连接的BluetoothSocket; - 调用
close()关闭连接,释放服务端 Socket 及其占用的资源,但是并不会关闭accept()返回的已连接BluetoothSocket。RFCOMM 只允许每个通道存在 1 个已连接客户端,因此服务端接收到已连接的 Socket 之后,就可以立刻在BluetoothServerSocket上调用close();
1 | private class AcceptThread extends Thread { |
注意:
BluetoothServerSocket和BluetoothSocket中的所有方法都是线程安全的方法。由于accept()是阻塞调用,因此不能在主 Activity 界面线程执行该调用,通常需要在一个新的线程中完成所有涉及BluetoothServerSocket或者BluetoothSocket的工作。如果要取消accept()等被阻塞的调用,同样通过另一个线程,在BluetoothServerSocket或者BluetoothSocket上调用close()。
客户端
客户端必须首先获取表示该远程设备的BluetoothDevice对象,然后从中获取BluetoothSocket并且发起连接,基本步骤如下所示:
- 使用
BluetoothDevice,通过调用createRfcommSocketToServiceRecord(UUID)获取BluetoothSocket。该方法会初始化BluetoothSocket对象,以用于客户端连接至BluetoothDevice。此处UUID必须与服务端listenUsingRfcommWithServiceRecord(String, UUID)中的UUID保持一致。 - 通过调用
connect()发起连接,客户端调用该方法以后,Android 会在新服务发现协议(SDP)数据库中查找UUID匹配的远程服务,如果查找并且连接成功,就会共享 RFCOMM 通道,同时connect()方法将会返回。如果连接失败,或者connect()超时约 12 秒以后,则该方法将会引发IOException。connect()也是阻塞调用,因此需要在主 Activity 之外的线程中执行此连接操作。
1 | private class ConnectThread extends Thread { |
数据读写
当成功连接多台设备以后,每台设备都会拥有已经完成连接的BluetoothSocket,接下来通过BluetoothSocket读写数据的步骤如下所示:
- 使用
getInputStream()和getOutputStream(),分别获取通过 Socket 处理数据传输的InputStream和OutputStream类; - 使用
read(byte[])和write(byte[])读取或者写入数据流,由于这 2 个方法都属于阻塞调用,因此需要使用单独的线程来进行读写操作。
1 | public class MyBluetoothService { |
低功耗蓝牙
NFC
Android 物联网应用开发实例




