Android 15 适配完全指南:Edge-to-Edge 与 16KB 页面适配
Android 15(API 35)已于 2024 年底发布,Google Play 要求:2025 年 8 月起新上架应用、11 月起在架应用更新都必须完成 Android 15 适配。本文系统梳理 Android 15 的核心变更及适配方案,帮助开发者快速完成升级。
一、适配前置准备
在开始适配前,需要先完成以下环境升级,避免后续出现兼容性问题:
| 项目 | 要求 | 说明 |
|---|---|---|
| targetSdkVersion | 35 | 针对 Android 15 |
| compileSdkVersion | 35 | 编译时获取最新 API |
| Android Studio | Koala Feature Drop 2024.1.2+ | 支持 Android 15 特性 |
| AGP(Gradle Plugin) | 8.3.0+,推荐 8.5.1+ | 支持 16KB 页面适配 |
Gradle 配置示例:
// app/build.gradle
android {
compileSdk 35
defaultConfig {
targetSdk 35
// ...
}
}
// 项目根目录 gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
二、全量影响(所有应用都会受影响)
以下变更对所有运行在 Android 15 设备上的应用生效,无论 targetSdk 是否为 35。
2.1 最低可安装 targetSdk 要求提升
变更说明: Android 15 要求应用的 targetSdkVersion 最低为 24(Android 7.0),低于此值的应用将无法安装。
适配建议: 检查应用的 targetSdkVersion,确保不低于 24。该要求后续每年可能逐步提升,建议提前做好版本规划。
⚠️ 注意:如果应用在 Google Play 上架,还需要满足 Google Play 的 targetSdk 要求(通常要求最新版本前一年的版本)。
2.2 16KB 页面大小强制适配(出海应用重点关注)
背景: Android 15 引入对 16KB 内存页面的支持,2025 年 11 月 1 日起,提交到 Google Play 且以 Android 15+ 为目标平台的所有新应用/应用更新,必须支持 64 位设备的 16KB 页面大小。
影响范围: 所有使用 NDK 库(包含三方 SDK 的 .so 库)的应用都需要重新编译适配。
性能收益(官方数据):
- 应用启动时间平均缩短 3.16%,部分应用可达 30%
- 应用启动耗电量平均减少 4.56%
- 相机热启动速度平均加快 4.48%,冷启动平均加快 6.60%
- 系统启动时间平均改善 8%
适配步骤:
第一步: 升级 AGP 到 8.3+(推荐 8.5.1+)
第二步: 开启 16KB ELF 对齐编译
// gradle.properties
android.bundle.enableUncompressedNativeLibs=true
第三步: 检查代码中硬编码 4096 页大小的逻辑
// ❌ 错误:硬编码页大小
#define PAGE_SIZE 4096
// ✅ 正确:动态获取页大小
#include <unistd.h>
long page_size = sysconf(_SC_PAGESIZE); // Linux/Android
真实踩坑案例: MMKV 2.x 版本支持 16KB 页面但不支持 32 位;1.3.x 版本原先不支持 16KB,后续作者针对 Google Play 上架要求做了适配,可直接升级使用。
2.3 强制停止应用后 Widget 停用
变更说明: 用户在 Android 15 设备上强制停止应用后,系统会取消应用所有待处理 Intent,导致应用的所有 Widget 灰显、无法交互。
行为说明: 用户下次主动启动应用后,Widget 会自动恢复,开发者无需额外适配。
三、targetSdk ≥ 35 需要适配的核心变更
以下变更仅在 targetSdkVersion = 35 时生效,是适配的重点工作。
🔴 高优先级:Edge-to-Edge(边到边显示)强制适配
这是所有应用中影响面最大的变更。
变更说明:
- Android 15 默认应用显示区域延伸至全屏,不再自动避开状态栏/导航栏
- 状态栏默认透明
- 原
systemStatusBarColorAPI 及R.attr#statusBarColor属性已废弃,在 Android 15 上无效
常见问题:
- 状态栏透明后与标题栏出现色差
- 底部按钮绘制到系统导航栏后方,导致点击区域被遮挡
适配方案对比
| 方案类型 | 实现方式 | 注意事项 |
|---|---|---|
| 临时方案(仅缓兵之计) | 创建 values-v35 资源目录,在 Theme 中添加 <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item> | 该配置在 targetSdk ≥ 36(Android 16)时失效,无法长期使用 |
| 正式适配(Compose 应用) | 使用 Material 3 组件(TopAppBar / BottomAppBar / NavigationBar)会自动处理 Insets;使用 Material 2 或自定义组件时手动添加对应 padding | 可直接复用官方 Insets 处理 API |
| 正式适配(非 Compose 应用) | 1. 布局添加 android:fitsSystemWindows="true",同时检查状态栏颜色是否符合 UI 要求2. 手动处理 WindowInsets | 若之前已自定义处理状态栏,可将 statusBarInsets.top 设为 0 避免重复 padding |
非 Compose 应用适配代码示例
// 方案一:使用 fitsSystemWindows(适合简单页面)
// 在布局根 View 中添加
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<!-- 方案二:手动处理 WindowInsets(推荐,更灵活) -->
```kotlin
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
fun setupEdgeToEdge(rootView: View) {
ViewCompat.setOnApplyWindowInsetsListener(rootView) { v, insets ->
val statusBarInsets = insets.getInsets(WindowInsetsCompat.Type.statusBars())
val navigationBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
v.setPadding(
navigationBarInsets.left,
statusBarInsets.top,
navigationBarInsets.right,
navigationBarInsets.bottom
)
insets
}
}
Compose 应用适配代码示例
// Material 3 组件自动处理 Insets,无需额外代码
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyApp() {
MaterialTheme {
Scaffold(
topBar = { TopAppBar(title = { Text("Android 15 适配") }) },
bottomBar = { NavigationBar { /* ... */ } }
) { paddingValues ->
// paddingValues 已包含状态栏和导航栏的 Insets
Content(modifier = Modifier.padding(paddingValues))
}
}
}
🔴 高优先级:前台服务相关限制
如果应用中使用了前台服务(ForegroundService),需要重点关注以下限制。
3.1 前台服务超时限制
影响范围: 仅针对 dataSync、mediaProcessing 两类前台服务。
规则说明:
- 应用处于后台时,这两类服务 24 小时内最多运行 6 小时
- 达到时长限制后系统回调
Service.onTimeout(int, int)方法,服务不再被视为前台服务 - 回调后需在几秒内调用
stopSelf(),否则系统抛出异常 - 用户将应用切回前台可重置计时,但重新启动同类型服务不会重置计时
- 24 小时时长用尽后启动对应类型服务,会抛出
ForegroundServiceStartNotAllowedException
适配方案:
// 重写 onTimeout 方法,主动停止服务
override fun onTimeout(startId: Int, fgsType: Int) {
Log.w("ForegroundService", "服务超时,类型: $fgsType")
stopSelf()
super.onTimeout(startId, fgsType)
}
推荐方案: 改用 WorkManager 替代前台服务,示例代码:
// 使用 WorkManager 调度后台任务(替代 dataSync 类型前台服务)
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val workRequest = OneTimeWorkRequestBuilder<MyWorker>()
.setConstraints(constraints)
.build()
WorkManager.getInstance(context).enqueue(workRequest)
3.2 BOOT_COMPLETED 广播接收器限制
规则说明: BOOT_COMPLETED 广播接收器中,不得启动 dataSync / camera / mediaPlayback / phoneCall / mediaProjection / microphone 类型的前台服务,否则抛出 ForegroundServiceStartNotAllowedException。
适配方案: 不要在广播接收器中直接启动前台服务,改用 WorkManager 延迟调度任务。
// ❌ 错误:在 BootReceiver 中直接启动前台服务
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// 在 Android 15 上会抛异常
context.startForegroundService(Intent(context, MyService::class.java))
}
}
// ✅ 正确:使用 WorkManager 延迟调度
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val workRequest = OneTimeWorkRequestBuilder<MyWorker>()
.setInitialDelay(5, TimeUnit.SECONDS) // 延迟启动,避免启动时受限
.build()
WorkManager.getInstance(context).enqueue(workRequest)
}
}
3.3 SYSTEM_ALERT_WINDOW 权限限制
规则说明: 持有 SYSTEM_ALERT_WINDOW 权限的应用,需要先显示可见的 TYPE_APPLICATION_OVERLAY 悬浮窗,才能启动前台服务,否则抛出异常。
适配方案: 启动前台服务前先检查悬浮窗可见性。
// 检查悬浮窗是否可见
if (Settings.canDrawOverlays(context)) {
// 显示悬浮窗后再启动前台服务
showOverlayWindow {
startForegroundService(context, Intent(context, MyService::class.java))
}
} else {
// 请求 SYSTEM_ALERT_WINDOW 权限
requestOverlayPermission(context)
}
🟡 中优先级:Intent 安全性提升
变更说明: Android 15 要求启动 Activity 的 Intent 必须设置 action,且需与目标组件的 intent 过滤器匹配,否则可能出现异常。
适配方案:
// ✅ 正确:显式设置 action
val intent = Intent(context, targetClass).apply {
action = Intent.ACTION_VIEW // 必须设置 action
}
try {
context.startActivity(intent)
} catch (e: ActivityNotFoundException) {
Log.e("Intent", "无法启动 Activity", e)
}
// ✅ 更好:使用显式 Intent(指定包名和类名,最安全)
val explicitIntent = Intent().apply {
component = ComponentName(context, targetClass)
action = Intent.ACTION_VIEW
}
🟡 中优先级:OpenJDK API 变更(对齐 OpenJDK 17)
Android 15 核心库对齐 OpenJDK 17,以下常用 API 出现行为变更:
字符串格式化校验变严
// ❌ 错误:使用参数索引 0(在 Android 15 上会抛 IllegalFormatArgumentIndexException)
String result = String.format("%0$s", "hello");
// ✅ 正确:使用 %1$s 或不指定索引
String result1 = String.format("%1$s", "hello");
String result2 = String.format("%s", "hello"); // 不指定索引,默认按顺序
Arrays.asList(...).toArray() 返回类型变更
// ❌ 错误:直接强转(会抛 ClassCastException)
String[] array = (String[]) Arrays.asList("a", "b").toArray();
// ✅ 正确:使用 toArray(new Type[0]) 指定类型
String[] array = Arrays.asList("a", "b").toArray(new String[0]);
// ✅ 推荐:使用 Java 9+ 的 List.of()
String[] array = List.of("a", "b").toArray(new String[0]);
语言代码处理变更
| 语言 | 旧代码(已废弃) | 新代码 |
|---|---|---|
| 希伯来语 | iw | he |
| 意第绪语 | ji | yi |
| 印度尼西亚语 | in | id |
对应的资源文件夹需要同步修改,例如 values-iw 改为 values-he。
四、适配检查清单
完成适配后,建议对照以下清单进行检查:
-
targetSdkVersion和compileSdkVersion已升至 35 - AGP 已升级到 8.3.0+(推荐 8.5.1+)
- 应用支持 16KB 页面大小(使用了 NDK 的应用必须检查)
- Edge-to-Edge 已适配(Compose 或非 Compose 方案)
- 前台服务已处理超时逻辑,或已迁移到 WorkManager
-
BOOT_COMPLETED接收器中不再直接启动前台服务 - 启动 Activity 的 Intent 已设置 action
- 代码中不再使用废弃的语言代码(iw/ji/in)
- 在 Android 15 真机或模拟器上完整测试
五、常见问题 FAQ
Q:如果暂时无法完成 Edge-to-Edge 适配,有没有临时方案?
A:可以在 values-v35 资源目录的 Theme 中添加 windowOptOutEdgeToEdgeEnforcement = true,但这只是缓兵之计,在 targetSdk ≥ 36(Android 16)时该配置会失效。
Q:我的应用没有使用 NDK,还需要关注 16KB 页面适配吗?
A:如果应用完全不使用任何 .so 库(包括三方 SDK 中的 so),则不受 16KB 页面大小影响。但建议检查依赖树中是否包含 NDK 库。
Q:如何快速判断应用是否需要适配前台服务超时?
A:搜索项目中的 ForegroundService 类型声明,如果使用了 dataSync 或 mediaProcessing 类型,则必须适配。
六、参考资源
- Android 15 官方变更列表(开发者文档)
- Edge-to-Edge 适配指南
- 16KB 页面大小适配指南
- 前台服务约束(Android 15)
- Google Play targetSdk 要求
如果本文对你有帮助,欢迎点赞收藏。如有疑问,欢迎在评论区交流。