本节书摘来华章计算机出版社《深入理解Android:卷III A》一书中的第3章,第3.3节,作者:张大伟 更多章节内容可以访问云栖社区“华章计算机”公众号查看。1
3.3音频外设的管理
这一节将探讨AudioService的另一个重要功能,那就是音频外设的管理。看过卷I第7章的读者应该对音频外设这个概念并不陌生。在智能机全面普及的时代,对有线耳机、蓝牙耳机等音频外设的支持已经是手机的标准,有些机型甚至支持HDMI、USB声卡等输出接口。再加上手机本身自带的扬声器与听筒,这样一来,一台手机上同时能进行音频输出的设备往往会有三四种甚至更多。如何协调这些设备的工作,使其符合用户的使用习惯、满足用户的需求变得非常重要。
卷I的第7章详细介绍过AudioPolicy如何进行设备的路由切换,然而并没有讨论音频设备为什么出现在AudioPolicy的设备候选列表中,这一节将以有线耳机为例讨论这个问题。
3.3.1 WiredAccessoryObserver 设备状态的监控
- WiredAccessoryObserver简介
这要从WiredAccessoryObserver开始讲起,它是内核通知有线耳机插入事件所到达的第一个环节。
WiredAccessoryObserbver继承自UEventObserver。UEventObserver是Android用来接收UEvent的一个工具类。UEventObserver类维护着一个读取UEvent的线程,注意这个线程是UEventObserver的一个静态成员,也就是说,一个进程只有一个。当调用UEventObserver的startObserving()函数开始监听时,会告诉这个线程UEventObserver关心什么样的UEvent,当匹配的事件到来时,监听线程会通过回调UEventObserver的onUEvent函数进行通知。读者可以看一下UEventObserver的源代码以了解其具体实现,这并不复杂。
WiredAccessoryObserver接收内核上报的和耳机/HDMI/USB相关的UEvent事件,并将其翻译成设备的状态变化。由于每种外设都有自己的UEvent与状态文件,因此WiredAccessoryObserver定义了一个内部类名为UEventInfo, 并且为自己感兴趣的每一个音频外设创建一个实例,其内部保存了对应外设的名字、UEvent地址及状态文件的地址。每当有合适的UEvent到来时,WiredAccessoryObserver就会查找匹配的UEventInfo实例,并且更新可用设备的状态列表,同时通知AudioService。
关于可用外设的状态列表,虽然称为列表,事实上,它只是一个整型的变量,名为mHeadsetState。在可用外设的状态列表中用一个二进制标志位表示某个外设的状态可用与否,这与AudioPolicyManager的mAvailableOutputDevices的用法是一样的。下面是各种外设的标志位的定义:
private static final int BIT_HEADSET = (1 << 0);
private static final int BIT_HEADSET_NO_MIC = (1 << 1);
private static final int BIT_USB_HEADSET_ANLG = (1 << 2);
private static final int BIT_USB_HEADSET_DGTL = (1 << 3);
private static final int BIT_HDMI_AUDIO = (1 << 4);
private static final int SUPPORTED_HEADSETS = (BIT_HEADSET|BIT_HEADSET_NO_MIC|
BIT_USB_HEADSET_ANLG|BIT_USB_HEADSET_DGTL|
BIT_HDMI_AUDIO);
private static final int HEADSETS_WITH_MIC = BIT_HEADSET;
举个例子,如果mHeadsetState等于0x00000002,也就是BIT_HEADSET_NO_MIC,表示目前手机上插入一个不带麦克风的耳机。而如果mHeadsetState等于0x00000011,也就是HEADSETS_WITH_MIC | BIT_HDMI_AUDIO,则表示目前手机上同时插入一个带有麦克风的耳机及HDMI输出线。
WiredAccessoryObserver工作原理就这么简单,我们接下来将以有线耳机为例子对其进行详细讨论。
- 启动与初始化
虽然WiredAccessoryObserver不是一个服务,但是它拥有系统服务的待遇—在system_server中同系统服务一起被加载,如下所示:
[SystemServer.java-->ServerThread.run()]
try {
new WiredAccessoryObserver(context);
} catch (Throwable e) {
reportWtf("starting WiredAccessoryObserver", e);
}
只有一个构造函数,其实,构造函数中并没有做太多的初始化工作,而是注册了一个BroadcastReceiver,监听ACTION_BOOT_COMPLETE。其真正的初始化工作是在这个BootCompletedReceiver中完成的。
[WiredAccessoryObserver.java-->BootCompletedReceiver.onReceive()]
public void onReceive(Context context, Intent intent) {
// 初始化
init();
// 开始对所有感兴趣的UEvent进行监听
for (int i = 0; i < uEventInfo.size(); ++i) {
UEventInfo uei = uEventInfo.get(i);
startObserving("DEVPATH="+uei.getDevPath());
}
}
这里的init()函数的作用是为了在开机后对外设的状态进行初始化。
[WiredAccessoryObserver.java-->WiredAccessoryObserver.init()]
private synchronized final void init() {
char[] buffer = new char[1024];
mPrevHeadsetState = mHeadsetState;
for (int i = 0; i < uEventInfo.size(); ++i) {
UEventInfo uei = uEventInfo.get(i);
try {
int curState;
/* 打开状态文件并从中读取状态信息。状态文件中保存着一个整数,非0则表示设备已插入。
通过UEventInfo的定义可以知道,有线耳机的状态文件路径为
/sys/class/switch/h2w/state */
FileReader file = new FileReader(uei.getSwitchStatePath());
int len = file.read(buffer, 0, 1024);
file.close();
curState = Integer.valueOf((new String(buffer, 0, len)).trim());
// 如果设备已插入,则更新设备的状态,否则不作处理
if (curState > 0) {
updateState(uei.getDevPath(), uei.getDevName(), curState);
}
} catch (Exception e) {
......
}
}
}
到这里WiredAccessoryObserver已经完成初始化了,已经对第一条UEvent的到来准备就绪。
- 耳机插入或拔出时的处理
如果有外设被插入或拔出,WiredAccessoryObserver的onUEvent()函数会被回调。参数event中保存了其详细的信息。
[WiredAccessoryObserver.java-->WiredAccessoryObserver.onUEvent()]
public void onUEvent(UEventObserver.UEvent event) {
try {
// UEvent事件的路径
String devPath = event.get("DEVPATH");
// 这个name其实就是UEventInfo中的mDevName,通过这个变量确定发生状态变化的设备名字
String name = event.get("SWITCH_NAME");
// 这个state与保存在状态文件中的数值的意义是一致的
// 事实上,当这条UEvent上报时,状态文件中的值也被更新成这个值
int state = Integer.parseInt(event.get("SWITCH_STATE"));
// 像初始化的init()函数一样,调用updateState()进行状态更新
updateState(devPath, name, state);
} catch (NumberFormatException e) {
......
}
}
[WiredAccessoryObserver.java-->WiredAccessoryObserver.updateState()]
private synchronized final void updateState(String devPath, String name, int state)
{
for (int i = 0; i < uEventInfo.size(); ++i) {
UEventInfo uei = uEventInfo.get(i);
if (devPath.equals(uei.getDevPath())) {
// 找到状态发生变化的外设所对应的UEventInfo并更新状态
update(name, uei.computeNewHeadsetState(mHeadsetState, state));
return;
}
}
}
看到这里,读者是否觉得updateState的实现有些笨拙了呢?如果以devName为键,将uEventInfo保存在Hashtable中,无论对代码的整洁还是执行的效率都是有帮助的。
注意uei.computeNewHeadsetState()这个函数,它的目的是通过UEvent上报的状态值计算出新的可用外设列表。
computeNewHeadsetState()这个函数的扩展性并不是太好,只是目前够用而已,读者可以自行研究。
继续前面的脚步,现在到了update()函数。这个函数的目的是对前面传入的newState进行全面检查,防止出现不正确的状态。这个函数的运算稍多些,为了方便分析,仅留下和有线耳机(h2w)相关的代码。
[WiredAccessoryObserver.java-->WiredAccessoryObserver.update()]
private synchronized final void update(String newName, int newState) {
/* 从headsetState中去掉不支持的外设,所以,如果不希望手机支持某种外设,比
如说USB_HEADSET,不需要从kernel改起,只要将其从SUPPORTED_HEADSETS中去
掉即可 */
int headsetState = newState & SUPPORTED_HEADSETS;
int h2w_headset = headsetState & (BIT_HEADSET | BIT_HEADSET_NO_MIC);
boolean h2wStateChange = true;
// 下面这行代码比较有意思,首先我们的目的是判断有线耳机的状态是否发生了变化
// mHeadsetState == headsetState这种条件很好理解,可是后面那个条件呢
if (mHeadsetState == headsetState || ((h2w_headset & (h2w_headset - 1)) != 0)) {
h2wStateChange = false;
}
// 如果是不正确的状态转换则直接忽略
if (!h2wStateChange) {
return;
}
// 更新可用外设列表
mHeadsetName = newName;
mPrevHeadsetState = mHeadsetState;
mHeadsetState = headsetState;
// 为什么要申请一个电源锁呢
mWakeLock.acquire();
// 状态已经更新完毕,发送消息给mHandler,我们可以想象出接下来要做什么了,通知AudioSevicervice
// 注意mHandler的定义,可以看出它运行在创建WiredAccessoryObserver的ServerThread中
mHandler.sendMessage(mHandler.obtainMessage(0,
mHeadsetState,
mPrevHeadsetState,
mHeadsetName));
}
这个函数的意图比较很明显,只是其中一个判断条件让人一时摸不着头脑,(h2w_headset & (h2w_headset - 1)) != 0。按照注释中的说法,此函数不接受同时有两种耳机出现的情况,也就是h2w_headst == BIT_HEADSET | BIT_HEADSET_NO_MIC,直接做这个判断不就可以了吗?仔细琢磨就能发现写这个条件的人的聪明之处。直接判断仅限于只有两种可能的外设时才能起作用,超过两个就很难处理了。而谷歌的这个做法既快捷,又可以应对任意多种可能的外设。读者可以思考一下为什么。
另外,这段代码在执行mHandler.sendMessage()的调用之前先申请了一个电源锁。这是一个很细节但很重要的做法。当发送消息给一个Handler时,必须考虑设备有可能在Handler得以处理消息之前进入深睡眠状态的极端情况(对延时消息来说,可能就是常见情况了)。在这种情况下,CPU将会进入休眠状态,从而使得消息无法得到及时处理,影响程序执行的正确性。
可用外设列表更新完毕后发送了一条消息给mHandler。当消息生效时,直接调用setDevicesState()函数,它会遍历所有SUPPORTED_HEADSET,然后对每个外设调用setDeviceState()。注意,这两个函数是devices与device的区别。setDeviceState()的目的就是要把指定外设的状态汇报给AudioService,我们看一下它的实现:
[WiredAccessoryObserver.java-->WiredAccessoryObserver.setDeviceState()]
private final void setDeviceState(int headset,
int headsetState,
int prevHeadsetState,
String headsetName) {
if ((headsetState & headset) != (prevHeadsetState & headset)) {
// 只有当这个外设的接入状态发生变化时才会继续
int device;
int state;
// 1表示可用,0表示不可用
if ((headsetState & headset) != 0) {
state = 1;
} else {
state = 0;
}
// 翻译可用外设列表中的外设为Audio系统的设备号
if (headset == BIT_HEADSET) {
device = AudioManager.DEVICE_OUT_WIRED_HEADSET;
} else if (headset == BIT_HEADSET_NO_MIC){
device = AudioManager.DEVICE_OUT_WIRED_HEADPHONE;
} else if (headset == BIT_USB_HEADSET_ANLG) {
device = AudioManager.DEVICE_OUT_ANLG_DOCK_HEADSET;
} else if (headset == BIT_USB_HEADSET_DGTL) {
device = AudioManager.DEVICE_OUT_DGTL_DOCK_HEADSET;
} else if (headset == BIT_HDMI_AUDIO) {
device = AudioManager.DEVICE_OUT_AUX_DIGITAL;
} else {
Slog.e(TAG, "setDeviceState() invalid headset type: "+headset);
return;
}
// 通知AudioService
mAudioManager.setWiredDeviceConnectionState(device, state, headsetName);
}
}
之后,程序的流程将会离开WiredHeadsetObserver,再次前往AudioService。
- 总结一下WiredAccessoryObserver
对WiredHeadsetObserver的分析就先告一段落,这里再简单回顾一下关于它的知识。
它是站在最前方的一个哨兵,时刻监听着和音频外设拔插相关的UEvent事件。
它接收到UEvent事件后,会翻译事件的内容为外设可用状态的变化。
它是为AudioService服务的,一旦有变化就立刻通知AudioService。
它虽然不是一个服务,但是它却运行在system_server中。
它不是唯一的音频外设状态监听者,它只负责监控有线连接的音频外设。其他的,如蓝牙耳机,在其他相关模块中维护。但是它们的本质是类似的,最终都要通知给AudioServic。有兴趣的读者可以自行研究。
3.3.2AudioService的外设状态管理
最终还是要回到AudioService中来,它才是音频相关操作的主基地。
- 处理来自WiredAccessoryObserver的通知
AudioService会如何处理外设的可用状态变化呢?仔细想想,在开发播放器的时候一定接触过ACTION_AUDIO_BECOMING_NOISY和ACTION_HEADSET_PLUG这两个广播吧。另外,更重要的是,这些变化需要让底层的AudioPolicy知道。所以,笔者认为AudioService外设状态管理分为三个内容:
管理发送ACTION_AUDIO_BECOMING_NOISY广播。
发送设备状态变化的广播,通知应用。
将其变化通知底层。
从WiredHeadsetObserver调用的setWiredDeviceConnectionState()函数开始:
[AudioService.java-->AudioService.setWiredDeviceConnectionState()]
public void setWiredDeviceConnectionState(int device, int state, String name) {
synchronized (mConnectedDevices) {
// 发送ACTION_AUDIO_BECOMING_NOISY广播的地方
int delay = checkSendBecomingNoisyIntent(device, state);
// 又是发送消息给mAudioHandler,注意这个消息有可能是延时的
// 这取决于checkSendBecomingNoisyIntent的返回值:delay
queueMsgUnderWakeLock(mAudioHandler,
MSG_SET_WIRED_DEVICE_CONNECTION_STATE,
device,
state,
name,
delay);
}
}
此函数负责两项工作:调用checkSendBecomingNoisyIntent()函数及发送SET_WIRED_DEVICE_CONNECTION_STATE消息给mAudioHandler。
checkSendBecomingNoisyIntent()函数的目的是判断当前状态的变化是否有必要发送BECOMING_NOISY广播。这个广播用于警告所有媒体播放应用声音即将从手机外放中进行播放。在绝大部分情况下,收到这个广播的应用都应当立即暂停播放,以避免用户无意识地泄露自己的隐私或打扰到周围的其他人。另外,这个函数的返回值决定了SET_WIRED_DEVICE_CONNECTION_STATE消息是否需要延时处理。其代码如下:
[AudioService.java-->AudioService.checkSendBecomingNoisyIntent()]
private int checkSendBecomingNoisyIntent(int device, int state) {
int delay = 0;
// 发送BECOMING_NOISY广播的前两个条件如下:
// 1.外设被拔除
// 2.外设是mBecomingNoisyIntentDevices指定的外设之一
// 既然这些设备从手机拔除后会AUDIO_BECOMING_NOISY,不妨称它们为安静外设
if ((state == 0) && ((device & mBecomingNoisyIntentDevices) != 0)) {
int devices = 0;
// 收集所有连接在手机上的安静外设
for (int dev : mConnectedDevices.keySet()) {
if ((dev & mBecomingNoisyIntentDevices) != 0) {
devices |= dev;
}
}
// 发送 BECOMING_NOISY广播的第三个条件:移除的设备必须是连接在手机上的最后一个安静外设
// 同时也是推迟后续处理的第一个条件:发送了BECOMING_NOISY广播
if (devices == device) {
delay = 1000; // 确定后续对这个状态变化的处理向后推迟1秒
sendBecomingNoisyIntent(); // 发送BECOMING_NOISY广播
}
}
// 推迟后续处理的另外一个条件:如果有和外设连接状态相关的延迟消息尚未被处理,那么
// 也必须推后消息的处理
if (mAudioHandler.hasMessages(MSG_SET_A2DP_CONNECTION_STATE) ||
mAudioHandler.hasMessages(MSG_SET_WIRED_DEVICE_CONNECTION_STATE)) {
delay = 1000;
}
return delay;
}
代码不长,有价值的内容不少。BECOMING_NOISY广播发出的条件是最后一个安静外设被拔出,这个很好理解。而推迟MSG_SET_WIRED_DEVICE_CONNECTION_STATE消息的生效时间这种做法可能一时难以弄明白。不过暂时先不管它,等我们了解了外设连接状态变化的流程后再解释它的意义。
回到setWiredDeviceConnectionState (),调用checkSendBecomingNoisyIntent()函数后,它发送MSG_SET_WIRED_DEVICE_CONNECTION_STATE给mAudioHandler,此消息生效后,mAudioHandler调用onSetWiredDeviceConnectionState函数。
[AudioService.java-->AudioHandler.onSetWiredDeviceConnectionState()]
private void onSetWiredDeviceConnectionState(int device, int state, String name)
{
synchronized (mConnectedDevices) {
// 如果拔下普通耳机,则会强制要求使用蓝牙耳机作为输出设备
if ((state == 0) && ((device == AudioSystem.DEVICE_OUT_WIRED_HEADSET) ||
(device == AudioSystem.DEVICE_OUT_WIRED_HEADPHONE))) {
setBluetoothA2dpOnInt(true);
}
// 这个函数对AudioPolicy进行了通知
handleDeviceConnection((state == 1), device, "");
// 如果插入普通耳机,则会取消强制使用蓝牙耳机的设置
if ((state != 0) && ((device == AudioSystem.DEVICE_OUT_WIRED_HEADSET) ||
(device == AudioSystem.DEVICE_OUT_WIRED_HEADPHONE))) {
setBluetoothA2dpOnInt(false);
}
// 从名字就可以看出,这是用来广播外设状态变化通知的,将通知对此感兴趣的应用程序
sendDeviceConnectionIntent(device, state, name);
}
}
在这个函数中,我们需要重点关注的是对handleDeviceConnection()和sendDevice-ConnectionIntent两个函数的调用。它们分别用来通知AudioPolicy与上层应用。
另外,还可以看到,在handleDeviceConnection()函数上下有一对关于蓝牙耳机的操作。从其实现上可以看出,如果拔出普通耳机,系统将会强制使用蓝牙耳机进行输出。如果插入耳机则会取消这个设置。这种操作完全可以放在AudioPolicyManager中实现。
看一下通知AudioPolicy的handleDeviceConnection()函数的实现吧!
[AudioService.java-->AudioService.handleDeviceConnection()]
private boolean handleDeviceConnection(boolean connected, int device, String params){
synchronized (mConnectedDevices) {
boolean isConnected = (mConnectedDevices.containsKey(device) &&
(params.isEmpty() || mConnectedDevices.get(device).equals(params)));
if (isConnected && !connected) {
// 外设被拔出,通过AudioSystem将状态设置到底层的AudioPolicyService
AudioSystem.setDeviceConnectionState(device,
AudioSystem.DEVICE_STATE_UNAVAILABLE,
mConnectedDevices.get(device));
mConnectedDevices.remove(device);
return true;
} else if (!isConnected && connected) {
// 外设被插入
AudioSystem.setDeviceConnectionState(device,
AudioSystem.DEVICE_STATE_AVAILABLE,
params);
mConnectedDevices.put(new Integer(device), params);
return true;
}
}
return false;
}
很简单吧?如果读者对卷I第7章的内容比较熟悉,那么一定知道AudioSystem.setDeviceConnectionState()这个函数意味着什么。它将更新底层的AudioPolicy中缓存的可用设备列表,同时,如果正在进行音频播放,那么这个函数还将触发音频设备的重新选择。
这一节提到“可用设备列表”的次数很多,很多地方都使用了这个概念。归纳一下,在本节所讨论的内容里,有三个地方有可用设备列表:
1)WiredAccessoryObserver: 目的是确认外设的状态变化是否合法,是否需要报告给AudioService。
2)AudioService: 它以一个Hashtable的形式保存了一个可用设备列表,它为AudioService向应用及底层AudioPolicyManager发送通知提供依据。
3)AudioPolicyManager: 它保存的可用设备列表在AudioPolicyManager需要重新选择音频输出设备时提供候选。
- 关于推迟处理外设状态
前面讨论checkSendBecomingNoisyIntent()函数的实现时提到了根据某些条件,有可能使MSG_SET_WIRED_DEVICE_CONNECTION_STAT延迟生效1秒。在这种情况下应用会在1秒之后才能收到设备状态变化的广播,同时,AudioPolicy也要在1秒之后才能更新可用设备列表并进行必要的设备切换。为什么要这么做呢?想想推迟的条件:
最后一个安静外设被移除,发送了BECOMING_NOISY广播。
队列中尚有两个消息在等候处理:MSG_SET_WIRED_DEVICE_CONNECTION_STATE 和MSG_SET_A2DP_CONNECTION_STATE。
只要这两个条件有一个满足,就会发生1秒推迟。下面分别讨论。
关于第一个条件,当最后一个安静外设被移除后,手机上可用的音频输出设备就只剩下扬声器了(听筒不能算是常规的音频输出设备,它只有在通话过程中才会用到)。那么在MSG_SET_WIRED_DEVICE_CONNECTION_STAT生效后,AudioPolicyManager将会切换输出到扬声器,此时正在播放的音频就会被外放出来。
很多时候,这并不是用户所期望的,用户可能不希望他人知道自己在听什么,或者不希望在某些场合下扬声器发出的声音打扰到其他人。何况耳机被拔除有可能还是个意外。所以,正在进行音频播放的应用可能希望收到耳机等安静设备被拔出时的通知,并且在收到后暂停播放。
读者可能会有疑问,在sendDeviceConnectionIntent()中不是发送了状态通知的广播了吗?其实,这个状态通知广播用在其他情况下可以,但是用在上述情况中是有问题的。按照上面的讨论,执行sendDeviceConnectionIntent()之前,先执行了handleDeviceConnection(),它会更新底层的可用设备列表,并且触发设备切换。于是应用有可能在收到状态通知之前,输出设备已经被切换成扬声器了,直到应用收到通知后暂停回放,这段时间内就会发生扬声器的漏音。
所以,Android引入了一个新的广播来应对这个问题,那就是BECOMING_NOISY广播。这个广播只有在最后一个安静外设被移除后才会发出,于是应用可以精确地知道音频即将从扬声器进行播放,而且后续的设备切换等动作被推迟了1秒,应用就有充足的时间收到BECOMING_NOISY广播并暂停播放。在正常情况下,这种做法可以杜绝漏音的情况出现。这是第一个延时条件的意义。
至于第二个条件,队列中尚有以下两个消息等候处理:MSG_SET_WIRED_DEVICE_CONNECTION_STATE 和MSG_SET_A2DP_CONNECTION_STATE,这其实是不得已的一种做法。考虑一下,为什么队列中尚有这两个消息在等候处理呢?一个是mAudioHandler所在的线程发生了阻塞,另一个就是这两个消息被延迟发送了。根据Handler现有的接口没有办法得知是哪一种情况,但是在正常情况下都是第二种,也是比较麻烦的一种情况。因为在这种情况下,如果正常发送MSG_SET_WIRED_DEVICE_CONNECTION_STATE消息,那么它的生效时间将会早于正在队列中排队的那两个消息。如此一来,就会发生外设可用状态紊乱的问题。所以,AudioService迫不得已在这种情况下推迟发送1秒。读者可以做个试验,快速地在手机上拔插耳机,将会看到通知栏内的耳机图标的变化总是会延迟1秒。
我们在之前的分析中没有见过MSG_SET_A2DP_CONNECTION_STATE,它和讨论的MSG_SET_WIRED_DEVICE_CONNECTION_STATE意义是一样的,而且有着几乎相同的处理逻辑,不过它是与蓝牙耳机相关的。
3.3.3音频外设管理小结
这一节以有线音频外设为例,探讨了从WiredAccessoryObserver收到UEvent开始到AudioService通知底层应用为止的AudioService对音频外设的管理机制。
总结一下音频外设拔插的处理过程:
由负责相关外设的模块监听从硬件上报的状态通知。将状态变化提交给AudioService进行处理。
AudioService得到相关模块发来的通知,根据需要发送BECOMING_NOISY消息给应用,并更新自己的可用设备列表。
AudioService将外设可用状态的变化通知AudioPolicy。AudioPolicy更新自己的可用设备列表,并重新选取音频输出设备。
AudioService将外设可用状态以广播的形式发送给应用等其他对此感兴趣的应用程序或系统模块。
蓝牙模块负责蓝牙耳机的连接/断开状态的监控并通知AudioService。AudioService收到此通知之后的代码路径虽然与本节所讨论的内容不完全一样,但其处理原则与有线耳机是一致的,读者可以自行分析学习。