2025年的第一篇Android适配,比以往来的更晚一些。废话不多说,我们开始!!
准备工作
首先将我们项目中的 targetSdk
和compileSdk
升至 35。
- 推荐使用Android Studio Koala Feature Drop | 2024.1.2或更高版本。
- AGP版本最低升级到8.3.0
plugins {
id 'com.android.application' version '8.3.0' apply false
}
影响Android 15上所有应用
1.最低可安装的目标 API 级别提升
Android 15要求应用的targetSDK最低为24,否则无法安装。这块和之前Android 14一样,以后每年应该都会加1,所以注意提前处理,避免新系统无法安装。
2.16KB页面大小支持
影响范围
使用任何NDK库的应用都需要重新编译以支持16KB页面大小的设备。这点之前还是默认开启的,后面调整为了不强制适配。但是自 2025 年 11 月 1 日起,提交到 Google Play 且以 Android 15 及更高版本的设备为目标平台的所有新应用和现有应用更新都必须支持 64 位设备上的 16 KB 页面大小。
它带来的性能提升有以下几点:
应用启动时间平均缩短3.16%,部分应用可达30%
应用启动时耗电量平均减少4.56%
相机启动速度:热启动平均加快4.48%,冷启动平均加快6.60%
系统启动时间平均改善8%
适配步骤
升级AGP版本到8.3或更高,推荐AGP 8.5.1 或更高版本。
使用16KB ELF对齐编译应用
检查引用特定页面大小的代码
具体操作,参考官方文档。
我们是做出海项目的,所以比较关注这块,目前使用到的三方sdk都有支持。比如我们项目中使用了MMKV,虽然MMKV 2.x版本支持16KB页面大小,但不支持32位,所以还不能直接升级使用。1.3.x版本支持32位,但不支持16KB页面大小。作者开始也明确不会支持,本来还打算自己修改重新编译。好在前一阵谷歌有了上架的限制,作者也对MMKV 1.3.x版本做了相关适配支持,这里也是表示感谢。
3.当用户强制停止应用时,widget被停用
如果用户在搭载 Android 15 的设备上强制停止应用,系统会暂时停用该应用的所有widget。这些 widget 会灰显,用户无法与其互动。这是因为从 Android 15 开始,当系统强制停止应用时,会取消应用的所有待处理 intent
。
系统会在用户下次启动应用时重新启用这些微件。
影响以Android 15或更高版本为目标平台的应用
以下都是影响targetSDK >= 35的应用,适配时需要重点关注
1.edge-to-edge
首先说个影响面比较大的,那就是edge-to-edge,翻译过来就是边到边,应用的显示区域延伸到了全屏,不会避开状态栏/导航栏区域。
首先状态栏/手势导航条区域默认将透明,三键导航默认不透明度为80%。
比如我之前都会给布局添加android:fitsSystemWindows="true"
和设置状态栏颜色进行沉浸状态栏的适配,但是由于Android 15状态栏现在变为透明,且setStatusBarColor
和 R.attr#statusBarColor
已废弃,对 Android 15 没有任何作用。所以出现了下面的情况:
状态栏透明,因此颜色为页面的背景色,和标题栏颜色不同。
未添加android:fitsSystemWindows="true"
的布局,底部按钮绘制在系统导航栏后面。
适配方法:
简单粗暴的方法就是停用edge-to-edge,可以创建values-v35
,在theme中添加windowOptOutEdgeToEdgeEnforcement
并设为true:
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
...
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
</style>
</resources>
不过windowOptOutEdgeToEdgeEnforcement
在以Android 16为目标平台的应用将不再生效,无法停用edge-to-edge。所以这个只是缓兵之计,如果你的适配工作量比较大且紧急,可以使用此方法。
精细化适配方法,首先注意两点:
setStatusBarColor
在Android 15上不生效。setNavigationBarColor
只针对三键导航栏有效果,手势导航条无效。
如果你使用Compose的话:
- 使用Material 3 组件(androidx.compose.material3),例如
TopAppBar
、BottomAppBar
和NavigationBar
,这些组件不会受到影响,因为它们会自动处理insets
。 - 使用Material 2 组件(androidx.compose.material),或者自定义的 Composables,这些组件不会自动处理
insets
。需要自己设置padding
。
非compose应用:
- 在应用布局中添加
android:fitsSystemWindows="true"
,但是需要检查状态栏颜色是否符合UI要求。 - 未添加
android:fitsSystemWindows="true"
的布局,相信大多数都处理了状态栏,所以需要检查页面底部在系统导航栏后面的情况。同时查看导航栏颜色是否符合UI要求。
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) {
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.root), new OnApplyWindowInsetsListener() {
@NonNull
@Override
public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) {
Insets statusBarInsets = insets.getInsets(WindowInsetsCompat.Type.statusBars());
Insets navigationBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars());
v.setPadding(
navigationBarInsets.left,
statusBarInsets.top, // 如果之前处理了状态栏,可以改为0
navigationBarInsets.right,
navigationBarInsets.bottom
);
return insets;
}
});
}
2.前台服务相关变更
前台服务超时限制
系统会限制某些前台服务在应用处于后台时允许运行的时长。目前,此限制仅适用于 dataSync
和 mediaProcessing
前台服务类型。
限制规则:
24小时内总共只能运行6小时
达到限制后,系统调用
Service.onTimeout(int, int)
方法,服务将不再被视为前台服务。服务有几秒钟时间调用
Service.stopSelf()
,如果服务未调用,系统会抛出内部异常。用户将应用打开到前台可重置计时。再开一个
dataSync
时间也不会重新计时。
适配代码参考:
public class MyDataSyncService extends Service {
@Override
public int onTimeout(int startId, int fgsType) {
Log.w("ForegroundService", "服务超时,类型: " + fgsType);
// 实现onTimeout方法,同时必须在几秒内调用stopSelf()
stopSelf();
return super.onTimeout(startId, fgsType);
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
如果24小时内已经运行了 6 个小时,则无法启动另一个dataSync
前台服务,系统会抛出ForegroundServiceStartNotAllowedException
,并显示类似“前台服务类型 dataSync 的时间限制已用完”的错误消息。
public class MainActivity extends AppCompatActivity {
private void startDataSyncService() {
Intent serviceIntent = new Intent(this, MyDataSyncService.class);
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent);
} else {
startService(serviceIntent);
}
} catch (ForegroundServiceStartNotAllowedException e) {
Log.e("Service", "前台服务启动被限制: " + e.getMessage());
// 使用WorkManager等替代方案
scheduleWorkWithWorkManager();
}
}
private void scheduleWorkWithWorkManager() {
// 使用WorkManager作为替代方案
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(DataSyncWorker.class)
.setConstraints(new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.build();
WorkManager.getInstance(this).enqueue(workRequest);
}
}
BOOT_COMPLETED广播接收器限制
在启动 BOOT_COMPLETED
广播接收器方面存在新限制前台服务。BOOT_COMPLETED
接收器不能启动以下类型的前台服务:
- dataSync
- camera
- mediaPlayback
- phoneCall
- mediaProjection
- microphone(自 Android 14 起,microphone 就受到此限制)
如果 BOOT_COMPLETED
接收器尝试启动任何上述类型的前台 服务,系统会抛出ForegroundServiceStartNotAllowedException
。
public class BootReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
// ❌ 不能在BOOT_COMPLETED中启动这些类型的前台服务
// Intent serviceIntent = new Intent(context, DataSyncService.class);
// context.startForegroundService(serviceIntent);
// ✅ 可以使用WorkManager调度任务实现你的需求
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(BootWorker.class)
.setInitialDelay(5, TimeUnit.SECONDS)
.build();
WorkManager.getInstance(context).enqueue(workRequest);
}
}
}
SYSTEM_ALERT_WINDOW权限限制
持有SYSTEM_ALERT_WINDOW
权限的应用需要有可见的overlay窗口才能启动前台服务,也就是说,应用需要先启动 TYPE_APPLICATION_OVERLAY
窗口,并且该窗口需要处于可见状态,然后您才能启动前台服务。否则系统会抛出 ForegroundServiceStartNotAllowedException
。
public class OverlayService extends Service {
private View overlayView;
private WindowManager windowManager;
@Override
public void onCreate() {
super.onCreate();
createOverlayWindow();
}
private void createOverlayWindow() {
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
overlayView = LayoutInflater.from(this).inflate(R.layout.overlay_layout, null);
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT
);
windowManager.addView(overlayView, params);
// 检查窗口可见性
overlayView.setOnWindowVisibilityChangedListener(new View.OnWindowVisibilityChangeListener() {
@Override
public void onWindowVisibilityChanged(int visibility) {
if (visibility == View.VISIBLE) {
// 可见时才可以安全启动前台服务
startForegroundServiceSafely();
}
}
});
}
private void startForegroundServiceSafely() {
if (overlayView != null && overlayView.getWindowVisibility() == View.VISIBLE) {
try {
Intent serviceIntent = new Intent(this, MyForegroundService.class);
startForegroundService(serviceIntent);
} catch (ForegroundServiceStartNotAllowedException e) {
Log.e("Service", "前台服务启动失败: " + e.getMessage());
}
}
}
}
3.提高 intent 安全性
Intent
必须有``action`- 与目标
intent
过滤器匹配
Intent intent = new Intent(context, targetClass);
// 确保Intent有action
intent.setAction(Intent.ACTION_VIEW);
try {
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
Log.e("Intent", "无法启动Activity", e);
}
4.OpenJDK API 变更
Android 15 继续推进 Android 核心库的现代化,以与最新 OpenJDK LTS 版本(OpenJDK 17)的功能保持一致。这些变更可能会影响针对 Android 15(API 级别 35)的应用兼容性,特别是在字符串格式化、集合处理和随机数生成等方面。
字符串格式化 API 变更
Android 15 对以下字符串格式化 API 的参数索引、标志、宽度和精度验证变得更加严格:
String.format(String, Object[])
String.format(Locale, String, Object[])
Formatter.format(String, Object[])
Formatter.format(Locale, String, Object[])
使用参数索引 0 时会抛出异常:
// 错误示例 - 会抛出异常
String result = String.format("%0s", "test");
// IllegalFormatArgumentIndexException: Illegal format argument index = 0
// 正确做法 - 使用索引 1
String result = String.format("%1$s", "test");
// 或者不使用索引
String result = String.format("%s", "test");
Arrays.asList(…).toArray() 的组件类型变更
使用 Arrays.asList(…).toArray() 时,生成的数组的组件类型现在是 Object,而不是底层数组元素的类型。因此,以下代码会抛出 ClassCastException:
String[] elements = (String[]) Arrays.asList("a", "b", "c").toArray();
适配方案
- 使用类型安全的方法‘
// 正确做法 - 指定数组类型
List<String> list = Arrays.asList("a", "b", "c");
String[] array = list.toArray(new String[0]);
- 使用 List.of()(推荐)
// 使用 Java 9+ 的 List.of()
List<String> list = List.of("a", "b", "c");
String[] array = list.toArray(new String[0]);
语言代码处理方式变更
使用 Locale
API 时,希伯来语、意第绪语和印度尼西亚语的语言代码不再转换为已废弃的形式(希伯来语:iw、意第绪语:ji、印度尼西亚语:in)。为其中一种语言区域指定语言代码时,请改用 ISO 639-1 中的代码(希伯来语:he、意第绪语:yi、印度尼西亚语:id)。
例如:
旧文件夹:
- values-iw/(希伯来语)
- values-ji/(意第绪语)
- values-in/(印度尼西亚语)
新文件夹:
- values-he/(希伯来语)
- values-yi/(意第绪语)
- values-id/(印度尼西亚语)
其他还有PDF的优化,HDR 余量控制,请求音频焦点的限制等变更。因为我实际也没有适配,这里就不说明了,有需要的可以查看官方文档。