RFC4137协议的全称是"State Machine for Extensible Authentication Protocol(EAP)Peer and Authenticator",它描述了Peer端(即Supplicant端)和Authenticator端通过状态机(State Machine)这种方式来实现EAP处理流程的具体步骤和相关细节。本节将重点介绍Supplicant端SM的设计原理。为了行文方便,本节将使用SUPP代替Supplicant。
#### **一、Supplicant端SM设计原理**
对状态机来说,最重要的是其状态切换图。RFC4137中SUPP SM状态切换如图4-21所示。
图4-21的内容极为丰富,此处先介绍其中三个知识点。
* SUPP SM一共定义了13个状态,每个状态用一个框表示。框顶部所示为状态名,如INTIALZE、IDLE等。
* 每个状态都可以有自己的Entry Action(以后简称EA)。进入这个状态后,EA将被执行。EA由状态框中状态名下边的伪代码(采用了类C++语法)表示。以FAILURE状态为例,当状态机进入该状态后,将执行"eapFail=TRUE"伪代码,eapFail是SUPP SM定义的变量,下文将详细介绍图中涉及的变量和相关函数。
* 图中的UCT代表Unconditional Transition,即无条件状态转换。以DISCARD状态和IDLE状态为例,由于UCT的存在,当SUPP SM在DISCARD状态中执行完其EA后,将直接转换到IDLE状态。
对一个状态机而言,其状态的转换是因为外界条件发生变化导致。在规范中,这些外界条件由变量来表达。图4-21中出现了很多变量和EA所包括的一些函数,它们都由RFC4137文档定义。了解它们的作用对真正理解SUPP SM有直接和重要帮助。接下来的章节就将介绍这些变量和函数。
:-: ![](https://box.kancloud.cn/55ec2d01f88d9ceb2d71cbc488e8ec6a_1074x1349.jpg)
图4-21 SUPP SM状态切换
:-: ![](https://box.kancloud.cn/0d78c75d9d8ac105c56b823c7f69594d_339x276.jpg)
图4-22 RFC4137 SUPP SM模块划分
RFC4137将和SUPP SM相关的模块分为三层,如图4-22所示。图中最底层是Lower Layer(LL),这一层的作用是接收和发送EAP包。位于中间的SUPP SM层实现了Supplican状态机。最上层是EAP Method(EM)层,它实现了具体的EAP方法。
SUPP SM将与EM层和LL层交互。一个最典型的交互例子就是LL收到EAP数据包后,将该数据包交给SUPP SM层去处理。如果该EAP包需要EM层处理(例如具体的验证算法需要EM完成),则SUPP SM层将该包交给EM。EM处理完的结果将由SUPP SM转交给LL去发送。
>[info] 提示 RFC4137中,三层之间交互的手段可以是设置变量,或者是调用函数。
先来看SUPP SM与LL交互时所使用的变量。
**1、LL层和SUPP SM层交互变量**
LL层和SUPP SM层的交互比较简单,主要包括三个步骤。
1. LL层收到EAP数据包后,将其保存在eapReqData变量中,然后设置eapReq变量为TRUE。这个变量的改变对SUPP SM层来说是一个触发信号(signal)。SUPP SM可能会发生状态转换。
2. SUPP SM层从eapReqData中取出数据后进行处理。如果有需要回复的数据,则设置eapResp值为TRUE,否则设置eapNoResp值为TRUE。回复数据存储在eapRespData中。LL层将发送此回复包。
3. 如果SUPP SM完成身份验证后,它将设置eapSuccess或eapFailure变量以告知LL层其验证结果。eapSuccess为TRUE,表明验证成功。eapFailure为TRUE,则验证失败。
上述描述中所涉及的变量及其类型如表4-1所示。注意,此处的数据类型属于伪代码。
>[info] 提示 在WPAS中,LL层并非那些直接利用socket进行数据收发的模块,而是EAPOL模块。
EAP和EAPOL模块的关系将留待下一节再介绍。
:-: ![](https://box.kancloud.cn/f727a1c1971b9c12bf47ec7a413146fc_1266x625.jpg)
:-: ![](https://box.kancloud.cn/a33fa1554c5d2a3d3b1f941674856ad8_1260x446.jpg)
表4-11 LL层和SUPP层交互变量
表4-1中altAccept和altReject两个变量的命名非常晦涩难懂。RFC4137指出这两个变量的定义在RFC3748中。实际上,RFC3748从头至尾都没有出现过这两个变量。经过仔细研究,笔者发现[这篇文章](http://lists.frascone.com/pipermail/eap/msg02578.html )对此有一个说法,内容如下。
这两个变量取名为lowerLayerSuccess和lowerLayerFailure更合适,它们用于通知LL层Success或Failure信息。结合上述资料,笔者查阅了RFC3748 7.12节,在802.11网络中,Indicatioin(通知)Success和Failure的可能场景如下。
* 当supplicant收到Disassociate帧或者Deauthenticate帧时,表示lowerLayerFailure。
* 当收到4-Way Handshake第一个Message时,表示lowerLayerSuccess。
>[info] 规范阅读提示 RFC4137中,上述变量还可分为两种类型。
1. 由LL层暴露给SUPP SM层的变量(Variables from Lower Layer to Peer)。它们从表4-1中的eapReq开始,到altReject结束。原则上,LL层和SUPP SM层都可以修改这些变量。
2. 由SUPP SM层暴露给LL层的变量(Variables from Peer to Lower Layer)。它们从表4-1中的eapResp开始,到eapKeyAvailable结束。原则上,LL层和SUPP SM层都可以修改这些变量。另外,这些变量也可由SUPP SM层暴露给EM层来使用。
接着来看SUPP SM层和EM层交互变量。
**2、SUPP SM层和EM层交互变量**
SUPP SM层和EM层的交互也是通过变量来完成的。这些变量如表4-2所示。
:-: ![](https://box.kancloud.cn/7e3b01017600b98a103d16db09317739_1269x259.jpg)
:-: ![](https://box.kancloud.cn/11409c16f177f299ca4a87d58d28dcf6_1058x699.jpg)
表4-2 SUPP SM和EM层交互变量
注意,methodState和decision的值由具体的认证方法(即Method)来确定。
>[info] 提示 本书不讨论所有EAP方法的具体实现。感兴趣的读者可以深入研究EAP模块。不过对EAP SUPP SM来说,methodState和decision的取值情况才是最重要的,因为它们会直接影响SUPP SM的状态切换。
**3、SUPP SM其他变量和处理函数**
RFC4137还为SUPP SM还定义了其他一些变量(Peer State Machine Local Variables)及函数。变量定义见表4-3。
:-: ![](https://box.kancloud.cn/6b6611f3d19c028846599965330c057f_1271x430.jpg)
表4-3 SUPP SM层内部变量定义
:-: ![](https://box.kancloud.cn/0fbc5defdc7e0c8ab10766ad0efb0c6e_1051x677.jpg)
表4-4 SUPP SM处理函数的定义。
注意,表4-4中用伪代码展示了这些函数的使用案例,它们并不遵守C++语法。例如:
~~~
(rxReq,rxSuccess,rxFailure,reqId,reqMethod)=parseEapReq(eapReqData)
~~~
等号左边为parseEapReq函数的返回值,等号右边括号中的"eapReqData"为parseEapReq的输入参数。如果使用案例中没有等号,则表示该函数无返回值(具体实现时,可设置该函数的返回值为void)。
注意 图4-21中还包含一些其他函数,奇怪的是规范中并没有列举它们。不过,相信读者很容易理解这些数作用,此处不详述。现在,读者能看懂图4-21所示的状态图了吗?
**4、SUPP SM定义的状态**
SUPP SM定义的13个状态如表4-5所示。
:-: ![](https://box.kancloud.cn/3e7efa982ce5d518877bfa7c9ce9933d_1264x255.jpg)
:-: ![](https://box.kancloud.cn/988433fcb311acd3c2664a81cea86520_1264x507.jpg)
表4-5 SUPP SM状态
前面展示了RFC4137中和SUPP SM相关的内容。笔者觉得这个状态机定义太过烦琐。不过,本着简单、明了并且没有歧义的原则,这种做法似乎又无可厚非。从笔者经验来看,读者只要能看懂图4-21所示SUPP SM状态切换图即算掌握了EAP模块的精髓了。WPAS中EAP SUPPSM该如何实现呢?请看下节。
#### **二、EAP SUPP SM代码分析**
上文中曾提到过,RFC4137定义的状态机非常烦琐,而具体实现可以根据情况进行裁剪。不过,WPAS中的EAP SUPP SM却较为严格得遵循了RFC4137。先来看其定义的数据类型和数据结构。
**1、相关数据结构与数据类型**
图4-23所示为EAP SUPP SM定义的枚举变量类型。
:-: ![](https://box.kancloud.cn/fc26c58bb469842712d0ff82098cf687_1114x604.jpg)
图4-23 EAP SUPP SM枚举类型定义
图4-23中,左图定义了EapDecision、EapMethodState、Boolean、eapol_bool_var和eapol_int_var枚举类型,它们都和上节介绍的变量及类型有关。右图定义了EapType枚举变量,代表不同的EAP Method。
:-: ![](https://box.kancloud.cn/ef9b457c586514883ba07f7e9df72ca6_1039x694.jpg)
图4-24所示为WPAS中和SUPP SM相关的数据结构。
图4-24中,eap_sm是RFC4137 EAP SUPP SM的代表。从其成员变量的命名可知,它几乎完全是按照RFC4137来实现的。eap_sm定义了一个名为EAP_state的枚举类型成员变量。
eap_sm通过m成员变量指向一个eap_method链表。eap_method是一个由next指针链接起来的单向链表,每一个eap_method对象代表一种具体的EAP Method(EAP Method的注册请回顾4.3.2节“eap_register_methods函数分析”)。eap_method最重要的是其处理函数,WPAS对其略有修改。例如,process函数实际上完成了表4-4中m.check、m.process和m.buildResp的功能。
eap_method中,process函数第三个参数的类型是`eap_method_ret*`,代表一个`eap_method_ret`对象。由图4-24可知,它包括了ignore、methodState、decision以及allowNotifications变量。
eap_sm的eapol_cb对象指向一个eapol_callbacks对象,它是LL层的代表。不过,eapol_callbacks的定义看起来和RFC4137关系不大。
>[info] 注意 图4-24中eap_sm第一个成员变量EAP_State是一个枚举类型,其枚举值就是图4-21中SUPP SM的各个状态。
EAP SUPP SM初始化时,eap_sm的eapol_callbacks被设置为eapol_cb对象。代码如下所示。
**eapol_supp_sm.c::eapol_cb定义**
~~~
static struct eapol_callbacks eapol_cb = {
eapol_sm_get_config,eapol_sm_get_bool,eapol_sm_set_bool,eapol_sm_get_int,
eapol_sm_set_int,eapol_sm_get_eapReqData,eapol_sm_set_config_blob,
eapol_sm_get_config_blob,eapol_sm_notify_pending,eapol_sm_eap_param_needed,
eapol_sm_notify_cert
};
~~~
上述函数相关代码我们留待碰到它们时再介绍。
**2、WPAS状态机通用宏**
WPAS中有许多状态机,所以它定义了一些通用宏来帮助实现状态机相关的代码。这些宏的定义如下所示。
**state_machine.h**
~~~
/*
定义一个状态的EA,它是一个函数声明。
而STATE_MACHINE_DATA也是一个宏。对于EAP SSM来说,其类型是struct eap_sm。
global代表触发该状态的原因是否为UCT。
*/
#define SM_STATE(machine, state) \
static void sm_ ## machine ## _ ## state ## _Enter(STATE_MACHINE_DATA *sm,int global)
// 每个状态进入后执行的一段代码。一般是打印一些信息,并设置新的状态
#define SM_ENTRY(machine, state) \
if (!global || sm->machine ## _state != machine ## _ ## state) { \
sm->changed = TRUE; \ // changed变量用于记录状态机的状态是否发生变化
wpa_printf(MSG_DEBUG, STATE_MACHINE_DEBUG_PREFIX ": " #machine \
" entering state " #state); \
} \
sm->machine ## _state = machine ## _ ## state; // 设置状态机的状态
// SM_ENTER宏对应一次函数调用,调用的是SM_STATE宏定义的函数
#define SM_ENTER(machine, state) \
sm_ ## machine ## _ ## state ## _Enter(sm, 0)
// 对应一次函数调用,表示因UCT而直接进入某个状态
#define SM_ENTER_GLOBAL(machine, state) \
sm_ ## machine ## _ ## state ## _Enter(sm, 1) // 这个函数由SM_STATE宏声明
// 运行状态机。该宏定义一个函数
#define SM_STEP(machine) \
static void sm_ ## machine ## _Step(STATE_MACHINE_DATA *sm)
// 该宏对应一次函数调用,即sm_##machine_Step(sm),sm参数由调用函数内声明
#define SM_STEP_RUN(machine) sm_ ## machine ## _Step(sm)
~~~
下面通过SUPP SM的实现代码来认识下上述通用宏的用法。
**3、EAP SUPP SM的实现**
SUPP SM的状态比较多,此处仅列举DISABLED状态的实现代码以帮助读者理解通用宏的作用。对状态机来说,其状态对应的EA非常重要。如下代码所示为DISABLED状态对应的EA。根据上节对通用宏的介绍,EA由SM_STATE宏来定义。
**eap.c::SM_STATE(EAP,DISABLED)**
~~~
该宏对应的代码是
static void sm_EAP_DISABLED_Enter(STATE_MACHINE_DATA *sm,int global)
*/
SM_STATE(EAP, DISABLED)
{
SM_ENTRY(EAP, DISABLED); // 每个状态的EA都会执行SM_ENTRY代码段
/*
SM_ENTRY宏对应的代码是:
if (!global || sm->EAP_state != EAP_DISABLED) {
sm->changed = TRUE;
wpa_printf(MSG_DEBUG, STATE_MACHINE_DEBUG_PREFIX ": " "EAP" \
" entering state " "DISABLED"); // 这段日志对了解SUPP SM当前处于哪个状态非常重要
}
// EAP_state是eap_sm中成员变量。读者可参考图4-24
sm->EAP_state = EAP_DISABLED;
*/
sm->num_rounds = 0;
}
~~~
SM_STATE只是定义了状态机某个状态的EA,那么状态机是如何运作的呢?根据图4-21以及前文所述,状态机的状态切换主要是通过判断条件是否满足来完成。SM_STEP定义的函数就是用于检查状态机的这些条件变量,然后根据情况进行状态转换的。SUPP SM的SM_STEP宏对应的代码如下所示。
**eap.c::SM_STEP(EAP)**
~~~
/*
SM_STEP宏对应的函数定义为:
static void sm_EAP_Step(STATE_MACHINE_DATA *sm)
*/
SM_STEP(EAP)
{
/*
对应UCT的处理。eapol_get_bool函数将调用eapol_callbacks对象中的eapol_sm_get_
bool函数其内部返回eap_sm中eapRestart成员变量的值。
*/
if (eapol_get_bool(sm, EAPOL_eapRestart) && eapol_get_bool(sm, EAPOL_portEnabled))
/*
调用由SM_STATE(EAP,INITIALZE)定义的函数,以进入EAP_INITIALIZE状态。
GLOBAL的意思是UCT。请读者注意此处的判断条件:如果eap_sm的eapRestart和
portEnabled成员变量都为true,则直接进入INITIALIZE状态。它完全和图4-21
SUPP SM状态图一样。
*/
SM_ENTER_GLOBAL(EAP, INITIALIZE);
else if (!eapol_get_bool(sm, EAPOL_portEnabled) || sm->force_disabled)
SM_ENTER_GLOBAL(EAP, DISABLED);
else if (sm->num_rounds > EAP_MAX_AUTH_ROUNDS) {
if (sm->num_rounds == EAP_MAX_AUTH_ROUNDS + 1) {
......// 有一些EAP方法在认证错误时会有很多消息往来,WPAS对此做了一个限制
// 一旦这些错误消息往来超过50次(由EAP_MAX_AUTH_ROUDS),则直接进入FAILURE状态
sm->num_rounds++;
SM_ENTER_GLOBAL(EAP, FAILURE); // GLOBAL代表UCT的情况
}
} else eap_peer_sm_step_local(sm); // 对应其他非UCT的情况
}
~~~
此处简单看一下eap_peer_sm_step_local的代码。
**eap.c::eap_peer_sm_step_local**
~~~
static void eap_peer_sm_step_local(struct eap_sm *sm)
{
switch (sm->EAP_state) {
......
case EAP_IDLE:
// 图4-21中,idle状态可依据条件不同而跳转到其他多个状态
// 下面这个函数用于选择目标状态及跳转到它
eap_peer_sm_step_idle(sm);// 根据图4-21的idle状态跳转,读者能想象出该函数的代码实现吗
break;
case EAP_RECEIVED:
eap_peer_sm_step_received(sm);
break;
case EAP_GET_METHOD:
if (sm->selectedMethod == sm->reqMethod)
SM_ENTER(EAP, METHOD); // 直接进入METHOD状态
else
SM_ENTER(EAP, SEND_RESPONSE); // 直接进入SEND_RESPONSE状态
break;
......
}
}
~~~
eap_peer_sm_step_local用于处理那些非UCT导致的状态切换。
>[info] 提示 EAP SUPP SM的代码虽不复杂,但由于SUPP SM状态和触发条件(即定义的那些变量)太多,想通过看代码去跟踪SUPP SM间的状态跳转是一件非常困难的事情。相比而言,图4-21比代码要直观,更加容易把注意力集中在目标状态以及它对应的EA上。另外,SM_STATE代码段中包含的那段wpa_printf输出将告知EAP模块当前的状态。读者以后在分析WPAS日志时千万要注意。
#### **三、EAP SUPP SM总结**
EAP SUPP SM基于RFC4137而实现,其内部变量的定义以及状态切换逻辑都来源于规范。以笔者的经验来看,掌握RFC4137是理解WPAS中EAP SUPP SM实现的基石。另外,对具体的处理逻辑而言,SUPP SM最重要的内容还是各个状态对应的EA。正如前文所述,图4-21对SUPP SM的运行极为重要,希望读者认真学习。
另外,对WPAS中状态机的实现来说,SM_STATE用于定义某个状态的EA(即一个函数)。每个EA都会执行SM_ENTRY宏定义的一段代码。SM_ENTER和SM_ENTER_GLOBAL宏用于调用SM_STATE定义的函数。GLOBAL代表UCT的情况。SM_STEP宏用于运行整个状态机。请读者注意它和SM_ENTER的区别。SM_ENTER宏将直接调用某个指定状态(由SM_ENTER宏的参数决定)的EA,而SM_STEP则将根据SM中的变量情况来决定下一个要跳转的状态,然后调用它的SM_ENTER。下面来看EAPOL模块的实现。
- 前言
- 第1章 准备工作
- 1.1 Android系统架构
- 1.2 工具使用
- 1.2.1 Source Insight的使用
- 1.2.2 Eclipse的使用
- 1.2.3 BusyBox的使用
- 1.3 本书资源下载说明
- 第2章 深入理解Netd
- 2.1 概述
- 2.2 Netd工作流程
- 2.2.1 main函数分析
- 2.2.2 NetlinkManager分析
- 2.2.3 CommandListener分析
- 2.2.4 DnsProxyListener分析
- 2.2.5 MDnsSdListener分析
- 2.3 CommandListener中的命令
- 2.3.1 iptables、tc和ip命令
- 2.3.2 CommandListener构造函数和测试工具ndc
- 2.3.3 InterfaceCmd命令
- 2.3.4 IpFwd和FirewallCmd命令
- 2.3.5 ListTtysCmd和PppdCmd命令
- 2.3.6 BandwidthControlCmd和IdletimerControlCmd命令
- 2.3.7 NatCmd命令
- 2.3.8 TetherCmd和SoftapCmd命令
- 2.3.9 ResolverCmd命令
- 2.4 NetworkManagementService介绍
- 2.4.1 create函数详解
- 2.4.2 systemReady函数详解
- 2.5 本章总结和参考资料说明
- 2.5.1 本章总结
- 2.5.2 参考资料说明
- 第3章 Wi-Fi基础知识
- 3.1 概述
- 3.2 无线电频谱和802.11协议的发展历程
- 3.2.1 无线电频谱知识
- 3.2.2 IEEE 802.11发展历程
- 3.3 802.11无线网络技术
- 3.3.1 OSI基本参考模型及相关基本概念
- 3.3.2 802.11知识点导读
- 3.3.3 802.11组件
- 3.3.4 802.11 Service介绍
- 3.3.5 802.11 MAC服务和帧
- 3.3.6 802.11 MAC管理实体
- 3.3.7 无线网络安全技术知识点
- 3.4 Linux Wi-Fi编程API介绍
- 3.4.1 Linux Wireless Extensions介绍
- 3.4.2 nl80211介绍
- 3.5 本章总结和参考资料说明
- 3.5.1 本章总结
- 3.5.2 参考资料说明
- 第4章 深入理解wpa_supplicant
- 4.1 概述
- 4.2 初识wpa_supplicant
- 4.2.1 wpa_supplicant架构
- 4.2.2 wpa_supplicant编译配置
- 4.2.3 wpa_supplicant命令和控制API
- 4.2.4 git的使用
- 4.3 wpa_supplicant初始化流程
- 4.3.1 main函数分析
- 4.3.2 wpa_supplicant_init函数分析
- 4.3.3 wpa_supplicant_add_iface函数分析
- 4.3.4 wpa_supplicant_init_iface函数分析
- 4.4 EAP和EAPOL模块
- 4.4.1 EAP模块分析
- 4.4.2 EAPOL模块分析
- 4.5 wpa_supplicant连接无线网络分析
- 4.5.1 ADD_NETWORK命令处理
- 4.5.2 SET_NETWORK命令处理
- 4.5.3 ENABLE_NETWORK命令处理
- 4.6 本章总结和参考资料说明
- 4.6.1 本章总结
- 4.6.2 参考资料说明
- 第5章 深入理解WifiService
- 5.1 概述
- 5.2 WifiService的创建及初始化
- 5.2.1 HSM和AsyncChannel介绍
- 5.2.2 WifiService构造函数分析
- 5.2.3 WifiStateMachine介绍
- 5.3 加入无线网络分析
- 5.3.1 Settings操作Wi-Fi分析
- 5.3.2 WifiService操作Wi-Fi分析
- 5.4 WifiWatchdogStateMachine介绍
- 5.5 Captive Portal Check介绍
- 5.6 本章总结和参考资料说明
- 5.6.1 本章总结
- 5.6.2 参考资料说明
- 第6章 深入理解Wi-Fi Simple Configuration
- 6.1 概述
- 6.2 WSC基础知识
- 6.2.1 WSC应用场景
- 6.2.2 WSC核心组件及接口
- 6.3 Registration Protocol详解
- 6.3.1 WSC IE和Attribute介绍
- 6.3.2 802.11管理帧WSC IE设置
- 6.3.3 EAP-WSC介绍
- 6.4 WSC代码分析
- 6.4.1 Settings中的WSC处理
- 6.4.2 WifiStateMachine的处理
- 6.4.3 wpa_supplicant中的WSC处理
- 6.4.4 EAP-WSC处理流程分析
- 6.5 本章总结和参考资料说明
- 6.5.1 本章总结
- 6.5.2 参考资料说明
- 第7章 深入理解Wi-Fi P2P
- 7.1 概述
- 7.2 P2P基础知识
- 7.2.1 P2P架构
- 7.2.2 P2P Discovery技术
- 7.2.3 P2P工作流程
- 7.3 WifiP2pSettings和WifiP2pService介绍
- 7.3.1 WifiP2pSettings工作流程
- 7.3.2 WifiP2pService工作流程
- 7.4 wpa_supplicant中的P2P
- 7.4.1 P2P模块初始化
- 7.4.2 P2P Device Discovery流程分析
- 7.4.3 Provision Discovery流程分析
- 7.4.4 GO Negotiation流程分析
- 7.5 本章总结和参考资料说明
- 7.5.1 本章总结
- 7.5.2 参考资料说明
- 第8章 深入理解NFC
- 8.1 概述
- 8.2 NFC基础知识
- 8.2.1 NFC概述
- 8.2.2 NFC R/W运行模式
- 8.2.3 NFC P2P运行模式
- 8.2.4 NFC CE运行模式
- 8.2.5 NCI原理
- 8.2.6 NFC相关规范
- 8.3 Android中的NFC
- 8.3.1 NFC应用示例
- 8.3.2 NFC系统模块
- 8.4 NFC HAL层讨论
- 8.5 本章总结和参考资料说明
- 8.5.1 本章总结
- 8.5.2 参考资料说明
- 第9章 深入理解GPS
- 9.1 概述
- 9.2 GPS基础知识
- 9.2.1 卫星导航基本原理
- 9.2.2 GPS系统组成及原理
- 9.2.3 OMA-SUPL协议
- 9.3 Android中的位置管理
- 9.3.1 LocationManager架构
- 9.3.2 LocationManager应用示例
- 9.3.3 LocationManager系统模块
- 9.4 本章总结和参考资料说明
- 9.4.1 本章总结
- 9.4.2 参考资料说明
- 附录