开发笔记-版本适配
开发笔记主要是记录开发中遇到的一些问题和解决方案,以及引发的一些思考,不会太深入地记录问题,但是会尽可能广泛地记录涉及到的内容,方便之后整理归纳和查阅
No.1 startForegroundService 方法导致报错 RemoteServiceException 引起闪退
闪退机型:(均为 Android 9.0 系统)
- 小米 MI NOTE 3
- 华为
- P30 Pro (VOG AL00)
- P10 PLUS (VKY AL00 )
- NOVA 4 (VCE AL00)
- Mate10 (ALP AL00)
- Mate20 (LYA AL00)
- P30 (ELE AL00)
- 荣耀
- HONOR 9 (STF AL00)
- HONOR 10 (COL AL10)
- HONOR V10 (BKL AL20)
- HONOR Note 10 (RVL AL09)
- HONOR Play (COR AL00)
- 索尼 G8142
- 一加 pro 7
报错详细信息:
1 |
|
Android O 后台执行限制
Android 8.0 为提高电池续航时间而引入的变更之一是,当您的应用进入已缓存状态时,如果没有活动的组件,系统将解除应用具有的所有唤醒锁
此外,为提高设备性能,系统会限制未在前台运行的应用的某些行为。具体而言:
- 现在,在后台运行的应用对后台服务的访问受到限制
- 应用无法使用其清单注册大部分隐式广播(即,并非专门针对此应用的广播)
- 默认情况下,这些限制仅适用于针对 O 的应用,不过,用户可以从 Settings 屏幕为任意应用启用这些限制,即使应用并不是以 O 为目标平台
Android 8.0 还对特定函数做出了以下变更:
- 如果针对 Android 8.0 的应用尝试在不允许其创建后台服务的情况下使用 startService() 函数,则该函数将引发一个 IllegalStateException
- 新的 Context.startForegroundService() 函数将启动一个前台服务。现在,即使应用在后台运行,系统也允许其调用 Context.startForegroundService()。不过,应用必须在创建服务后的五秒内调用该服务的 startForeground() 函数,否则会导致 ANR
Android O 后台服务限制
在后台中运行的服务会消耗设备资源,这可能降低用户体验。 为了缓解这一问题,系统对这些服务施加了一些限制
系统可以区分 前台 和 后台 应用,(用于服务限制目的的后台定义与内存管理使用的定义不同;一个应用按照内存管理的定义可能处于后台,但按照能够启动服务的定义又处于前台)如果满足以下任意条件,应用将被视为处于前台:
- 具有可见 Activity(不管该 Activity 已启动还是已暂停)
- 具有前台服务
- 另一个前台应用已关联到该应用(不管是通过绑定到其中一个服务,还是通过使用其中一个内容提供程序),例如,如果另一个应用绑定到该应用的服务,那么该应用处于前台:
- IME
- 壁纸服务
- 通知侦听器
- 语音或文本服务
如果以上条件均不满足,应用将被视为处于后台。
绑定服务不受影响,这些规则不会对绑定服务产生任何影响,如果您的应用定义了绑定服务,则不管应用是否处于前台,其他组件都可以绑定到该服务
处于前台时,应用可以自由创建和运行前台服务与后台服务,进入后台时,在一个持续数分钟的时间窗内,应用仍可以创建和使用服务,在该时间窗结束后,应用将被视为处于 空闲 状态。 此时,系统将停止应用的后台服务,就像应用已经调用服务的 Service.stopSelf()
方法,在这些情况下,后台应用将被置于一个临时白名单中并持续数分钟。 位于白名单中时,应用可以无限制地启动服务,并且其后台服务也可以运行,处理对用户可见的任务时,应用将被置于白名单中,例如:
- 处理一条高优先级 Firebase 云消息传递 (FCM) 消息。
- 接收广播,例如短信/彩信消息。
- 从通知执行 PendingIntent。
在很多情况下,您的应用都可以使用 JobScheduler 作业替换后台服务,例如 CoolPhotoApp 需要检查用户是否已经从朋友那里收到共享的照片,即使该应用未在前台运行,在之前的版本中,应用使用一种会检查其云存储的后台服务。 为了迁移到 Android 8.0,开发者使用一个计划作业替换了这种后台服务,该作业将按一定周期启动,查询服务器,然后退出,在 Android 8.0 之前,创建前台服务的方式通常是先创建一个后台服务,然后将该服务推到前台,而 Android 8.0 有一项复杂功能;系统不允许后台应用创建后台服务,因此,Android 8.0 引入了一种全新的方法,即 Context.startForegroundService()
,以在前台启动新服务,在系统创建服务后,应用有五秒的时间来调用该服务的 startForeground()
方法以显示新服务的用户可见通知,如果应用在此时间限制内未调用 startForeground(),则系统将停止服务并声明此应用为 ANR
所以为了解决这个问题,应该:
- 将启动 Service 调用的 startService 改为 startForegroundService
- 调用了 startForegroundService 后需要在 Service 里继续调用 Service.startForeground(),有如下三个注意事项
- 在 Service 的 onCreate 方法和 onStartCommand 方法中都调用 startForeground,因为 onCreate 方法不一定会每一次都调用,主要是针对后台保活的服务,如果在服务A运行期间,保活机制又调用 startForegroundService 启动了一次服务A,那么这样不会调用服务A的 onCreate 方法,只会调用 onStartCommand 方法
- notification ID 必须不为 0,否则会报同样的错误
- 调用 stopSelf 必须要在调用 startForeground 之后
这样应该就能够解决了
相关文章:Android Service 生命周期、Android 通知渠道、Android Oreo 通知新特性
No.2 Android Q 反射失效导致K线图缩放异常
在处理一些手势缩放事件时,可以用如下做法:
1 |
|
只需要在 ScaleListener 中实现想要的逻辑就可以了
但是,在 中有一个属性 mMinSpan,在执行构造函数初始化时,会赋予一个从 viewConfiguration 对象获得的 mMinScalingSpan 值,又从 onTouchEvent() 方法中可以看到,当 span < mMinSpan 时,会调用 mListener.onScaleEnd(this),意思就是,当双指缩小到这个尺寸时,就不会再缩小了,所以造成了缩放不顺畅的效果,那么这个尺寸是多少呢,要怎么修改呢
1 |
|
跟踪进去看到,ViewConfiguration 类中的 get() 方法中,会用 ViewConfiguration 的私有构造函数创建一个 ViewConfiguration 对象
1 |
|
而 ViewConfiguration 的私有构造函数中,会取到一个系统中写死的值
1 |
|
这个 mm 是指屏幕的物理毫米尺寸,换算成 dp 大概是三四百像素左右,这么大,难怪卡顿了,必须要改掉
1 |
|
首先用了反射的方法,将这个值直接给他改成 50 像素
1 |
|
在 Android Q 之前的版本,运行完美,但是一但用户更新了 Android Q,就又会出现缩放不灵敏的感觉,就是说,我们的反射失效了
查阅文档,可以在 Android Q 中受限的灰名单中的非 SDK 接口列表中看到下面这一条:
1 |
|
也就是说,在 Android 9 中还是不建议使用的这个变量,现在直接受限了,使用反射是取不到改不了的了,其实不单只是这个变量,类里的其他变量都多多少少无法使用了,说明通过反射来修改一些私有变量在 Android Q 中其实并不是太可靠了,那没办法了,只能用最后一步了,直接复制整个类,然后改掉想要改掉的部分,然后替换掉原来的 ScaleGestureDetector
1 |
|
No.3 Android 14 适配 Intent 相关
最近把 HoshiCore 库进行了一下 Android 14 的适配,起因是我的另一个项目里,提升 targetSdk 到 14 后,所有页面跳转都失效了,查了一下资料,原来是 Intent 有一些安全性调整,有如下一大段描述(原文是这篇文章的 Security 子标题下的 Restrictions to implicit and pending intents 部分)
对于面向 Android 14 的应用,Android 通过以下方式限制应用向内部应用组件发送隐式 intent:隐式 intent 仅传递给导出的组件,应用必须使用明确的 intent 来交付给未导出的组件,或者将组件标记为已导出(exported)。
如果应用创建一个 mutable pending intent ,但 intent 未指定组件或包,系统现在会抛出异常。
这些更改可防止恶意应用拦截只供给用内部组件使用的隐式 intent,例如:
1
2
3
4
5
6
7
8
<activity
android:name=".AppActivity"
android:exported="false">
<intent-filter>
<action android:name="com.example.action.APP_ACTION" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>如果应用尝试使用隐式 intent 启动该 activity,则会抛出异常:
1
2
// Throws an exception when targeting Android 14.
context.startActivity(Intent("com.example.action.APP_ACTION"))要启动未导出的 Activity,应用应
改用显式 Intent给 Intent 对象设置 package
1
2
3
4
5
6
// This makes the intent explicit.
val explicitIntent = Intent("com.example.action.APP_ACTION")
explicitIntent.apply {
package = context.packageName
}
context.startActivity(explicitIntent)
上述描述中,原版的描述是 “改用显式 Intent”,而我认为应该是 “给 Intent 对象设置 package” 即可,在一开始我们的认知里,显式 Intent 指的是明确写出了 Activity 的 Intent,应该也可以说是用了以下这个构造函数的情形:
1 |
|
而隐式 Intent 指不像显式那样直接指定需要调用的 Activity 的一种 Intent,它是设置 Action、Data、Category 等参数,让系统来筛选出合适的 Activity 的一种 Intent,筛选是根据 <intent-filter>
来进行的
官方的例子里很明显就是调用了以下这种构造函数,是用了 Action 的,应该是属于隐式声明的
1 |
|
所以我认为并没有改成了显式声明,只是在原来的隐式声明上作了一些调整,也可能是我理解出了偏差,官方可能把这种指定了 package 的 Intent 认为是显式的,因为它指定的东西足够多了
当然,咬文嚼字多说无益,解决问题最重要,这里记录一些思考,旨在避免这类随意的文档描述混淆视听,搞乱了原来的理解,如果以后有新的认识,可以再作记录
No.4 提升 minSdkVersion 后 APK 变大
公司项目引入了一个第三方的远程预览库,最近需要提升版本,版本提升后发现需要同步提升 minSdkVersion,于是又把 minSdkVersion 提升到 23,结果打包后发现 APK 增大了 70M。把新旧 APK 都拉到 AS 里面一看,发现 lib 文件夹下的各个 .so 库的体积都翻了一倍
网上搜索了一下相关症状,发现是因为 minSdkVersio 23 以下时,android:extractNativeLibs 默认为 true,而 minSdkVersion 大于等于 23 时,android:extractNativeLibs 默认为 false,下面是 google 对这个属性的一些解析
此属性指示软件包安装程序是否将原生库从 APK 提取到文件系统。如果设置为 “false”,则原生库以未压缩的形式存储在 APK 中。虽然您的 APK 可能较大,但应用加载速度更快,因为库是在应用运行时直接从 APK 加载。
extractNativeLibs 的默认值取决于 minSdkVersion 和您使用的 AGP 版本。在大多数情况下,默认行为很可能符合您的预期,您无需显式设置此属性。从 AGP 4.2.0 开始,DSL 选项 useLegacyPackaging 取代了 extractNativeLibs 清单属性。请使用应用的 build.gradle 文件中的 useLegacyPackaging(而非清单文件中的 extractNativeLibs)来配置原生库压缩行为。如需了解详情,请参阅版本说明使用 DSL 打包压缩的原生库。
所以,可以手动设置一下 android:extractNativeLibs 或者 useLegacyPackaging,即可避免提升 minSdkVersion 后 APK 包体积变大的问题