Android系统从早期版本就内置了语音识别框架,开发者无需接入第三方SDK,只调用系统的Speech Recognition接口就能实现“说话转文字”的功能。不过这套API在实际集成时坑不少:有的设备根本没有预装识别服务,有的回调迟迟不触发,有的错误码看不懂。这篇文章从API原理讲到可运行的Demo,再重点聊聊如何对语音识别功能做系统性测试,包括手动验证和自动化测试两条路线。

一、两套官方API:RecognizerIntent与SpeechRecognizer
Android官方提供了两种调用语音识别的方式。第一种是通过RecognizerIntent发起一个隐式Intent,系统会弹出Google语音搜索的对话框,用户说完话后结果以Activity回调的形式返回。这种方式实现最简单,十行代码就能跑起来,适合“按一下按钮说一句话”这种轻量场景。
第二种是使用SpeechRecognizer类,它不依赖任何UI,识别过程完全在后台完成,识别结果通过RecognitionListener回调实时推送,甚至可以拿到中间识别结果(partial results),适合做实时字幕、语音输入法这类需要连续交互的功能。需要注意的是,SpeechRecognizer必须 在主线程创建,否则会直接抛出异常。
两者的核心区别可以总结为:RecognizerIntent是“借用系统的界面”,SpeechRecognizer是“只借用系统的引擎”。正式项目中推荐SpeechRecognizer,可控性更强,也更容易做单元测试和UI自动化测试。下面是SpeechRecognizer的标准用法:
// 必须在主线程调用
SpeechRecognizer recognizer = SpeechRecognizer.createSpeechRecognizer(context);
recognizer.setRecognitionListener(new RecognitionListener() {
@Override
public void onResults(Bundle results) {
ArrayList<String> texts =
results.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
if (texts != null && !texts.isEmpty()) {
Log.d("ASR", "最终识别结果: " + texts.get(0));
}
}
@Override
public void onPartialResults(Bundle partialResults) {
// 中间结果,可用于实时展示
ArrayList<String> texts =
partialResults.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
if (texts != null) {
Log.d("ASR", "中间结果: " + texts.get(0));
}
}
@Override
public void onError(int error) {
Log.e("ASR", "识别出错, 错误码: " + error);
}
@Override
public void onReadyForSpeech(Bundle params) { }
@Override
public void onBeginningOfSpeech() { }
@Override
public void onRmsChanged(float rmsdB) { }
@Override
public void onBufferReceived(byte[] buffer) { }
@Override
public void onEndOfSpeech() { }
@Override
public void onEvent(int eventType, Bundle params) { }
});
二、搭建可测试的识别Demo
做测试的前提是有一个功能完整、结构清晰的被测对象。我们先写一个简单的Activity,包含一个按钮和一段展示文字的TextView,点击按钮后启动SpeechRecognizer开始监听。权限方面,Android 11以下只需要RECORD_AUDIO运行时权限;从Android 11开始,如果应用想访问语音识别结果,还需要声明<queries>元素来声明对识别服务的可见性,否则SpeechRecognizer.isRecognitionAvailable()会一直返回false。
清单文件中的关键配置如下:
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<queries>
<intent>
<action android:name="android.speech.RecognitionService" />
</intent>
</queries>
Activity中的启动逻辑建议把startListening和stopListening封装成独立方法,并把识别结果写入一个可观察的数据对象(比如LiveData)。这样做的好处是,后续写Espresso测试时可以直接对LiveData断言,而不必依赖Toast之类的瞬态UI,测试稳定性会好很多。识别Intent的构建也有讲究,EXTRA_LANGUAGE_MODEL决定识别模型偏向自由语句还是命令词,EXTRA_PARTIAL_RESULTS设为true才会回调中间结果:
private void startListening() {
Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, "zh-CN");
intent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true);
intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 3);
recognizer.startListening(intent);
}
三、手动测试:真机与模拟器的验证要点
自动化之前,手动测试永远是第一道关。首先要确认设备上是否存在识别服务:SpeechRecognizer.isRecognitionAvailable(context)返回true只说明有服务响应Intent,在Android 13以上更推荐用SpeechRecognizer.isOnDeviceRecognitionAvailable(context)判断是否支持端侧离线识别。国内部分厂商的ROM阉割了Google服务,RecognizerIntent这条路基本走不通,此时要么引导用户安装识别组件,要么换用云端识别SDK,这是集成前必须做的前期调研。
真机测试时建议覆盖这几类用例:正常语速朗读、极短语句(例如单字)、长时间静音后说话、快速连续两次点击识别按钮、识别过程中来电话打断。每一种都对应不同的系统行为,比如静音超时会触发ERROR_SPEECH_TIMEOUT(错误码6),打断后可能触发ERROR_CLIENT(错误码5)。常见错误码整理如下:
| 错误码 | 常量名 | 常见原因 |
|---|---|---|
| 1 | ERROR_NETWORK_TIMEOUT | 网络不佳,云端识别超时 |
| 2 | ERROR_NETWORK | 无网络连接 |
| 3 | ERROR_AUDIO | 录音异常, mic被占用 |
| 5 | ERROR_CLIENT | 客户端错误,常见于未在主线程调用 |
| 6 | ERROR_SPEECH_TIMEOUT | 长时间没有检测到语音 |
| 7 | ERROR_NO_MATCH | 有声音但无法匹配出文字 |
| 8 | ERROR_RECOGNIZER_BUSY | 上一 次识别还没结束就再次启动 |
| 9 | ERROR_INSUFFICIENT_PERMISSIONS | RECORD_AUDIO权限被拒 |
模拟器测试是个容易被忽视的技巧。Android模拟器从某个版本起支持虚拟麦克风的音频注入,可以配合adb emu命令或者直接在宿主机播放音频文件,把一段固定的语音“喂”给模拟器。固定音频输入的价值在于可复现——同一句话重复识别十次,结果的一致性可以直接反映识别引擎的稳定性,这是真人对着麦克风说话做不到的。
四、自动化测试:Espresso与UI Automator结合
语音识别的自动化难点在于“声音从哪来”。系统API无法直接注入音频到RecognitionListener,所以业界普遍做法是把识别逻辑抽象成接口,测试时用Mock实现替换真实的SpeechRecognizer。比如定义一个RecognitionEngine接口,生产环境里由SpeechRecognizer实现,测试环境里由一个能按时回调预设文本的Fake对象实现。这样ViewModel层的逻辑(状态切换、结果处理、错误恢复)就可以用纯JUnit测试覆盖,速度极快且完全确定性。
public interface RecognitionEngine {
void start(Locale locale);
void stop();
void setListener(EngineListener listener);
}
// 测试用的Fake实现,直接回调预设结果
public class FakeEngine implements RecognitionEngine {
private EngineListener listener;
@Override
public void start(Locale locale) {
// 模拟500ms后返回识别结果
new Handler(Looper.getMainLooper()).postDelayed(() -> {
listener.onResult("你好世界");
}, 500);
}
@Override
public void stop() { }
@Override
public void setListener(EngineListener listener) {
this.listener = listener;
}
}
至于UI层的端到端测试,Espresso配合IdlingResource可以做到“点击按钮,等待识别结果出现在TextView上”的断言。核心思路是注册一个IdlingResource,在onReadyForSpeech回调时置为忙碌状态,在onResults或onError时置为空闲,Espresso会自动等待状态变化,避免手写Thread.sleep导致的测试抖动:
@Test
public void clickButton_showsRecognizedText() {
onView(withId(R.id.btn_start)).perform(click());
onView(withId(R.id.tv_result))
.check(matches(withText("你好世界")));
}
如果测试需要跨应用(比如识别弹出了系统级语音对话框),Espresso就无能为力了,这时换用UI Automator,它可以直接操作其他应用的界面元素。另外要注意,CI环境里跑这类测试必须保证模拟器带有识别服务,建议在CI脚本的smoke test阶段先用adb shell pm list packages检查识别服务包是否存在,不存在就直接跳过该组用例,避免误报失败。
五、常见问题排查清单
最后把实际项目里高频出现的几个问题整理成清单,方便对号入座。
- isRecognitionAvailable返回false:Android 11以上检查清单里是否声明了
<queries>;国产ROM检查是否缺失Google组件。 - 回调一次都不触发:确认SpeechRecognizer是否在主线程创建,以及是否忘记调用
setRecognitionListener就直接startListening。 - 报错ERROR_RECOGNIZER_BUSY:上一个识别实例没有销毁,务必在Activity销毁时调用
recognizer.destroy()。 - 离线识别不生效:端侧识别需要Android 12以上并使用
createOnDeviceSpeechRecognizer创建实例,同时设备要下载对应语言的离线包。 - 中文识别成英文:检查
EXTRA_LANGUAGE是否设置为zh-CN,部分设备还要确认系统语言与地区设置。
总体来看,Android语音识别的测试工作分三层:引擎可用性验证、功能逻辑的单元测试、真实语音输入的端到端测试。把识别逻辑抽象成可替换的接口是让整套测试跑起来的关键,其余的问题无非是权限、线程和错误码这三座大山,逐项排查基本都能解决。
Android语音识别Speech RecognitionRecognizerIntent测试修改时间:2026-09-13 14:51:01