Android 14 前台服务启动崩溃:区分缺少类型、权限和后台启动限制
Android 14 前台服务崩溃时,按异常区分服务类型、对应权限、运行时前提和后台启动条件,以录音服务说明正确的顺序。
应用在 Android 13 能启动前台服务,把 targetSdk 提高到 34 后,在 Android 14 出现 MissingForegroundServiceTypeException: Starting FGS without a type,或 SecurityException: Starting FGS with type ... requires permissions。这两个错误指向不同的检查层,不能用同一份“添加全部权限”的配置处理。
先保留完整异常、服务组件名、设备 API、实际安装包的 targetSdk,以及触发时 Activity 是否可见。下面重点讨论 Android 14、目标 API 34 及以上的类型和权限要求;后台启动限制从 Android 12、目标 API 31 及以上就已存在。
按异常确定缺的是哪一层
MissingForegroundServiceTypeException 表示服务提升到前台时没有得到所需的类型声明。检查最终合并清单中对应的 service,以及调用 startForeground() 时选用的类型。不要只检查项目里另一个同名功能的服务。
SecurityException 则要继续读异常里的类型和权限:可能缺少该类型的前台服务权限,也可能没满足运行时条件,或者应用当前状态不允许使用“仅在使用期间”的权限。只看异常类名不够。
如果是 ForegroundServiceStartNotAllowedException,重点转向从后台启动的限制和实际触发来源。声明了类型并不意味着任意后台回调都可以启动服务。传入的类型不属于清单声明范围还可能抛出 IllegalArgumentException,这也不能用申请运行时权限解决。
以用户主动开始录音的服务为例
假设应用需要在用户按下“开始录音”后继续采集麦克风,应声明 microphone,而不是为了避免漏项加上 camera|location|microphone。后两种资源没有使用,就不应要求它们的前置条件。
下面的清单片段使用 compileSdk >= 34,放入项目既有清单。RecordService 是应用自己的录音服务,名称需与实际类一致:
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission
android:name="android.permission.RECORD_AUDIO" />
<application>
<service
android:name=".RecordService"
android:exported="false"
android:foregroundServiceType="microphone" />
</application>
实际项目应把 service 合并进已有 application,保留其中其他属性。FOREGROUND_SERVICE 和类型对应的 FOREGROUND_SERVICE_MICROPHONE 是清单权限;后者属于 normal 权限,不会产生运行时申请框。RECORD_AUDIO 则需要运行时取得用户许可。不能只声明麦克风 FGS 权限就认为能录音。
其他服务选择与真实用途匹配的类型。例如蓝牙设备交互的 connectedDevice 具有自己的前置条件,不能照搬录音权限。shortService 不要求单独的类型权限,但有时限和用途限制;它也不是长期后台任务的替代标签。
先授权,再从可见页面发起
录音功能的普通启动路径应按这个顺序:用户在可见 Activity 操作,应用请求并取得 RECORD_AUDIO,然后在仍符合启动条件时调用 startForegroundService();服务及时调用 startForeground(),再开始实际录音工作。
以下函数放在已有的 ComponentActivity 内,示例支持 API 23 及以上,并使用 AndroidX Core、Lifecycle。它只演示已经取得权限后的启动检查,不包含权限弹窗和录音实现。权限申请成功后,仍应确认用户当前希望开始录音,且页面处于可交互状态,再调用它。
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
private fun startRecordingFromVisiblePage(): Boolean {
if (!lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
return false
}
if (ContextCompat.checkSelfPermission(
this, Manifest.permission.RECORD_AUDIO
) != PackageManager.PERMISSION_GRANTED
) {
return false
}
ContextCompat.startForegroundService(
this, Intent(this, RecordService::class.java)
)
return true
}
这个检查不能代替对后台入口的设计,也不能消除检查与启动之间所有状态变化。不要把它移进定时器、广播或后台消息处理器,就认为获得了可见 Activity 的启动条件;需要后台启动的业务应逐项核对相应豁免,失败时向用户提供可操作的入口。
服务中提升前台的调用可写成下面这样。它位于 RecordService : Service 内,使用 AndroidX Core 1.12.0 及以上。notification 是已经构建好的有效通知;API 26 及以上要事先创建它对应的渠道,设置有效小图标,且通知 ID 必须为正数。示例只演示提升步骤,不能当成完整录音服务。
import android.app.Notification
import android.content.pm.ServiceInfo
import android.os.Build
import androidx.core.app.ServiceCompat
private fun promoteRecording(notification: Notification) {
val type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
} else {
0
}
ServiceCompat.startForeground(this, 1001, notification, type)
}
microphone 类型常量从 API 30 起可用,因此这里按 API 30 分支。不要在服务先启动后才弹窗申请 RECORD_AUDIO,也不要在调用 startForeground() 前做耗时初始化,错过系统要求的提升时限。停止录音时,原有服务还应释放录音资源,并按设计调用 stopForeground()、stopSelf() 结束,不留下空转的常驻服务。
已授权,为什么从后台启动仍失败
麦克风、相机和部分定位权限具有 while-in-use 边界。在 Android 14、目标 API 34 及以上,系统会在创建相关前台服务时检查权限当前是否可用。应用位于后台时,即使 checkSelfPermission() 返回 PERMISSION_GRANTED,也不能据此推断此刻可以从后台创建麦克风服务。
普通的“允许后台启动 FGS”豁免,与“允许在后台创建依赖 while-in-use 权限的 FGS”豁免不是同一份条件。某次高优先级消息满足前一种条件,也不能自动推导出可从后台启用麦克风。通知或小组件交互等情形有各自规则,必须结合实际入口判断。
这与“用户在可见页面启动录音,之后切到后台继续录音”也不同。后者正是麦克风前台服务的使用场景之一;前提是启动顺序、权限和通知符合要求。不能把继续运行的能力理解为后台可随时新建服务。
不要为了处理异常而要求无关相机、后台定位权限,或把服务组件公开给其他应用。降低 targetSdk 只能暂时改变部分行为的触发条件,并没有修正服务模型。
检查的是服务真实行为
修复后,先在 Android 14 上从可见页面启动:应无上述异常,前台服务及时建立,实际录音能够开始;切到后台后按业务设计继续,停止后资源和服务退出。再覆盖用户拒绝麦克风权限、权限撤回,以及从后台消息误触发的路径,确认应用能拒绝不满足条件的启动并给出可理解的状态。
通知权限也单独验证。POST_NOTIFICATIONS 的授权与否不替代前台服务的类型和麦克风前提;拒绝它不意味着可以省略 FGS 通知。若仍失败,保存新的完整异常,按其中明确要求回到相应层检查,不在清单中继续堆叠不相关类型。


