升级 Android 目标版本到 31(S) 居然这么多坑
我之前从 29 升级到 30 那次改动已经非常大了。这次应该不会太多改动,没想到总归还是 too young too simple, sometimes naive
. 升级目标版本到 31 也不是那么简单。下面是谷歌官方提供的两个文章,分别详细列举了升级到版本的变更以及升级到 Android 12 的详细的变更:
谷歌官方已经给了详细的说明,这里我分享我在适配过程中遇到的问题和解决思路。
2、exported 属性
本次适配需要做的最明显的一个变更是修改 exported 属性。这个属性是之前就存在。我之前只在个别几个 Service 属性中使用了它。在 31 上开始要求开发者明确指定组件的 exported 属性。
对于没有声明 exported 属性的应用,在启动的过程中就会抛出如下异常,
对于 exported 属性,你可以查看谷歌官方文档的详细解释:
适配这个属性并不难,只需要在 manifest 中明确指定每个组件的 exported 属性即可。一般来说,遵循如下原则:如果组件中使用了 intent-filter 等属性,那么它大概率是需要对外暴露的,此时需要将 exported 属性直为 true,其他情况下置为 false 即可。。
对于引用的三方类库中的 xml 属性也可以通过覆写声明方式增加 exported 以兼容处理,
<activity android:name="com.squareup.leakcanary.internal.DisplayLeakActivity"
android:exported="false"/>
3、PendingIntent 的变动
这是一个隐藏的变动,非常坑又不像 exported 属性那样容易被察觉。这边变动主要是要求开发者指定在创建 PendingIntent 的时候传入的 flags 参数的可变性。
这可以通过在之前的 flags 基础上增加 FLAG_MUTABLE
和 FLAG_IMMUTABLE
两个属性来完成。比如,之前我的 flags 是,PendingIntent.FLAG_CANCEL_CURRENT
,当我想将其修改为不可变的时候,就可以使用如下方式进行修改:
val flags = PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
复制代码
它可能是需要改动最多的一个变动,根据我在项目中修改的情况来看,以下几个场景需要排查:
桌面小控件 Appwidget
通知 Notification
桌面快捷方式 Shortcut
同时从几个方向来检索项目中需要改动的地方:
直接检索 PendingIntent 的 flags 的调用,比如
FLAG_ONE_SHOT, FLAG_NO_CREATE, FLAG_CANCEL_CURRENT
和FLAG_UPDATE_CURRENT
等,建议查看源码之后进行检索PendingIntent 的静态方法工厂,比如
PendingIntent.getBroadcast()
、PendingIntent.getActivity()
等。因为,获取 PendingIntent 的时候需要指定 flags 参数。
那么,另一个问题来了,究竟什么时候该选择 FLAG_MUTABLE
,什么时候该选择 FLAG_IMMUTABLE
呢?
它的注释是这么说的,
Flag indicating that the created PendingIntent should be immutable. This means that the additional intent argument passed to the send methods to fill in unpopulated properties of this intent will be ignored. FLAG_IMMUTABLE only limits the ability to alter the semantics of the intent that is sent by send by the invoker of send. The creator of the PendingIntent can always update the PendingIntent itself via FLAG_UPDATE_CURRENT.
也就是说,FLAG_IMMUTABLE
的“不可变”指的是,当 PendingIntent 设置了 flags 为“不可变”之后,调用它的 send 方法时传入的 Intent 将会被忽略。
这里举一个具体的场景,比如在列表类的 Appwidget 里,我们会使用 PendingIntent 设置列表的某一项的点击事件。考虑到列表量比较大,为每一个列表条目都声明一个 PendingIntent 显然开销太大。所以,Android 的处理机制是,
val views = RemoteViews(context.packageName, R.layout.layout_appwidget_note_list)
val i = Intent(context, MainActivity::class.java)
i.action = ACTION_APPWIDGET_NOTE_CLICK
val pi = PendingIntent.getActivity(context, 0, i, FLAG_CANCEL_CURRENT_MUTABLE)
views.setPendingIntentTemplate(R.id.lv, pi)
如上所示,首先定一个一个 PendingIntent,并调用 RemoteViews 的 setPendingIntentTemplate
方法传入,作为一个模版。然后在 RemoteViewsFactory 的 getViewAt
方法中为每个列表项设置点击时的 Intent,
val row = RemoteViews(context.packageName, R.layout.item_appwidget_note)
val i = Intent().putExtras(extras)
row.setOnClickFillInIntent(R.id.root, i)
当用户触发了点击事件的时候,系统会在调用 PendingIntent 的 send 方法时将 Intent 传入并唤起组件。此时,如果我们将 PendingIntent 的 flags 设置为 FLAG_IMMUTABLE
,那么这里发送时传入的 Intent 参数将被忽略,因此可能导致虽然唤起了其他组件,但是参数丢失的情况。而对于那种,声明 PendingIntent 时就传入了 Intent 的时候,一般来说不需要设置为 FLAG_MUTABLE
的。
以上是 PendingIntent 的改动,刚好在我的项目里两种情况都有遇到,所以详细分析了一下。
4、构建项目 JDK 需要升级
当将项目的 targetSdkVersion
升级到了 31 之后,构建项目的时候可能会遇到如下异常,
当然你也可能不会遇到这个问题。那主要的原因是,你的 Android Studio 里 Gradle 构建时用到的版本已经是 Java 11 的了。可以通过 Preference->Build->Gradle 查看当前 Android Studio 中使用的 JDK 版本,
在 Gradle JDK
处修改构建时用的 JDK 版本即可。
以上是针对 Android Studio 构建时的情况。但当我们使用脚本或者命令行构建项目的时候需要用到的就不是 Android Studio 的 JDK 版本了。此时,可以通过 java --version
查看环境变量中配置的 JDK 版本。
我们不能直接修改环境变量中的 JDK 版本解决上述编译问题。因为毕竟除了开发,我们可能还有很多其他应用在使用 JDK 环境。此时,我们可以通过 Gradle 构建时的命令来指定构建时使用的 JDK。
gradlew -Dorg.gradle.java.home=你的 JDK 路径