这是 面试系列 的第三期。本期我们将来探讨一下 Android 四大组件的重要组成部分:Service。
往期内容传递:
Android 面试(一):说说 Android 的四种启动模式
Android 面试(二):如何理解 Activity 的生命周期
Android 面试(三):用广播 BroadcastReceiver 更新 UI 界面真的好吗?
Service 有多重要?
之前在「兰柳学」的文章中看到这样一个场景,挺有意思的,先给大家分享一下,让我们一起来看看对 Service 的无知到底会有多麻烦。
场景:如果一个应用要从网络上下载一个文件,并在 Activity 上展示进度条,这个 Activity 要求是可以转屏的。那么在转屏时 Actvitiy 会重启,如何保证下载的进度条能正确展示进度呢?
不会 Service 的人,一般会想出来这样的方案。
- 在转屏前将进度缓存,转屏后再读出来。
- 使用
android:configChanges
设置,让转屏时 Activity 不销毁和重建。
- 使用
针对第 1 种方案,其实细想漏洞百出。首先,转屏的过程中,我们知道 Activity 的重建算是比较耗时的,可能会有几百毫秒甚至更久,这时候下载线程仍然在工作,进度肯定和保存时的进度不一致了,如何处理这个问题呢?
第 2 个方案,大家可以自己展开思考,实际的项目中可能会需要额外做一些事情来处理 ContentView 的横竖布局的问题。
如果采用 Service,你有什么好主意呢?不妨在评论区给出。
一定听过 Service 吧,它有几种启动方式?
Service 是一个专门在后台执行长时间操作的类,它并不与用户产生 UI 交互。它提供了两种启动方式。
- started
其它组件调用startService()
启动一个 Service。一旦启动,Service 将一直运行在后台,即使启动这个 Service 的组件已经被销毁。通常一个被 start 的 Service 会在后台执行单独的操作,也并不需要给启动它的组件返回结果。只有当 Service 自己调用stopSelf()
或者其它组件调用stopService()
才会终止。- bind
其它组件可以调用bindService()
来绑定一个 Service。这种方式会让 Service 和启动它的组件绑定在一起,当启动它的组件销毁的时候,Service 也会自动进行 unBind 操作。同一个 Service 可以被多个组件绑定,只有所有绑定它的组件都进行了 unBind 操作,这个 Service 才会被销毁。
当然,Service 还可以同时在上述两种方式下运行。这涉及到 Service 的两个回调方法的执行: onStartCommand()
(通过 start 方式启动一个 Service 时的回调方法。)、onBind()
(通过 bind 方式启动一个 Service 回调的方法)。
无论通过那种方式启动 Service(start、bind、start & bind),任何组件(甚至其他应用的组件)都可以使用 Service。并通过 Intent 传递参数。当然,你也可以将 Service 在 AndroidMenifest.xml
文件中配置成私有的,不允许其他应用访问。
将
android:exported
属性设为 false,表示不允许其他应用程序启动本应用的组件,即便是显式 Intent 也不行(even when using an explicit intent)。这可以防止其他应用程序启动您的 Service 组件。
Service 的生命周期
一谈到组件,我们总是喜欢去研究它的生命周期,而这时候用图来展示肯定是最好的了。
这两条路径并不是毫不相干的。当调用
startService()
去 start 一个 Service 后,你仍然可以 bind 这个 Service。比如:当播放音乐的时候,需要调用 startService()
启动指定的音乐,当需要获取该音乐的播放进度的时候,又需要调用 bindService()
,在这种情况下,除非 Service 被 unbind,此前调用 stopService()
和 stopSelf()
都不能停止该 Service。
这些生命周期方法在使用的时候并不需要调用各自的父类方法。
两条生命周期路径都可以包含两个嵌套的生命周期:
完整生命周期(entire lifetime):从
onCreate()
被调用,到onDestroy()
返回。和 Activity 类似,一般在onCreate()
方法中做一些初始化的工作,在onDestroy()
中做一些资源释放的工作。如,若 Service 在后台播放一个音乐,就需要在onCreate()
方法中开启一个线程启动音乐,并在onDestroy()
中结束线程。活动生命周期(activity lifetime):从
onStartCommand()
或onBind()
回调开始,由相应的startService()
或bindService()
调用。start 方式的活动生命周期结束就意味着完整证明周期的结束,而 bind 方式,当onUnbind()
返回后,Service 的活动生命周期结束。
值得注意的是,无论是
startService()
还是bindService()
启动 Service,onCreate()
和onDestroy()
均会被回调。
Service 的 onCreate() 可以执行耗时操作吗?
Service 运行在主线程中,它并不是一个新的线程,也不是新的进程,所以并不能执行耗时操作。
那如果要在 Service 中执行耗时操作,怎么做?
我想基本所有人都能想到使用 Thread,事实上我们也经常这么做。需要在主线程执行耗时操作,无非就是开一个线程,然后一阵混沌操作。当然,你还可以使用 AysncTask
或 HandlerThread
来替代 Thread 创建线程。
当然没有问题,那还有其它更有意思的方式吗?
有,当然有,IntentService 就是一个不错的选择。
纷繁复杂的 IntentService
IntentService
继承于Service
,若 Service 不需要同时处理多个请求,那么使用IntentService
将是最好选择。你只需要重写onHandleIntent()
方法,该方法接收一个回调的 Intent 参数,你可以在方法内进行耗时操作,因为它默认开启了一个子线程,操作执行完成后也无需手动调用stopSelf()
方法,onHandleIntent()
将会自动调用该方法。
使用 IntentService 的要点如下:
- 默认在子线程中处理回传到
onStartCommand()
方法中的 Intent; - 在重写的
onHandleIntent()
方法中处理按时间排序的 Intent 队列,所以不用担心多线程(multi-threading)带来的问题。 - 当所有请求处理完成后,自动停止 Service,无需手动调用
stopSelf()
方法; - 默认实现了
onBind()
方法,并返回 null; - 默认实现了
onStartCommand()
方法,并将回传的 Intent 以序列的形式发送给onHandleIntent()
,您只需重写该方法并处理 Intent 即可。
小结
当我们知道了 Service 的用途,心中有一个 Service 相关的概念时,针对实际的场景还是要做具体的分析再决定是否使用 Service。因为 Service 仍然是在主线程中调用,还是要开线程才能处理长时间的工作,Service 和 UI 的交互也让这个方式变得不那么简便。如果你只需要在当前界面去做一些耗时操作,界面退出或改变时,工作也要停止,那么这时直接使用 Thread(或者 AsyncTask, ThreadHandler)会更合适你。