Android-Service
Android-Service
1. Service的状态和种类
Service 分为 Started
和 Bound
两种状态(可共存),以及 Foreground
和 Background
两种类型。
1.1 Service的启动和绑定状态
Service 可以通过 start
和 bind
两种方法启动。
startService()
后该 Service 即变为Stared
状态,处于后台运行中,与启动它的 Activity 生命周期无关,除非手动stopService(Intent)
或 Service 内部调用自己的stopSelf()
终止,或被 Android 系统回收。bindService()
后该 Service 为Bound
状态,主要提供 C / S 接口,允许组件与 Service 通信或跨进程通信,生命周期与启动它的宿主绑定,宿主销毁时会自动解除与 Service 的绑定,当Bound
状态的 Service 没有被任何宿主绑定时,就会销毁该 Service。
Service 可以同时处于 Started
状态和 Bound
状态,也即 Service 通过一种方式启动后,仍然可以被其他宿主通过另一种方式连接(但不会重新启动)。当 Service 同时处于 Started
和 Bound
状态时,Service 的生命周期同时受到两种状态的影响,仅当两种状态都断开时,Service 才会销毁,也即:必须被 stopService()
并且被 unbindService()
后,Service 才会销毁。
1.2 Service的前台和后台
Service 可以在后台执行长时间而无界面的操作,且默认处于主进程的主线程中。
BackGround Service:用户不可感知的后台操作,如监控、轮询拉取等。
- 默认情况下,不论通过
start
还是bind
启动的 Service 都是后台 Service。后台 Service 对用户不可感知,但内存优先级较低,在内存不足时会被系统杀死(根据 Service 的参数可以指定是否需要在内存空闲后重新启动),最大允许 200 秒(ActiveServices#SERVICE_BACKGROUND_TIMEOUT
)卡顿时间,超出则会抛出 ANR。
- 默认情况下,不论通过
Foreground Service:执行一些用户可感知的操作,如 Audio 播放。
可以通过调用
Service#startForeground(int id, Notification notification)
将一个 Service 设为前台 Service,其中 Notification 不能为null
否则会抛出异常,因此 ForegroundService 强制要求在通知栏显示一个服务图标,但内存优先级高,即使内存不足也不会被系统杀死,最大允许 20 秒(ActiveServices#SERVICE_TIMEOUT
)卡顿时间,超出则会抛出 ANR。在 API 26 (Android 8.0) 以上可以直接通过
startForegroundService()
启动一个 ForegroundService,但在 ForegroundService 启动后需要在 10 秒(ActiveServices#SERVICE_START_FOREGROUND_TIMEOUT
)内调用startForeground()
,否则会抛出 ANR。
2. Service生命周期
所有 Service 都必须在 Manifest 内声明注册:
1 | <service |
exported
: 是否能被其他 App 隐式启动(通过指定 Intent 的 ActionName 启动),当没有指定Intent-Filter
时默认值为false
,指定了Intent-Filter
时默认值为true
。process
: 指定进程,默认情况下 Service 运行在主进程的主线程中,可以通过指定process
属性分配独立进程。isolatedProcess
: 是否运行在一个 没有任何权限 的特殊进程中,通常用于一些沙盒任务。enabled
: 是否启用这个 Service (系统是否能实例化这个 Service),默认为true
,当设置为false
时,相当于在 Manifest 中移除该 Service 的标签。- 关于
enabled
属性有个很常见的问题就是:如果想要不启用这个 Service,为何要在 Manifest 中声明又禁用呢?直接不写这个 Service 或者不在 Manifest 中声明不就行了吗?实际上这个属性更多用在外部依赖上,例如项目依赖了一个 Module,但是又想禁用 Module 中的某个 Service,则可以在项目 Manifest 中禁用该 Service,然后 Manifest 合并后就会禁用该 Service,理论上四大组件都可以通过这个方式禁用。
- 关于
2.1 Start方式启动Service
以 start 方式启动 Service:
1 | Intent intent = new Intent(context, DemoService.class); |
需要注意:
Service#stopSelf()
和Service#stopSelfResult(int startId)
的效果等同于Context#stopService(Intent)
。- 仅被 Start 启动而没有被任何 Client Bind 的后台 Service,将在 App 进入后台 1 分钟后被 Kill。
2.2 Bind方式启动Service
如果通过 bind 方式绑定 Service,在 Service 中返回 IBinder 类的实例,则 Client 就能通过 Binder 的方式与 Service 通信。
虽然默认情况下 Service 运行在主进程的主线程当中,但 Service 本身是支持跨进程的,所以 Android 选择了 Binder 作为通信方式。
2.3 Service生命周期
Service 中常见的生命周期回调如下:
1 | public class DemoService extends Service { |
同时处于 Started
和 Bound
状态下的 Service,在被 Stop 并且 onUnbind()
后才会销毁。需要注意的是,Service 的 onUnbind()
有两种触发方式,只要满足其中任意一条就会触发 onUnbind()
回调:
所有
Context.BIND_AUTO_CREATE
模式 Bind 的 Client 均已 Unbind。此时其他模式 Bind 的 Client 会收到
ServiceConnection#onServiceDisconnected()
回调,视为其他 Client 异常断开。所有 Client 都不是
Context.BIND_AUTO_CREATE
模式,且都已 Unbind。此时所有 Client 都不会收到
ServiceConnection#onServiceDisconnected()
回调,视为正常断开。
一个 Service 不论是否已经启动,都可以被多次 Start、Stop 和 Bind,Start 和 Stop 的次数不必须匹配,但 Unbind 的次数必须匹配 Bind 的次数,如果 Client 与 Service 不存在绑定关系,则调用 Unbind 会抛出异常。
3. Client与Service通信
如果 Client 需要与 Service 通信,则必须通过 bind
方式与目标 Service 绑定连接。
3.1 通过Binder持有Service
Client 直接持有 Service 实例对象通信,原理是:
- 利用 Binder 跨进程通信的方式,直接在 Client 中拿到 Service 的 Binder 对象。
- 然后从 ServiceBinder 中获取 Service 实例对象,相当于把 Service 的实例对象跨进程传到了 Client 进程中。
- Client 持有了 Service 实例对象,即可直接操作 Service。
1 | /** |
3.2 通过Messenger发送消息
Client 和 Service 通过互相持有对方的 Messenger 通信,原理是:
- Client 和 Service 各自通过 Handler 构造各自的 Messenger,则各自的 Messenger 都可以通过 Handler 接收消息。
- Service 在
onBind()
中返回serviceMessenger.getBinder()
,相当于返回 Service 进程的 Binder。 - 然后 Client 在成功连接 Service 后,通过 Service 返回的 Binder 构造并持有 Service 的 Messenger,也就相当于能向 Service 的 Handler 发送消息。
- 此时 Service 还不能向 Client 发送消息,所以 Client 需要首先向 Service 发送一条消息,在消息中指定
replyTo
为 Client 自己的 messenger。 - 则 Service 收到这条消息后,就能通过消息的
replyTo
取出并持有 Client 的 Messenger,也就相当于能向 Client 的 Handler 发送消息。 - 此时 Client 和 Service 都互相持有了对方的 Messenger,就可以互相发送消息。
1 | /** |
3.3 其他方式
除了使用 Binder 方式以外,Activity 与 Service 之间的通信还能通过 BroadCastReceiver 以及 EventBus 的方式实现。
4. 高版本Service的限制
从 API 21 (Android 5.0) 开始,Google 全面禁止了隐式启动 Service,仅允许显式启动 Service,不论目标 Service 是否处于同一个 App 内,都需要指定目标 Service 所在 App 的包名以及该 Service 的全路径。隐式启动 Service 将会抛出异常,该限制主要是为了避免 App 恶意唤起后台服务:
1 | Service Intent must be explicit |
隐式启动 Service 是指仅通过 ActionName 启动 Service,即使可能存在多个 Service 的 ActionName 重复。API 21 之后,必须使用显式启动,否则会抛出异常,
从 API 26 (Android 8) 开始,Google 针对资源控制限制了后台服务,处于后台的 App 将不允许后台 Service 再直接通过 startService()
方式启动另一个后台 Service。此前 Service 需要启动后再调用 Service#startForeground()
才能将 Service 设为前台服务,而不论通过 Start 还是 Bind 启动的 Service 默认都作为后台服务,因此 API 26 提供了一个新的 Api startForgroundService()
来直接启动一个默认前台服务。
- 由于指定了启动的是前台服务,因此其会在内部调用
ActiveServices.setServiceForegroundInnerLocked()
Post 了一个延时消息,必须在 10s 内调用Service#startForeground()
,其内部 Remove 了该延时消息,否则该延时消息就会抛出 ANR。 - 并且如果
Service#startForeground(int id, Notification notification)
中id == 0
或notification == null
都会抛出Null Notification
异常,因此启动前台服务一定会创建一个通知提醒用户。
后台 Service 不能直接 Start 另一个后台 Service,这条规则受到 App 前后台策略的影响:
- 处于前台的 App 可以自由启动前台或后台 Service。
- 进入后台的 App 在一段时间窗内(数分钟)仍然可以自由启动创建前台或后台 Service,在时间窗结束后系统将会停止 App 的后台 Service。
但 Android 对 App 是否前台的判断,并非是简单地判断是否存在前台 Activity。Google 文档中指出,一个 App 如果满足以下几点,则被视为处于前台:
- 具有可见 Activity
- 具有前台 Service
- 被另一个前台 App 关联(被另一个前台应用 Bind Service 或使用了 ContentProvider),例如:
- IME
- 壁纸 Service
- 通知监听
- 语音或文本服务
4.1 Service保活
随着高版本 Android 对后台策略逐步收紧,理论上已经没有 100% 保活的方式,API 26 以上 Service 保活的思路主要就是两个 Service 互相启动并绑定;
- AService:
- 首先由某个前台 Activity Start 一个 AService。
- AService 的
onStartCommand()
返回START_STICKY
。 - AService Start 和 Bind BService。
- AService 在
onDestroy()
中再次 Start 和 Bind BService。
- BService:
- BService 的
onStartCommand()
同样返回START_STICKY
。 - BService 被启动后,Start 和 Bind AService。
- BService 在
onDestroy()
中再次 Start 和 Bind AService。
- BService 的
AService 和 BService 都需要在
onStartCommand()
中返回START_STICKY
,这样其中一个 Service 由于内存被系统杀死后,仍有机会被重启并继续拉活对方。由于后台 Service 如果仅被 Start 而没有被 Bind,会在 App 置入后台 1 分钟后被 Kill,所以 Service 互相拉活时,需要同时 Start 和 Bind 对方。
由于高版本禁止从后台启动任何后台服务,所以在 AService 和 BService 互相 Start 时需要判断版本,API 26 以上需要
startForegroundService()
。
4.2 几乎不可见的Activity
主要思路是在 Activity#onCreate()
中将 Activity 设置为只有一个像素大小的透明的背景。
1 |
|