Android版本应用适配指南
背景
此专栏主题旨在于帮助使用iMin POS机应用开发者,快速了解Android版本升级后应用可能需要适配的内容,可以比对贵司当前的应用是否涉及到下面内容的修改,让贵司的应用可以更快速做出适配,并应用到高版本的POS机上。
Android11版本升级至Android13
说明
Google官方变更说明:https://developer.android.google.cn/about/versions/13/migration?hl=zh-cn
一、功能和权限变更
1. PendingIntent 的变更
温馨提示
如果你的应用未适配Android12以上的版本,且有用到PendingIntent,可能会存在抛出异常导致闪退的情况,所以您可以根据下面的说明做适配或者进Android developer官网查看更详细的内容!
异常如下:
Task exception on worker thread: java.lang.IllegalArgumentException: com.imin.apitest:
Targeting S+ (version 31 and above) requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE
be specified when creating a PendingIntentPendingIntent 的核心变更是 FLAG_MUTABLE 和 FLAG_IMMUTABLE 标志的强制要求,这是 Android 12 引入的,在 Android 13 中依然是必须遵守的规则。
1.1. Android 12 及以上主要变更
变更说明:
强制性标志:在创建 PendingIntent 时,必须显式指定其可变性:FLAG_MUTABLE(可变的)或 FLAG_IMMUTABLE(不可变的)。
安全性提升:此举旨在防止 PendingIntent 被恶意应用拦截并篡改其内部 Intent,从而提升安全性。
默认行为移除:在 Android 11 及以前,没有指定标志时有一个默认行为。现在这个默认行为被移除了,不指定标志会导致 IllegalArgumentException。
适配方案:
优先使用 FLAG_IMMUTABLE:除非你确切地知道需要允许系统或其他应用修改你放入 PendingIntent 中的 Intent,否则为了安全,应始终使用 FLAG_IMMUTABLE。这是最常见的情况。
仅在需要时使用 FLAG_MUTABLE:例如,当你将 PendingIntent 提供给系统 UI(如自定义通知的按钮操作)使用,并且希望系统在发送回 Intent 时填充一些额外数据(如 ACTION_SEND 的额外数据),这时才需要使用 FLAG_MUTABLE。
代码示例:
情况一:大多数场景,使用 FLAG_IMMUTABLE
比如USB 设备权限请求,当你的应用需要访问 USB 设备时,必须获得用户授权
private static final String ACTION_USB_PERMISSION = "com.example.USB_PERMISSION";
// 创建权限请求的 PendingIntent
private void requestUSBPermission(UsbDevice device) {
UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
// 创建 BroadcastReceiver 来接收权限结果
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
registerReceiver(usbPermissionReceiver, filter);
// 创建 PendingIntent
Intent permissionIntent = new Intent(ACTION_USB_PERMISSION);
permissionIntent.setPackage(getPackageName()); // 确保发送到当前应用
PendingIntent pendingIntent = PendingIntent.getBroadcast(
this,
0,
permissionIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
// 请求权限
usbManager.requestPermission(device, pendingIntent);
}情况二:需要系统修改 Intent 的场景,使用 FLAG_MUTABLE
// 例如,一个在通知中用于回复消息的 Action Button
// 系统需要将用户输入的文本作为一个 Extra 添加到你的 Intent 中
Intent replyIntent = new Intent(this, MessageReplyReceiver.class);
replyIntent.setAction("com.yourapp.ACTION_REPLY");
PendingIntent replyPendingIntent = PendingIntent.getBroadcast(
applicationContext,
conversationId, // 使用唯一 ID
replyIntent,
PendingIntent.FLAG_MUTABLE // 允许系统修改此 Intent
);
// 然后将 replyPendingIntent 设置到通知的 Action 上
Notification.Action action = new Notification.Action.Builder(
Icon.createWithResource(this, R.drawable.ic_reply),
"Reply",
replyPendingIntent
).build();修改说明:
//之前使用方法
PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_USB_PERMISSION), 0);
//升级Android13之后
PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_USB_PERMISSION),
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);1.2. Android 13 对 PendingIntent 的额外细化
Android 13 进一步细化了可变 PendingIntent 的行为,要求开发者更精确地声明其用途。
变更说明:
对于使用 FLAG_MUTABLE 的 PendingIntent,现在可以(并且推荐)通过 setter 方法(如 setActivity, setBroadcast, setService)来预先指定其目标组件。这为系统提供了更多信息,有助于安全性。
适配方案(针对使用 FLAG_MUTABLE 的场景):
// Android 13+ 更安全的方式创建可变 PendingIntent (用于 Broadcast)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val replyIntent = Intent("com.yourapp.ACTION_REPLY")
val replyPendingIntent: PendingIntent = PendingIntent.getBroadcast(
applicationContext,
conversationId.toInt(),
replyIntent,
PendingIntent.FLAG_MUTABLE
)
} else {
// ... 旧版本代码
}注意
对于 getActivity 和 getService,API 本身已经隐含了目标组件,所以这个变化对 getBroadcast 影响最大。
2. 组件声明属性export的变化
在Android 12及更高版本中,android:exported属性必须显式设置,设为true可使组件可被其他应用调用。
变更说明:
Android 12(API 31)引入了更严格的安全要求,要求所有<activity>、<service>和<receiver>组件必须显式声明android:exported属性。
android:exported的作用:
true:允许其他应用(或系统)启动该组件(如通过隐式Intent)。
false:仅限同一应用或具有相同用户ID的应用访问。
不设置:在Android 12+中会直接报错。
适配方案:
主Activity:通常设为true(否则无法通过桌面图标启动)。
后台Service:若需被其他应用调用(如辅助功能),设为true;否则设为false
BroadcastReceiver:系统广播(如开机启动)需设为true,自定义广播可设为false。
代码示例:
<manifest ...>
<application ...>
<!-- 主Activity需设为exported=true -->
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- 后台Service(仅限应用内调用) -->
<service
android:name=".InternalService"
android:exported="false" />
</application>
</manifest>3. 安全性变更
变更说明:停止使用共享用户 ID
适配方案:
如果您的应用使用已废弃的 android:sharedUserId 属性,并且不再依赖于该属性的功能,您可以将 android:sharedUserMaxSdkVersion 属性设置为 32
代码示例:
<manifest ...>
<!-- To maintain backward compatibility, continue to use
"android:sharedUserId" if you already added it to your manifest. -->
android:sharedUserId="SHARED_PACKAGE_NAME"
android:sharedUserMaxSdkVersion="32"
...
</manifest>这个属性会告知系统,您的应用不再依赖于共享用户 ID。如果您的应用声明 android:sharedUserMaxSdkVersion 并且首次安装在搭载 Android 13 或更高版本的设备上,则应用的行为就像您从未定义过 android:sharedUserId 一样。更新后的应用仍会使用现有的共享用户 ID。
注意
如果您已在清单中定义了 android:sharedUserId 属性,请不要将其移除。这样做会导致应用更新失败。
建议
对于第三方应用,强烈不建议再使用 sharedUserId。这个特性本身就被 Google 视为过时且不安全的,因为它打破了 Android 固有的应用沙盒隔离机制。推荐使用ContentProvider
4. 通知运行时权限
遵循最佳实践,例如在用户与通知功能相关交互时请求权限,并解释权限的用途。
变更说明:
从 Android 13(API 级别 33)开始,应用向用户发送通知需要请求新的运行时权限 (POST_NOTIFICATIONS),而不仅仅是在清单文件中声明。
适配方案:
更新应用的 targetSdkVersion 至 33 或更高。
在应用的清单文件中声明 POST_NOTIFICATIONS 权限。
在应用运行时,通过 Activity 的 requestPermissions 方法或 ActivityResultContracts.RequestPermission 合约向用户请求该权限。
代码示例:
<manifest ...>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application ...>
...
</application>
</manifest>二、常见问题
1. Presentation的变更
说明:https://developer.android.google.cn/reference/kotlin/android/view/WindowManager.LayoutParams
如果此前在Presentation的实现类里面做了如下操作(通常用于副屏异显)
if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
// For details such as picture in picture, please check the android sdk
getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
}else {
// Android versions below 8.0 use the following apis to achieve the above functions
getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY);
}可能会报如下错误:
java.lang.IllegalArgumentException: Window type mismatch. Window Context's window type is 2037,
while LayoutParams' type is set to 2038. Please create another Window Context via
createWindowContext(getDisplay(), 2038, null) to add window with type:2038可以参考如下修改:
if(Build.VERSION.SDK_INT >=Build.VERSION_CODES.S_V2){
//设置2037或者不设置采用默认的type
getWindow().setType(2037);
}else if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
// 画中画等详细请查看android sdk For details such as picture in picture, please check the android sdk
getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
}else {
// 8.0 以下的安卓版本要实现上述功能使用以下api Android versions below 8.0 use the following apis to achieve the above functions
getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY);
}2. 无法获取粘贴板内容
Android 13+:应用必须在前台可见才能读取剪贴板
前台可见性要求:应用只有在前台可见时才能读取剪贴板内容
自动清除:剪贴板内容在一段时间后会自动清除
预览限制:防止应用偷偷读取剪贴板
适配策略:
在 onResume() 等生命周期方法中读取
使用剪贴板监听器结合前台状态检查
提供用户友好的提示信息
兼容性:
低版本 Android 仍按原有方式工作
判断应用是否在前台:
public boolean canReadClipboard(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> appProcesses = activityManager.getRunningAppProcesses();
if (appProcesses != null) {
for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
if (appProcess.pid == android.os.Process.myPid()) {
return appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
}
}
}
return false;
}
return true;
}Android13版本升级至Android15
说明
Google官方变更说明:https://developer.android.google.cn/about/versions/15/get?hl=zh-cn
一、功能和权限变更
1. 最低可安装目标 API 级别
变更说明:
用户无法安装 targetSdkVersion 低于 24 的应用。
适配方案:
需要尽快升级targetSdkVersion大于24,如果需要上传各大应用商店(如Google Play,小米/华为/OPPO/Vivo等应用市场),需要根据平台的上传规则调整targetSdkVersion,建议升级至35.
代码示例:
android {
compileSdk = 35
defaultConfig {
applicationId 'com.imin.xxx'
minSdkVersion 30
targetSdkVersion 35 //需要大于等于24,建议升级至35
...
...
}
}2. 支持 16 KB 页面大小
官方文档:https://developer.android.google.cn/guide/practices/page-sizes?hl=zh-cn#build
注意
自 2025 年 11 月 1 日起,提交到 Google Play 且以 Android 15 及更高版本为目标平台的所有新应用和现有应用更新都必须在 64 位设备上支持 16 KB 页面大小。国内应用商店目前暂无强制要求,但建议提前布局
变更说明:
新一代设备开始采用16KB内存页(Page Size)机制,逐步替代传统的4KB内存页设计。此项底层变更对应用兼容性产生直接影响,特别是对依赖Native层库、JNI接口或自定义内存管理模块的应用程序
适配方案:
检测应用是否使用了native代码,可以通过Android Studio的apk分析器进行辅助分析
查看 lib 文件夹,其中会托管共享对象 (.so) 文件(如有)。如果存在任何共享对象文件,则表明您的应用使用了原生代码。对齐列会针对存在对齐问题的任何文件显示警告消息。
如果没有共享对象文件或没有 lib 文件夹,则表示您的应用未使用原生代码。
代码示例:
a. 使用CMake编译so库的场景
#set(CMAKE_SHARED_LINKER_FLAGS "-Wl,-z,max-page-size=16384")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,-z,max-page-size=16384 -Wl,-z,common-page-size=16384")b. 使用ndk-build编译so库的场景
LOCAL_LDFLAGS += "-Wl,-z,max-page-size=16384"3. 安全变更
3.1. 对隐式 intent 和待处理 intent 的限制
防止恶意应用拦截意在供应用内部组件使用的隐式 intent。
变更说明:
对于以 Android 14(API 级别 34)或更高版本为目标平台的应用,Android 会通过以下方式限制应用向内部应用组件发送隐式 intent:
隐式 intent 只能传送到导出的组件。应用必须使用显式 intent 传送到未导出的组件,或将该组件标记为已导出。
如果应用通过未指定组件或软件包的 intent 创建可变待处理 intent,系统会抛出异常
适配方案:
启动非导出(export=false)的 activity,应用应改用显式 intent
直接按照原来的方式启动,可能会抛出异常,导致应用闪退
// Throws an ActivityNotFoundException exception when targeting Android 14.
context.startActivity(new Intent("com.example.action.APP_ACTION"));代码示例:
<activity
android:name=".AppActivity"
android:exported="false">
<intent-filter>
<action android:name="com.example.action.APP_ACTION" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>//显式Intent启动
// This makes the intent explicit.
Intent explicitIntent =
new Intent("com.example.action.APP_ACTION")
explicitIntent.setPackage(context.getPackageName());
context.startActivity(explicitIntent);3.2. 前台服务类型是必填项
变更说明:
Android10 引入 foregroundServiceType,Android 14 进一步细化前台服务类型要求,必须明确声明服务用途
如果您的应用以 Android 14(API 级别 34)或更高版本为目标平台,则必须为应用中的每个前台服务至少指定一项前台服务类型。您应选择一个能代表应用用例的前台服务类型。系统需要特定类型的前台服务满足特定用例。
适配方案:
在 AndroidManifest.xml 中为前台服务声明具体的服务类型,并在启动服务时设置相应的类型
代码示例:
<!-- AndroidManifest.xml -->
<service
android:name=".MyForegroundService"
android:foregroundServiceType="location|camera" />//service代码里面 大于34需要增加type
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
startForeground(1,builder.build(),FOREGROUND_SERVICE_TYPE_LOCATION || FOREGROUND_SERVICE_TYPE_CAMERA);
}else{
startForeground(1, builder.build());
}3.3. 在运行时注册的广播接收器必须指定导出行为
变更说明:
在运行时注册的广播接收器必须指定导出行为,以 Android 14(API 级别 34)或更高版本为目标平台的应用必须明确声明接收器的导出状态
适配方案:
使用 Context.registerReceiver() 注册广播接收器时,必须指定 RECEIVER_EXPORTED 或 RECEIVER_NOT_EXPORTED 标志
代码示例:
IntentFilter filter = new IntentFilter("ACTION_MY_BROADCAST");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED);
} else {
registerReceiver(receiver, filter);
}RECEIVER_EXPORTED含义:
接收器可以接收来自其他应用的广播
使用场景:系统广播或需要跨应用通信的广播
示例:电池状态变化、网络状态变化等系统广播
RECEIVER_NOT_EXPORTED含义:
接收器只能接收来自本应用或系统的广播
使用场景:应用内部使用的私有广播
示例:应用内自定义的 Action 广播
二、常见问题
1.1. 第三方SDK不支持16 KB怎么办?
建议:
联系SDK提供商要求更新。
寻找替代方案。
考虑移除该SDK。
如果是开源的,自己编译兼容版本
文档更新说明
提示
本文档会持续更新,尽情关注