Android 17 正式发布!target 37 一大批旧代码直接不能用了

0 阅读7分钟

Android 17 已经正式发布,对应 API level 37。

target SDK 37 以后,有几类旧假设会失效:大屏不能继续锁方向和比例,部分配置变化默认不再重建 Activity,本地网络访问要走新权限或系统选择器,一些运行时反射写法也会直接失败。

如果项目准备升级 targetSdk,下面这些点要先进兼容性验证。

Image

大屏限制

Android 17 继续推进大屏和桌面模式。对 target API 37 的 App,在 sw >= 600dp 的大屏设备上,系统会忽略一批旧限制,包括 screenOrientationsetRequestedOrientation()resizeableActivity=false,以及 minAspectRatio / maxAspectRatio 这类比例约束。游戏按 Google Play 的 app category 仍有豁免。

这意味着很多“强制竖屏”的业务页面,在平板、折叠屏、桌面窗口里要按可变窗口处理。以前 Manifest 写法能兜住,现在系统会把窗口尺寸和姿态交还给用户。

旧代码里常见的写法大概是这样:

<activity
    android:name=".MainActivity"
    android:screenOrientation="portrait"
    android:resizeableActivity="false" />

升级 target 37 后,页面要在窄屏、宽屏、分屏、自由窗口下正常使用。登录、支付、相机预览、地图、播放器这些页面,最容易暴露布局假设。

Android 17 还加入了 App Bubbles、Bubble Bar 和桌面交互式 PiP。用户可以把 App 变成浮动 bubble,大屏任务栏会管理这些 bubble;桌面 PiP 也不再只是只读小窗,而是保留交互。页面最小宽高、焦点、输入法、返回栈,都要按真实窗口状态检查。

Image

Activity 重建

Android 17 改了部分配置变化的默认处理方式。对一些不需要完整 UI 重绘的 configuration change,系统默认不再重启 Activity,而是把变化发给 onConfigurationChanged()

原文里列到的包括 CONFIG_KEYBOARDCONFIG_KEYBOARD_HIDDENCONFIG_NAVIGATIONCONFIG_TOUCHSCREENCONFIG_COLOR_MODE。如果项目以前依赖 Activity 重建来重新读取资源、重建 UI 或刷新状态,这里会有行为差异。

需要完整重建时,Manifest 里可以显式声明新的 android:recreateOnConfigChanges

<activity
    android:name=".EditorActivity"
    android:recreateOnConfigChanges="keyboard|keyboardHidden|navigation" />

更稳的做法是把这类配置变化当成普通状态更新处理。比如外接键盘、触控板、导航设备变化时,页面不要默认假设自己会走一遍 onCreate()

override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)
    updateInputMode(newConfig.keyboard, newConfig.navigation)
}

这个点很适合用 Android 17 模拟器配合外接键盘、鼠标、触控板测试。只点页面流程不够,要看设备输入形态变化后,快捷键、焦点和输入框状态有没有丢。

Continue On

Android 17 加了 Continue On,用来把任务从手机切到平板等设备上继续。用户在平板任务栏里看到最近在手机上打开的 App 建议,点击后可以打开 App,并进入上一次任务位置;也支持 app-to-web 的回退。

App 侧入口比较直接:在 Activity 里启用 handoff,然后提供要传递的数据。

class MyHandoffActivity : Activity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setHandoffEnabled(true, null)
    }

    override fun onHandoffActivityDataRequested(
        handoffRequestInfo: HandoffActivityDataRequestInfo
    ): HandoffActivityData {
        return createHandoffData()
    }
}

Continue On 要处理的是“继续”的粒度。阅读 App 可能是文章 ID 和滚动位置,电商 App 可能是商品详情或购物车,编辑类 App 则要处理草稿保存和账号一致性。

如果任务状态只存在内存里,跨设备就接不上。至少要有稳定的 deep link、服务端状态或本地可同步状态,否则 Continue On 只能打开 App 首页。

Compose 优先

Android 17 发布时,Android 开发文档里明确写到:新的 Android API、库、工具和开发者指南会面向 Jetpack Compose 构建。android.widget 里的 legacy View 组件,以及 Fragment、RecyclerView、ViewPager 这类基于 View 的 Jetpack 库进入维护模式,只保留关键 bug 修复,不再加新功能。

现有 View 项目还能继续维护,真实项目里 View 和 Compose 也会长期共存。实际影响是:新能力、新示例、新适配方案会优先出现在 Compose 生态里。

大屏适配就是一个明显例子。Material 3 Adaptive 里的 NavigationSuiteScaffold 可以根据窗口尺寸切换底部导航和导航栏;Navigation 3 里的 list-detail、supporting pane 也更适合平板和桌面布局。

项目里如果还在大量使用 XML,新增页面、设置页、二级页面、平板专用布局更适合放到 Compose。核心交易链路不需要一次性迁,但新页面继续用旧 View 栈,后面接大屏、桌面、bubble 和 PiP 时会更吃力。

Image

性能限制

Android 17 对性能也有几类变化。

App 内存限制已经单独值得写一篇。超过设备 RAM 对应限制时,系统可以终止进程;退出原因里会看到 ApplicationExitInfo.REASON_OTHER,description 里包含 MemoryLimiter:AnonSwap。这类问题不是普通 Java OOM,Crash 平台可能没有常规堆栈。

target 37 后,android.os.MessageQueue 换成 lock-free 实现,可以减少 missed frames、改善启动和繁忙队列性能。但如果项目通过反射访问 MessageQueue 私有字段或方法,就可能出问题。测试代码需要看 TestLooperManager 新增的 peekWhen() 和 poll(),不要继续依赖内部实现。

还有一个运行时变化更直接:target 37 的 App 不能再修改 static final 字段。反射修改会抛 IllegalAccessException,JNI 里用 SetStatic<Type>Field 修改会让应用崩溃。

val field = BuildConfig::class.java.getDeclaredField("DEBUG")
field.isAccessible = true
field.set(null, true) // Android 17 + target 37 会失败

这类写法通常出现在测试 hook、灰度开关、老的反射框架或三方 SDK 里。升级 target 前,先全文搜 setAccessible(true)getDeclaredFieldMessageQueueSetStatic 这类关键词,比等线上异常更靠谱。

隐私权限

Android 17 继续把“拿一类数据”改成“让用户选一次数据”。系统联系人选择器、Photo Picker 纵横比定制、系统渲染的位置按钮、EyeDropper API 都属于这个方向。

EyeDropper 的调用方式比较清楚。它用系统能力让用户从屏幕上取色,不需要 App 自己申请屏幕录制或媒体投影权限。

val eyeDropperLauncher =
    registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
        if (result.resultCode == Activity.RESULT_OK) {
            val color = result.data?.getIntExtra(Intent.EXTRA_COLOR, Color.BLACK)
            applyPickedColor(color)
        }
    }

fun launchColorPicker() {
    eyeDropperLauncher.launch(Intent(Intent.ACTION_OPEN_EYE_DROPPER))
}

本地网络访问也变了。target 37 的 App 访问局域网设备时,要么使用系统中介的设备选择器,要么申请新的 ACCESS_LOCAL_NETWORK 运行时权限。它归在 NEARBY_DEVICES 权限组里,用户已经授权过同组权限时,不一定会再次弹窗。

<uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK" />

智能家居、投屏、打印、局域网调试、设备发现这类功能,要重点检查发现设备、连接设备和权限拒绝后的 UI 状态。以前直接扫局域网的实现,target 37 后要重新验证。

SMS OTP 也有保护。标准短信验证码对大多数 target 37 App 会延迟 3 小时可见,相关广播会被保留,SMS provider 查询会被过滤。依赖读短信提取验证码的实现,要迁到 SMS Retriever 或 SMS User Consent。

Image

动态加载

Android 14 已经对 DEX / JAR 动态加载做了 safer dynamic code loading 限制。Android 17 把这类保护扩展到 native library。

target 37 后,如果通过 System.load() 加载 native 文件,这些文件必须标记为只读,否则系统会抛 UnsatisfiedLinkError

val soFile = File(filesDir, "plugin.so")

// 写入完成后,加载前要把文件改成只读
soFile.setReadOnly()
System.load(soFile.absolutePath)

插件化、热修复、算法包下发、动态 native 能力加载,都要查这条链路。只看 APK 自带的 lib/ 目录不够,运行时写入 app 私有目录再加载的 .so 才是风险点。

验证路径

Android 17 兼容性验证不要只跑一遍启动和登录。

先用 Android Studio 安装 Android 17 SDK 和 64-bit Emulator system image,再把现有线上包安装到 Android 17 设备或模拟器上跑一轮。确认没有运行时行为变化后,再切 targetSdk 37 跑同样流程。

重点验证这些路径:

大屏 / 折叠屏 / 桌面窗口:
方向、分屏、自由缩放、最小窗口、bubble、PiP

输入设备:
外接键盘、鼠标、触控板、焦点、快捷键

运行时:
MessageQueue 反射、static final 反射、native 动态加载

权限:
局域网设备发现、短信验证码、联系人、位置、取色

性能:
内存退出原因、R8 配置、LeakCanary、ProfilingManager

SDK、库、游戏引擎也要提前处理。下游 App 想升级 target 37 时,如果你的 SDK 还在反射 MessageQueue、读短信、动态加载 native 文件,最后会卡住别人。

Image

最后

Android 17 对开发者最直接的检查点,是 target SDK 37 后的行为变化。

大屏限制、Activity 重建、本地网络权限、SMS OTP、MessageQueuestatic final、native 动态加载,这些都能在代码里找到对应入口。先把这些路径跑通,再看是否接 Continue On、App Bubbles、桌面 PiP 和新的媒体能力。

#Android #Android17 #JetpackCompose #Android开发