Android 15 适配完全指南:Edge-to-Edge 与 16KB 页面适配

7 阅读7分钟

Android 15 适配完全指南:Edge-to-Edge 与 16KB 页面适配

Android 15(API 35)已于 2024 年底发布,Google Play 要求:2025 年 8 月起新上架应用、11 月起在架应用更新都必须完成 Android 15 适配。本文系统梳理 Android 15 的核心变更及适配方案,帮助开发者快速完成升级。


一、适配前置准备

在开始适配前,需要先完成以下环境升级,避免后续出现兼容性问题:

项目要求说明
targetSdkVersion35针对 Android 15
compileSdkVersion35编译时获取最新 API
Android StudioKoala 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 默认应用显示区域延伸至全屏,不再自动避开状态栏/导航栏
  • 状态栏默认透明
  • systemStatusBarColor API 及 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 前台服务超时限制

影响范围: 仅针对 dataSyncmediaProcessing 两类前台服务。

规则说明:

  1. 应用处于后台时,这两类服务 24 小时内最多运行 6 小时
  2. 达到时长限制后系统回调 Service.onTimeout(int, int) 方法,服务不再被视为前台服务
  3. 回调后需在几秒内调用 stopSelf(),否则系统抛出异常
  4. 用户将应用切回前台可重置计时,但重新启动同类型服务不会重置计时
  5. 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]);
语言代码处理变更
语言旧代码(已废弃)新代码
希伯来语iwhe
意第绪语jiyi
印度尼西亚语inid

对应的资源文件夹需要同步修改,例如 values-iw 改为 values-he


四、适配检查清单

完成适配后,建议对照以下清单进行检查:

  • targetSdkVersioncompileSdkVersion 已升至 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 类型声明,如果使用了 dataSyncmediaProcessing 类型,则必须适配。


六、参考资源


如果本文对你有帮助,欢迎点赞收藏。如有疑问,欢迎在评论区交流。