Android 12 创建 PendingIntent 崩溃:正确选择 FLAG_IMMUTABLE 与 FLAG_MUTABLE
普通通知、直接回复和依赖库分别检查 PendingIntent 创建点,按功能选择不可变或可变标记,并处理 targetSdk 34 的目的地限制。
应用把 targetSdk 升到 31 或更高后,在 Android 12 及以上设备创建通知、定时任务或播放控制时崩溃,日志出现 Targeting S+ (version 31 and above) requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified。需要修正的是创建 PendingIntent 时的可变性声明。
这个要求同时涉及设备系统和应用的目标版本。compileSdk 决定编译时能使用哪些 API,单独提高它不等于启用这项运行时行为;应核对实际安装变体的 targetSdk。下面的 Kotlin 示例使用 compileSdk >= 31,并保留低版本系统分支。
从异常堆栈找真正的创建位置
从 PendingIntent.checkFlags 往下看,找到第一个属于应用或依赖库的调用。如果失败处是播放器内部的 getBroadcast(),只修改应用为通知正文创建的 getActivity(),并不能改变播放器创建的另一个对象。
在工程中搜索 PendingIntent.getActivity、getBroadcast、getService 和 getForegroundService。如果堆栈落在依赖中,检查当前构建变体实际解析的版本和对应源码,再升级到已修复该调用、且与项目兼容的版本。不要随意添加一个不在堆栈中的库来碰运气,也不要只关闭 UnspecifiedImmutableFlag 检查;Lint 的开关不会取消设备上的校验。
普通通知点击使用不可变 PendingIntent
如果通知点击只是打开应用里指定的页面,通常无需让发送方修改 Intent。下面的函数把目的组件写清楚,并使用不可变标记。MainActivity 是项目已有并已在清单中声明的页面,itemId 是业务记录编号,requestCode 应按应用的通知标识规则分配。
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
fun openItemPendingIntent(
context: Context,
requestCode: Int,
itemId: String
): PendingIntent {
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
putExtra("item_id", itemId)
}
val flags = PendingIntent.FLAG_UPDATE_CURRENT or
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
}
return PendingIntent.getActivity(
context, requestCode, intent, flags
)
}
FLAG_IMMUTABLE 从 API 23 起可用,所以兼容更早系统时在 API 23 分支添加。项目的 minSdk >= 23 时,可以直接使用 FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE。
FLAG_UPDATE_CURRENT 与不可变并不冲突。不可变限制的是持有者在发送时通过额外 Intent 改写内容;创建者仍可用 FLAG_UPDATE_CURRENT 更新匹配对象中的 extras。它也不会让每次调用自动获得不同的 PendingIntent:只有 extras 不同,可能仍匹配同一个操作。若多个通知必须打开不同记录,应另行设计稳定且不同的 requestCode,或按业务需要设置不同的 Intent data,并分别点击验证。
只有需要接收补充数据时才使用可变标记
通知直接回复等功能需要系统在发送时加入数据,这类操作可能需要 FLAG_MUTABLE。先确认功能确实依赖这种能力,再修改相应调用;不要把工程里所有对象统一改为可变,也不能同时添加 FLAG_IMMUTABLE 和 FLAG_MUTABLE。
下面只演示直接回复动作的 PendingIntent 部分,不是一套完整的通知回复实现。ReplyReceiver 必须是项目已有的接收器,由它读取 RemoteInput 结果;通知还需正确配置对应的 RemoteInput 和 action。
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
fun replyPendingIntent(context: Context): PendingIntent {
val intent = Intent(context, ReplyReceiver::class.java).apply {
action = "com.example.app.REPLY"
}
val mutableFlag =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE
} else {
0
}
return PendingIntent.getBroadcast(
context,
2001,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag
)
}
这里的 action 是示例,应替换为项目自己的命名空间。API 31 以下的这一分支没有设置不可变标记,保留旧系统默认可变的行为。不要因为这段代码中的接收器需要补充回复数据,就把打开详情页的普通点击也改为可变。
对于定位、NFC 前台分发等其他用途,应单独核对该 API 是否需要填充 Intent;“含有 extras”本身不是使用可变标记的理由。
targetSdk 34 还要检查可变对象的目的地
在 Android 14 上,目标 API 34 及以上的应用创建可变 PendingIntent 时,如果内部 Intent 既没有指定组件,也没有指定包,会触发新的 IllegalArgumentException。因此只加 FLAG_MUTABLE,可能让旧异常变成另一个异常。
上面的接收器示例使用 Intent(context, ReplyReceiver::class.java),已限定具体组件。对应用内部操作优先采用这种写法;确需按 action 解析时,也应核对目标包和接收组件的范围。不要用 FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT 作为通用修复,它会保留这项限制试图阻止的风险。
android:exported 决定组件是否向其他应用公开,是另一项配置。为解决可变性错误而把所有组件改成 exported="true",不会补上缺少的标记,还会改变外部可访问范围。
按实际触发入口验证
修复后,在 Android 12 及以上设备重新执行堆栈对应的入口:创建通知、播放控制或安排任务,确认不再在 PendingIntent 创建时抛出异常。随后实际点击通知,确认页面和记录正确;有直接回复时,再确认接收器能取得回复文本。
若需要兼容 API 23 以下设备,也检查旧分支;目标 API 34 及以上时,再检查 Android 14 上的可变动作。成功标准包括正确触发业务行为,不能只看通知能显示。调整标记后若回复文本丢失,应回查该动作是否确实需要可变性;若依赖升级引入其他行为变化,回退该依赖及相关适配,不要同时撤掉所有已核实的安全限定。


