云社区 博客 博客详情

详解Android四大组件之——服务

misssuk 发表于 2019-11-30 20:42:28 2019-11-30
0
0

【摘要】 关于服务定义Service是一种可在后台执行长时间运行操作而不提供界面的应用组件。服务可由其他应用组件启动,而且即使用户切换到其他应用,服务仍将在后台继续运行。此外,组件可通过绑定到服务与之进行交互,甚至是执行进程间通信(IPC)。例如,服务可在后台处理网络事务、播放音乐,执行文件I/O或与内容提供程序进行交互。类型前台服务执行一些用户能注意到的操作。例如,音频应用会使用前台服...

关于服务

定义

Service 是一种可在后台执行长时间运行操作而不提供界面的应用组件。服务可由其他应用组件启动,而且即使用户切换到其他应用,服务仍将在后台继续运行。此外,组件可通过绑定到服务与之进行交互,甚至是执行进程间通信 (IPC)。例如,服务可在后台处理网络事务、播放音乐,执行文件 I/O 或与内容提供程序进行交互。

类型

  • 前台服务

执行一些用户能注意到的操作。例如,音频应用会使用前台服务来播放音频曲目。前台服务必须显示通知。即使用户停止与应用的交互,前台服务仍会继续运行。

  • 后台服务

后台服务执行用户不会直接注意到的操作。例如,如果应用使用某个服务来压缩其存储空间,则此服务通常是后台服务。

  • 绑定服务

当应用组件通过调用 bindService() 绑定到服务时,服务即处于绑定状态。绑定服务会提供客户端-服务器接口,以便组件与服务进行交互、发送请求、接收结果,甚至是利用进程间通信 (IPC) 跨进程执行这些操作。仅当与另一个应用组件绑定时,绑定服务才会运行。多个组件可同时绑定到该服务,但全部取消绑定后,该服务即会被销毁。

虽然分开概括讨论启动服务和绑定服务,但服务可同时以这两种方式运行,它既可以是启动服务(以无限期运行),亦支持绑定。唯一的问题在于实现一组回调方法:onStartCommand()(让组件启动服务)和 onBind()(实现服务绑定)。

如何创建服务

如要创建服务,必须创建 Service 的子类(或使用它的一个现有子类)。在实现中,必须重写一些回调方法,从而处理服务生命周期的某些关键方面,并提供一种机制将组件绑定到服务。

主要的回调方法:

  • onStartCommand()

当另一个组件(如 Activity)请求启动服务时,系统会通过调用 startService() 来调用此方法。执行此方法时,服务即会启动并可在后台无限期运行。如果实现此方法,则在服务工作完成后,需通过调用 stopSelf()stopService() 来停止服务。(如果只想提供绑定,则无需实现此方法。)

  • onBind

当另一个组件想要与服务绑定(例如执行 RPC)时,系统会通过调用 bindService() 来调用此方法。在此方法的实现中,必须通过返回 IBinder 提供一个接口,以供客户端用来与服务进行通信。务必实现此方法;但是,如果并不希望被绑定,则应返回 null

  • onCreate()

首次创建服务时,系统会(在调用 onStartCommand() 或 onBind() 之前)调用此方法来执行一次性设置程序。如果服务已在运行,则不会调用此方法

  • onDestroy()

当不再使用服务且准备将其销毁时,系统会调用此方法。服务应通过实现此方法来清理任何资源,如线程、注册的侦听器、接收器等。这是服务接收的最后一个调用。

除了继承service实现上诉的几个重要回调之外,还需要在

清单文件中注册service:


  ...
  
  
  ...
  

关于service标签,可以操作的属性有:

 元素拥有自己的 enabled 属性,该属性适用于所有应用组件,包括服务
 // 属性都为“true”(因为它们都默认使用该值)时,系统才能启用服务
 android:enabled=["true" | "false"]
 //其他应用的组件是否能调用服务或与之交互,默认值取决于服务是否包含 Intent 过滤器,没有任何过滤器则为false
 //当该值为“false”时,只有同一个应用或具有相同用户 ID 的应用的组件可以启动服务或绑定到服务
 android:exported=["true" | "false"]
 //阐明服务是满足特定用例要求的前台服务
 android:foregroundServiceType=["connectedDevice" | "dataSync" |
"location" | "mediaPlayback" | "mediaProjection" |
"phoneCall"]
 //表示服务的图标,如果未设置该属性,则转而使用为应用整体指定的图标
 android:icon="drawable resource"
 //如果设置为 true,则此服务将在与系统其余部分隔离的特殊进程下运行。此服务自身没有权限,只能通过 Service API 与其进行通信(绑定和启动)
 android:isolatedProcess=["true" | "false"]
 //可向用户显示的服务名称,如果未设置该属性,则转而使用为应用整体设置的标签
 android:label="string resource"
 //实现服务的 Service 子类的名称
 android:name="string"
 //实体启动服务或绑定到服务所必需的权限的名称如果 startService()、bindService() 或 stopService() 的调用者尚未获得此权限,该方法将不起作用,且系统不会将 Intent 对象传送给服务
 //如果未设置该属性,则对服务应用由  元素的 permission 属性所设置的权限。如果二者均未设置,则服务不受权限保护
 android:permission="string"
 //将运行服务的进程的名称
 android:process="string" 
 >
. . .

service的活动周期:

  • 通过调用 startService() 启动服务

这会引起对 onStartCommand() 的调用,则服务会一直运行,直到其使用 stopSelf() 自行停止运行,或由其他组件通过调用 stopService() 将其停止为止

  • 通过调用 bindService() 来创建服务,且未调用 onStartCommand()

服务只会在该组件与其绑定时运行。当该服务与其所有组件取消绑定后,系统便会将其销毁

关于系统可能停止service的情况:

  • 只有在内存过低且必须回收系统资源以供拥有用户焦点的 Activity 使用时,Android 系统才会停止服务
  • 如果将服务绑定到拥有用户焦点的 Activity,则它其不太可能会终止
  • 如果将服务声明为在前台运行,则其几乎永远不会终止
  • 如果服务已启动并长时间运行,则系统逐渐降低其在后台任务列表中的位置,而服务被终止的概率也会大幅提升,如果服务是启动服务,则必须将其设计为能够妥善处理系统执行的重启
  • 如果系统终止服务,则其会在资源可用时立即重启服务,但这取决于从 onStartCommand() 返回的值

onStartCommand()方法的可返回值:

  • START_ NOT_STICKY

如果系统在 onStartCommand() 返回后终止服务,则除非有待传递的挂起 Intent,否则系统不会重建服务。这是最安全的选项,可以避免在不必要时以及应用能够轻松重启所有未完成的作业时运行服务。

  • START_STICKY

如果系统在 onStartCommand() 返回后终止服务,则其会重建服务并调用 onStartCommand(),但不会重新传递最后一个 Intent。相反,除非有挂起 Intent 要启动服务,否则系统会调用包含空 Intent 的 onStartCommand()。在此情况下,系统会传递这些 Intent。此常量适用于不执行命令、但无限期运行并等待作业的媒体播放器(或类似服务)。

  • START_ REDELIVER_INTENT

如果系统在 onStartCommand() 返回后终止服务,则其会重建服务,并通过传递给服务的最后一个 Intent 调用 onStartCommand()。所有挂起 Intent 均依次传递。此常量适用于主动执行应立即恢复的作业(例如下载文件)的服务。

上面已经提到,创建服务除了继承Service之外,还可以直接使用它的一个现有子类,那么这个之类是谁呢?就是下面这位了...

IntentService

关于两者,他们适应不同的应用场景来实现服务:

  • Service

这是适用于所有服务的基类。扩展此类时,必须创建用于执行所有服务工作的新线程,因为服务默认使用应用的主线程,这会降低应用正在运行的任何 Activity 的性能。

  • IntentService

这是 Service 的子类,其使用工作线程逐一处理所有启动请求。如果不要求服务同时处理多个请求,此类为最佳选择。实现 onHandleIntent(),该方法会接收每个启动请求的 Intent,以便执行后台工作。

可提供的特性:

  • 创建默认的工作线程,用于在应用的主线程外执行传递给 onStartCommand() 的所有 Intent
  • 创建工作队列,用于将 Intent 逐一传递给 onHandleIntent() 实现
  • 在处理完所有启动请求后停止服务,因此永远不必调用 stopSelf()
  • 提供 onBind() 的默认实现(返回 null)
  • 提供 onStartCommand() 的默认实现,可将 Intent 依次发送到工作队列和 onHandleIntent() 实现

扩展实现IntentService需要注意的点:

  • onStartCommand() 必须返回默认实现,从源码可见缘由:
@Override
public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
    onStart(intent, startId);
    return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
}

@Override
public void onStart(@Nullable Intent intent, int startId) {
    Message msg = mServiceHandler.obtainMessage();
    msg.arg1 = startId;
    msg.obj = intent;
    mServiceHandler.sendMessage(msg);
}

  • 实现onHandleIntent()

至此,我们已经知道如何创建服务,那么创建好的服务又应如何进行开启、停止和绑定呢?这就是接下来我们要解决的问题...

服务的开启、停止、绑定

服务的开启

可以通过将 Intent 传递给 startService()startForegroundService(),从 Activity 或其他应用组件启动服务。Android 系统会调用服务的 onStartCommand() 方法,并向其传递 Intent,从而指定要启动的服务。

至API 26开始,系统对开启服务有了严格的限制:

如果应用面向 API 级别 26 或更高版本,除非应用本身在前台运行,否则系统不会对使用或创建后台服务施加限制。如果应用需要创建前台服务,则其应调用 startForegroundService()。此方法会创建后台服务,但它会向系统发出信号,表明服务会将自行提升至前台。创建服务后,该服务必须在五秒内调用自己的 startForeground() 方法。

服务的停止

除非必须回收内存资源,否则系统不会停止或销毁服务,并且服务在 onStartCommand() 返回后仍会继续运行。服务必须通过调用 stopSelf() 自行停止运行,或由另一个组件通过调用 stopService() 来停止它。

一旦请求使用 stopSelf() 或 stopService() 来停止服务,系统便会尽快销毁服务。

如果服务同时处理多个对 onStartCommand() 的请求,则不应在处理完一个启动请求之后停止服务,因为可能已收到新的启动请求(在第一个请求结束时停止服务会终止第二个请求)。为避免此问题,可以使用 stopSelf(int) 确保服务停止请求始终基于最近的启动请求。换言之,在调用 stopSelf(int) 时,您需传递与停止请求 ID 相对应的启动请求 ID(传递给 onStartCommand() 的 startId)。此外,如果服务在能够调用 stopSelf(int) 之前收到新启动请求,则 ID 不匹配,服务也不会停止。

服务的绑定

绑定服务允许应用组件通过调用 bindService() 与其绑定,从而创建长期连接。此服务通常不允许组件通过调用 startService() 来启动它。

如要创建绑定服务,您需通过实现 onBind() 回调方法返回 IBinder,从而定义与服务进行通信的接口。

服务只用于与其绑定的应用组件,因此若没有组件与该服务绑定,则系统会销毁该服务。不必像通过 onStartCommand() 启动的服务那样,以相同方式停止绑定服务。

在前台运行的服务

前台服务是用户主动意识到的一种服务,因此在内存不足时,系统也不会考虑将其终止。前台服务必须为状态栏提供通知,将其放在运行中的标题下方。这意味着除非将服务停止或从前台移除,否则不能清除该通知。

在使用前台服务时,应注意以下事项:

只有当应用执行的任务需供用户查看(即使该任务未直接与应用交互)时,才应使用前台服务。因此,前台服务必须显示优先级为 PRIORITY_LOW 或更高的状态栏通知,这有助于确保用户知道应用正在执行的任务。如果某操作不是特别重要,因而想使用最低优先级通知,则可能不适合使用服务;相反,可以考虑使用Sceduler。

创建前台任务,需要请求必要的权限:

如果应用面向 Android 9(API 级别 28)或更高版本并使用前台服务,则其必须请求 FOREGROUND_SERVICE 权限。这是一种普通权限,因此,系统会自动为请求权限的应用授予此权限。

如果面向 API 级别 28 或更高版本的应用试图创建前台服务但未请求 FOREGROUND_SERVICE,则系统会抛出 SecurityException。

创建前台服务的示例:

val pendingIntent: PendingIntent =
Intent(this, ExampleActivity::class.java).let { notificationIntent ->
PendingIntent.getActivity(this, 0, notificationIntent, 0)
}

val notification: Notification = Notification.Builder(this, CHANNEL_DEFAULT_IMPORTANCE)
.setContentTitle(getText(R.string.notification_title))
.setContentText(getText(R.string.notification_message))
.setSmallIcon(R.drawable.icon)
.setContentIntent(pendingIntent)
.setTicker(getText(R.string.ticker_text))
.build()
//提供给 startForeground() 的整型 ID 不得为 0
startForeground(ONGOING_NOTIFICATION_ID, notification)

移除前台服务:

stopForeground (boolean removeNotification) // 是否同时移除通知

此方法不会停止服务。但是,如果服务仍运行于前台时将其停止,则通知也会随之移除。

服务的生命周期

与 Activity 类似,服务也拥有生命周期回调方法,可通过实现这些方法来监控服务状态的变化并适时执行工作。以下展示了每种生命周期方法:

class MyService : Service() {
// 指示服务被终止时的行为
private var startMode: Int = 0   
// 绑定客户端的接口           
private var binder: IBinder? = null        
// 指示是否应使用onRebind
private var allowRebind: Boolean = false   

override fun onCreate() {
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    // 由于调用startService(),服务正在启动
    return mStartMode
}

override fun onBind(intent: Intent): IBinder? {
    // 客户端使用bindService()绑定到服务
    return mBinder
}

override fun onUnbind(intent: Intent): Boolean {
    // 所有客户端都与unbindService()解除绑定
    return mAllowRebind
}

override fun onRebind(intent: Intent) {
    // 在已经调用onUnbind()之后,客户端使用bindService()绑定到服务
}

override fun onDestroy() {
}

附上一张经典的service生命周期图示:

关于service的生命周期:

  • 服务的整个生命周期贯穿调用 onCreate() 和返回 onDestroy() 之间的这段时间。与 Activity 类似,服务也在 onCreate() 中完成初始设置,并在 onDestroy() 中释放所有剩余资源。例如,音乐播放服务可以在 onCreate() 中创建用于播放音乐的线程,然后在 onDestroy() 中停止该线程。
  • 服务的活动生命周期从调用 onStartCommand() 或 onBind() 开始。每种方法均会获得 Intent 对象,该对象会传递至 startService() 或 bindService()。对于启动服务,活动生命周期与整个生命周期会同时结束(即便是在 onStartCommand() 返回之后,服务仍然处于活动状态)。对于绑定服务,活动生命周期会在 onUnbind() 返回时结束。

Andoird8.0开始对后台Service限制

我们知道,每次在后台运行时,应用都会消耗一部分有限的设备资源,例如 RAM。 这可能会影响用户体验,如果用户正在使用占用大量资源的应用(例如玩游戏或观看视频),影响会尤为明显。 所以,为了提升用户体验,从Android 8.0(API 级别 26)开始,对应用在后台运行时可以执行的操作施加了限制。

需要注意的是:

默认情况下,这些限制仅适用于适配 Android 8.0(API 级别 26)或更高版本的应用。 然而,即使应用适配的 API 级别低于 26,用户也可以从设置界面,为任意应用启用其中大多数限制。

不会对绑定 Service 产生任何影响。 如果应用定义了绑定 Service,则不管应用是否处于前台,其他组件都可以绑定到该 Service。

IntentService 是一项 Service,因此其遵守针对后台 Service 的新限制。 因此,许多依赖 IntentService 的应用在适配 Android 8.0 或更高版本时无法正常工作。 出于这一原因,Android 支持库 26.0.0 引入了一个新的JobIntentService类,该类提供与 IntentService 相同的功能,但在 Android 8.0 或更高版本上运行时使用作业而非 Service。

对于我们开发者而言,最直接的影响就是,应用处于空闲状态时,可以使用的后台 Service 被限制。 这些限制不适用于前台 Service,因为前台 Service 更容易引起用户注意。

那么什么是空闲状态呢?

空闲状态

处于前台时,应用可以自由创建和运行前台与后台 Service。 进入后台时,在一个持续数分钟的时间窗内,应用仍可以创建和使用 Service。 在该时间窗结束后,应用将被视为处于空闲状态。 此时,系统将停止应用的后台 Service,就像应用已经调用 Service 的 Service.stopSelf() 方法一样。

什么时候应用被视为处于前台

  • 具有可见 Activity(不管该 Activity 已启动还是已暂停)。
  • 具有前台 Service。
  • 另一个前台应用已关联到该应用(不管是通过绑定到其中一个 Service,还是通过使用其中一个内容提供程序)。

创建前台Service方式变更

  • Android 8.0 之前

创建前台 Service 的方式通常是先创建一个后台 Service,然后将该 Service 推到前台

  • Android 8.0 开始

系统不允许后台应用创建后台 Service。 因此,Android 8.0 引入了一种全新的方法,即 startForegroundService(),以在前台启动新 Service。 在系统创建 Service 后,应用有五秒的时间来调用该 Service 的 startForeground() 方法以显示新 Service 的用户可见通知。 如果应用在此时间限制内未调用 startForeground(),则系统将停止此 Service 并声明此应用为 ANR。

在系统增加限制的同时,当然也提供了其他的途径来帮助开发者解决这些限制带来的问题。在大多数情况下,应用都可以使用 JobScheduler 克服这些限制。 这种方法允许应用安排其在未活跃运行时执行工作,不过仍能够使系统可以在不影响用户体验的情况下安排这些作业。

可行的迁移方案

  • 如果处于后台时,应用需要创建一个前台 Service,使用 startForegroundService() 方法,而非 startService()。
  • 如果 Service 容易引起用户注意,将其设置为前台 Service。 例如,播放音频的 Service 始终应为前台 Service。 使用 startForegroundService() 方法创建 Service, 而非 startService()。
  • 寻找一种使用计划作业实现 Service 功能的方式。 如果 Service 未在执行容易立即引起用户注意的操作,一般情况下,都能够使用计划作业。
  • 在应用正常处于前台之前,请推迟后台工作。 今年金九银十我花一个月的时间收录整理了一套知识体系,如果有想法深入的系统化的去学习的,可以点击传送门,我会把我收录整理的资料都送给大家,帮助大家更快的进阶。

文章来源: www.oschina.net,作者:二营长的意大利炮手,版权归原作者所有,如需转载,请联系作者。

原文链接:https://my.oschina.net/u/4219156/blog/3135690

登录后可下载附件,请登录或者注册

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:huaweicloud.bbs@huawei.com进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。
评论文章 //点赞 收藏 0
点赞
分享文章到微博
分享文章到朋友圈

上一篇:商品列表

下一篇:开始你的api:NetApiStarter

评论 (0)


登录后可评论,请 登录注册

评论