开发笔记-版本适配
开发笔记主要是记录开发中遇到的一些问题和解决方案,以及引发的一些思考,不会太深入地记录问题,但是会尽可能广泛地记录涉及到的内容,方便之后整理归纳和查阅
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 认为是显式的,因为它指定的东西足够多了
当然,咬文嚼字多说无益,解决问题最重要,这里记录一些思考,旨在避免这类随意的文档描述混淆视听,搞乱了原来的理解,如果以后有新的认识,可以再作记录