记一次 Android 15 适配
公司有个项目一直没有维护,Google Play 上收到用户投诉,提示目标版本过低,无法安装,于是需要进行一次适配,这里详尽记录一下经过
很多年前写过一篇 Android 9、10 的适配记录,但是过于凌乱,那时候基础也差,基本是搜到什么就粘贴进项目里,能跑起来就完事了(其实现在也差不多)。后面有一些开发笔记用于记录适配相关的事项,但是好像又比较零散,想着还是以记录完整经过的模式来写,写时候比较舒服,读起来也更通顺
项目背景
1 |
|
提升相关版本
最基础的一步是提升 targetSdkVersion,然后直接运行,发现报错提示还需要提升 AGP 版本,这里提升到 8.7.3
这个 AGP 版本指的是 Android Gradle 插件版本(Android Gradle Plugin),该插件版本适用于在相应 Android Studio 项目中构建的所有模块,可以在以下几个地方指定:
- Android Studio 的 File > Project Structure > Project 菜单中指定插件版本
- 在顶级 build.gradle.kts 文件中指定
1
2
3plugins {
id("com.android.application") version "8.7.3" apply false
} - 使用 build.gradle 的话,指的是 classpath ‘com.android.tools.build:gradle:8.7.3’ 这个
1
2
3
4
5buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:8.7.3'
}
}
提升 AGP 版本后,对应的,Gradle 版本要提升到 8.9,JDK 需要升级至 17 以上
后续因为 reverse() 方法的问题,提升了 Kotlin 版本到 1.9.22,又因为在 Kotlin 版本大于 1.7 时,Room 库 2.4.3 以下版本下无法识别挂起函数,编译报错,再把 Room 库提升到最新版本 2.6.1 后,又报如下问题
1 |
|
意思是这几个注解下不能用可空的类型,需要改成非空类型,好办,改完再运行下,无问题了
参考文章
模块级别的 build.gradle 改动
这里的 build.gradle 指的是除了顶级 build.gradle 之外的 build.gradle,需要做以下改动
namespace
每个模块下的 build.gradle 都需要声明一个 namespace(下列代码举例子为了方便所以写到一起了,事实是分开的),新建项目会自带
1 |
|
这个东东是干嘛的呢?为什么另一个项目不需要?待研究后补充,现在已知的是新增了这个之后,会导致你以前的 R 文件引用产生变化
以前假设你在 test 模块下写代码,test 模块引用 common 模块,你只 import com.hoshi.test.R
,就既可以使用 test 模块的资源,又可以使用 common 模块的资源
但是现在不一样了,你 import com.hoshi.test.R
只能使用 test 模块的资源,如果你想用 common 的资源,你就需要在资源文件前加上完整包名,如 context.getString(com.hoshi.common.R.string.common_today)
增加了 namespace 后,AndroidManifest 里面的 package="com.hoshi.test"
就可以删掉了
zipAlignEnabled
zipAlignEnabled 废弃,不再需要声明,删掉代码即可,直接是自动默认为开的
buildConfig
新版本 AGP,默认关闭 buildConfig,会导致你的 BuildConfig.xxxxx 全部出现异常,需要作如下配置
1 |
|
其他写法调整
1 |
|
这种调整经常会有,如果遇到可以往类似方向调整,或者直接搜一下如何解决
存储权限适配
targetSdkVersion 大于等于 33,并且运行在 Android 13 的手机上时,通过代码判断是否拥有 READ_EXTERNAL_STORAGE 权限,一定返回 false
XXPermissions 库不再允许申请 READ_EXTERNAL_STORAGE 权限(会直接闪退并告知原因是申请 READ_EXTERNAL_STORAGE),而 ActivityResultLauncher 库中,取图片、视频的 PickContentLauncher 会判断是否拥有 READ_EXTERNAL_STORAGE 权限,所以需要做一下适配
首先彻底移除自己项目中所有的 READ_EXTERNAL_STORAGE,使用 READ_MEDIA_IMAGES、READ_MEDIA_VIDEO、READ_MEDIA_AUDIO 替代,然后把 PickContentLauncher 单独取出来,进行如下修改
1 |
|
最后找一下用到原 PickContentLauncher 类的地方,将其替换成自己魔改过的 PickContentLauncher 类
MQTT 相关调整
因为以下众多原因,需要对 MQTT 库进行一些魔改
- SCHEDULE_EXACT_ALARM 权限需要和 Google 官方说明申请原因,还要拍摄录像告知使用场景,最好移除掉
- 前台 service 需要声明 android:foregroundServiceType
- 动态广播接收器必须指定导出的行为,也就是 registerReceiver 方法要加一个 flag 说明是 RECEIVER_EXPORTED 还是 RECEIVER_NOT_EXPORTED
Github 上有挺多方案,甚至还有 Kotlin 版本的,现在追求改动最小,所以沿用了这个方案。这个方案将之前官方 MQTT 用到的 AlarmManager 直接用 Worker 替换掉,且也做了 2、3 两点的适配,十分符合此次适配的需求
隐式 Intent 和 PendingIntent 的限制
官方的说法是,要启动非导出活动,应用程序应使用显式意图,具体可以直接看我之前写过的文章中的某段 —— Android 14 适配 Intent 相关
这个不难,如果自己写有统一的跳转方法的话,直接给 intent 加上 packageName 即可,这里需要特别注意的就是,第三方库里面的跳转如果没有加上 packageName 就会导致闪退,我这里又是 ActivityResultLauncher 库里面的跳转导致的,只能说开源不易,不能怪人,自己发现后及时修改即可
Edge to Edge 适配,沉浸式的新方案
之前一直用 ImmersionBar 库来做沉浸式,提升到 Android 15 后,发现在某些页面中,状态栏和应用顶部的控件又重叠了,好家伙全都白搞了,又要一轮全新的适配。虽然官方告知可以在 theme 中声明 <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
来暂时规避,但是 Android 16 明确说了会废弃并停用这个选项,躲得了初一躲不过十五,还是乖乖适配好了
首先把 ImmersiveBar 相关的解决布局与状态栏重叠问题的代码(比如 ImmersionBar.fitsSystemWindows()
这些)移除掉,避免重复处理,然后写相关的 View 扩展代码如下
1 |
|
在状态栏和控件重叠的页面,让重叠的控件调用上面代码即可
参考文章
_Collection.reversed() 扩展方法闪退问题
报错如下
1 |
|
这里报错的是集合类的扩展方法 reversed(),以前用得好好的,突然就不行了,感觉可能也和 APG 版本、JDK 版本有关。我这里直接替换为 asReversed() 即可,下面说下它们之间有什么不同
简单概括就是:两个类都会返回一个 List,改变 list.reversed() 返回的 List 中的元素,不会影响到原 List,而改变 list.asReversed() 返回的 List 中的元素,会影响到原 List
后来想到还是要看看为什么突然就不行了,留意到我的 Kotlin 版本是 1.6.21,看了下扩展方法里面的具体实现,处于位置 kotlin-stdlib-common-1.6.21-sources.jar!\generated_Collections.kt,代码如下
1 |
|
这里的这个 expect 关键字是用于跨平台的,这里用 expect 声明了方法之后,其他平台再使用对应的 actual 关键字实现对应的方法,每个目标平台(如 JVM、JS、Native 等)都可以有自己的 actual 实现
那么可以猜测,应该是提升了 JDK 版本为 17 后,对应的 Native 实现没有实现到这个方法了,所以导致报错,于是升级 Kotlin 版本试试,发现升级后,reverse 扩展方法位置变了,在 _CollectionsJVM.kt 里面,且代码变为如下
1 |
|
看起来是个 actual 方法,有实现了,应该可行了,编译运行,执行相关的方法,没有报错了
参考文章
R8 混淆相关
上面的步骤都处理完了,提交代码,进行远程打包,直接报错
1 |
|
打开提示中的对应目录,有如下内容,说明我们缺少了这些,需要加上。这里的 -dontwarn 是关闭警告的意思,为什么需要加上这些,具体原理不明,估计是 JDK 版本提升后,R8 的一些相关配置改变了
1 |
|
复制粘贴到 proguard-rules.pro 中,再上传打包,这次没问题了,打包 apk 成功,但是安装好后一运行就闪退了,好好好,再看闪退日志
1 |
|
搜索资料得知,如果使用了 R8 混淆,JDK 17 默认会开启 R8 fullMode 混淆模式,完整模式下,对于没有 keep 的类,将会擦除泛型信息。而我的项目中 ViewBinding 是用反射的方式在基类中处理的,并且使用了类似于 BaseResponse<T> 的类,有自定义解析器,所以精准踩中了这个雷。一个个去手动配置混淆豁免规则过于麻烦,所以选择直接手动关闭完整模式,在 gradle.properties 文件中添加以下代码即可
1 |
|
参考文章
总结
至此,这次的 Android 15 告一段落了。不得不说 Android 的适配每次都能给你来点新花样,每次有新版本时,及时对 APP 进行适配,能够减轻将来再有新版本时适配的工作量,同时也能更快利用上一些新版本 Bug 特性,还是值得重视的