Android Studio如何让虚拟助手App常驻后台监听唤醒词
问题根因
你之前的实现无法后台运行核心是3个硬伤:
- 所有唤醒检测、语音识别逻辑全绑定在
MainActivity生命周期,onStop回调里主动写了停止Porcupine、销毁SpeechRecognizer的逻辑,退到后台Activity被系统回收后所有功能自然终止 - 你之前用的普通Service优先级极低,安卓系统内存紧张时会优先杀掉无可见通知的后台服务,根本无法常驻
- 没有适配安卓8.0以后的后台执行限制、13+通知权限、14+前台服务麦克风类型要求,也没有处理国产ROM的电池优化查杀逻辑,就算服务拉起也会被系统断权限、杀进程
可行实现方案
第一步:修改Manifest配置
先在AndroidManifest.xml里加齐需要的权限、声明前台服务:
<!-- 必要权限 --> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- 安卓14必须声明前台服务类型权限 --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> <!-- 安卓13+必须申请通知权限才能发前台服务通知 --> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <!-- 申请电池优化白名单权限,降低被系统查杀概率 --> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <application ...> <!-- 声明常驻唤醒前台服务 --> <service android:name=".VoiceWakeupService" android:foregroundServiceType="microphone" android:exported="false" /> <activity android:name=".MainActivity" ...> ... </activity> </application>
第二步:实现独立前台服务
把所有唤醒检测、语音识别、指令响应逻辑从Activity挪到独立的前台服务VoiceWakeupService中,完全脱离Activity生命周期:
package com.eylon.jarvis; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.Service; import android.content.Intent; import android.media.MediaPlayer; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.speech.RecognitionListener; import android.speech.RecognizerIntent; import android.speech.SpeechRecognizer; import android.widget.Toast; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; import java.util.ArrayList; import java.util.Locale; import ai.picovoice.porcupine.Porcupine; import ai.picovoice.porcupine.PorcupineException; import ai.picovoice.porcupine.PorcupineManager; import ai.picovoice.porcupine.PorcupineManagerCallback; public class VoiceWakeupService extends Service { private static final String CHANNEL_ID = "wakeup_service_channel"; private static final int NOTIFICATION_ID = 1001; private static final String ACCESS_KEY = "Oc8ZOSkVtJHWKhVW3iGMedHDSCSXn6P4vQtrQBl8hNLXwLmxLhs2AA=="; private PorcupineManager porcupineManager = null; private SpeechRecognizer speechRecognizer; private Intent speechRecognizerIntent; private AppState currentState = AppState.STOPPED; private final Handler mainHandler = new Handler(Looper.getMainLooper()); private enum AppState { STOPPED, WAKEWORD, STT } private void showToast(String message) { mainHandler.post(() -> Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT).show()); } private final PorcupineManagerCallback porcupineCallback = keywordIndex -> mainHandler.post(() -> { try { porcupineManager.stop(); } catch (PorcupineException e) { showToast("停止唤醒检测失败"); return; } speechRecognizer.startListening(speechRecognizerIntent); currentState = AppState.STT; }); @Override public void onCreate() { super.onCreate(); // 创建通知渠道,启动前台服务(安卓8.0+必须,否则服务会直接被系统杀掉) createNotificationChannel(); Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("Jarvis 运行中") .setContentText("等待唤醒指令") .setSmallIcon(R.mipmap.ic_launcher) .setPriority(NotificationCompat.PRIORITY_LOW) .build(); startForeground(NOTIFICATION_ID, notification); // 初始化语音识别、唤醒引擎 initSpeechRecognizer(); initPorcupine(); } private void initSpeechRecognizer() { if (!SpeechRecognizer.isRecognitionAvailable(this)) { showToast("当前设备不支持语音识别"); stopSelf(); return; } speechRecognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); speechRecognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM); speechRecognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, "en-US"); speechRecognizerIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, "请说出指令"); // 注意必须用ApplicationContext创建,不要绑定Activity speechRecognizer = SpeechRecognizer.createSpeechRecognizer(getApplicationContext()); speechRecognizer.setRecognitionListener(new RecognitionListener() { @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 onError(int error) { // 识别出错后自动回到唤醒检测状态,不要终止服务 switch (error) { case SpeechRecognizer.ERROR_AUDIO: showToast("录音出错"); break; case SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS: showToast("缺少录音权限"); break; case SpeechRecognizer.ERROR_NETWORK_TIMEOUT: case SpeechRecognizer.ERROR_NETWORK: showToast("网络异常"); break; } startWakeupDetect(1000); } @Override public void onResults(Bundle results) { ArrayList<String> data = results.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION); if (data != null && !data.isEmpty()) { String transcript = data.get(0).toLowerCase(Locale.ROOT); handleVoiceCommand(transcript); } startWakeupDetect(3000); } @Override public void onPartialResults(Bundle partialResults) {} @Override public void onEvent(int eventType, Bundle params) {} }); startWakeupDetect(0); } private void initPorcupine() { try { porcupineManager = new PorcupineManager.Builder() .setAccessKey(ACCESS_KEY) .setKeyword(Porcupine.BuiltInKeyword.JARVIS) .setSensitivity(0.7f) .build(getApplicationContext(), porcupineCallback); } catch (PorcupineException e) { showToast("唤醒引擎初始化失败:" + e.getMessage()); stopSelf(); } } private void startWakeupDetect(int delayMills) { speechRecognizer.stopListening(); currentState = AppState.WAKEWORD; mainHandler.postDelayed(() -> { if (currentState == AppState.WAKEWORD && porcupineManager != null) { try { porcupineManager.start(); } catch (PorcupineException e) { showToast("启动唤醒检测失败"); } } }, delayMills); } private void handleVoiceCommand(String transcript) { if (transcript.equals("good morning")) { playAudioResponse(R.raw.good_morning); } else if (transcript.equals("who is your creator")) { playAudioResponse(R.raw.creator); } } private void playAudioResponse(int audioRes) { MediaPlayer player = MediaPlayer.create(getApplicationContext(), audioRes); player.setOnCompletionListener(MediaPlayer::release); player.start(); } private void createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel channel = new NotificationChannel( CHANNEL_ID, "唤醒服务通知", NotificationManager.IMPORTANCE_LOW ); channel.setDescription("语音助手后台运行通知"); NotificationManager manager = getSystemService(NotificationManager.class); manager.createNotificationChannel(channel); } } @Override public int onStartCommand(Intent intent, int flags, int startId) { // 服务被系统意外杀掉后自动重启 return START_STICKY; } @Override public void onDestroy() { super.onDestroy(); if (porcupineManager != null) { try { porcupineManager.stop(); porcupineManager.delete(); } catch (PorcupineException e) { e.printStackTrace(); } } if (speechRecognizer != null) { speechRecognizer.destroy(); } mainHandler.removeCallbacksAndMessages(null); } @Nullable @Override public IBinder onBind(Intent intent) { return null; } }
第三步:精简MainActivity逻辑
删掉原来MainActivity里所有和Porcupine、SpeechRecognizer相关的初始化、生命周期处理代码,只保留权限申请、启动/停止服务的逻辑:
- 必须删掉原来
onStop回调里停止检测、销毁识别实例的代码,退后台不要干预服务运行 - 首次启动时依次申请录音、通知权限,安卓12+还要引导用户手动开启「后台使用麦克风」权限
- 开关按钮只负责发送启动/停止服务的Intent:
// MainActivity按钮点击逻辑示例 public void process(View view) { if (button.isChecked()) { if (checkAllNecessaryPermissions()) { Intent serviceIntent = new Intent(this, VoiceWakeupService.class); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { startForegroundService(serviceIntent); } else { startService(serviceIntent); } // 引导用户将App加入电池优化白名单 requestBatteryOptimizeWhiteList(); } else { requestAllNecessaryPermissions(); button.setChecked(false); } } else { stopService(new Intent(this, VoiceWakeupService.class)); } }
注意事项(避坑)
- 小米、华为、OPPO、vivo等国产定制ROM默认有严格的后台查杀机制,就算是前台服务也可能被杀死,必须引导用户手动在系统设置里给App开启「自启动允许」「后台活动无限制」权限,没有通用绕开方案
- 安卓12及以上版本,App退到后台后默认无法使用麦克风,除了声明权限,部分机型需要用户手动在权限设置里打开「后台允许使用麦克风」开关,否则后台唤醒会失效
- 服务里创建SpeechRecognizer、Porcupine实例时必须用
getApplicationContext(),不能传Activity实例,否则会绑定Activity生命周期,Activity销毁后实例也会被回收 - 不要在服务里持有Activity的强引用,如果需要把识别状态同步到UI,用本地广播、LiveData等组件解耦
内容的提问来源于stack exchange,提问作者AGJ
相关产品推荐
相关产品推荐

