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 PendingIntent

PendingIntent 的核心变更是 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接口或自定义内存管理模块的应用程序

适配方案:

  1. 检测应用是否使用了native代码,可以通过Android Studio的apk分析器进行辅助分析

  2. 查看 lib 文件夹,其中会托管共享对象 (.so) 文件(如有)。如果存在任何共享对象文件,则表明您的应用使用了原生代码。对齐列会针对存在对齐问题的任何文件显示警告消息。

  3. 如果没有共享对象文件或没有 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。

  • 如果是开源的,自己编译兼容版本

1.2. 设置锁屏后服务启动使用了SharedPreferences报错

错误信息:

SharedPreferences in credential encrypted storage are not available until after user (id 0) is unlocked

建议:

监听开机广播后再做服务启动和SharedPreferences的处理

文档更新说明

提示

本文档会持续更新,尽情关注