学习 Android 应用开发时,我们首先接触到的是 Activity
。简单地理解,一个 Activity
就是 Android 应用的一个页面。而在某些情况下,我们可能需要在“页面”不可见,仍然执行某些操作。这个时候,我们就需要 Service
了。
和 Activity
类似,Service
也是 Android 非常重要的组件之一,主要用于在后台执行某些任务。比方说,我们下载一个文件时,即使用户不在下载页面,我们也希望下载任务能够继续进行。
使用 Service
非常简单,我们只需要简单继承 Service
并实现 onBind()
方法,然后在 manifest
里注册即可:
public class MyService extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
// AndroidManifest.xml
<service android:name=".MyService" />
跟启动 Activity
类似,我们构造一个 Intent
并 start 它:
Intent intent = new Intent(this, MyService.class);
startService(intent);
我们使用 startService()
启动它。在这种情况下,我们称这个 Service
是一个 started service。started service 主要用于执行某个任务的场合。比如,我们要下载一个文件,就可以 startService()
,然后让 service
在后台帮我们去下载。文件下载结束后,service
就可以自己停止自己的生命:
stopSelf();
但是,这里存在一个问题。我们可能使用这个 service
同时下载多个文件。这种情况下,就不能简单地调用 stopSelf()
了。因为一个文件下载完成,并不意味着所有文件都下载结束。
这种情况的一个可选办法是,在下载完成后,显式检查是否还有其他任务,如果没有,就 stopSelf()
:
private final Object mLock = new Object();
private int mNumTask;
private void stopSelfIfAllDone() {
synchronized (mLock) {
if (--mNumTask == 0) {
stopSelf();
}
}
}
上面介绍的这种结束方式,适用于在 Service
启动多个工作线程的情况。另一种情况是,我们只使用一个工作线程,每个任务都由该线程串行执行。这种情况下,我们有理由相信,最后一个 task 执行完,就没有其他任务了。此时,我们可以直接使用 Android 提供的 IntentService
。即便你不想用 IntentService
,看看它的实现方式,也是非常值得的。
当我们 startService()
的时候,系统会回调 Service
的 onStartCommand
方法:
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
}
这里我们关心的是 startId
参数。多次 startService()
后,系统每次调用 onStartCommand()
的 startId
都不同。在 stopSelf()
的时候,我们还可以把 startId
传递给它:
stopSelf(startId);
在这种情况下,只有当传入的 startId
是 onStartCommand()
最后收到的那个时,service
才会真的停止。
假设有这么一个场景,我们 startService()
3 次,得到的 startId
分别为 1, 2, 3
。接下来,在每个任务完成后,我们都 stopSelf(startId)
。由于最后收到的 startId
是 3。所以前两个 stopSelf(1), stopSelf(2)
都不会真的停止 Service
。
这就是 IntentService
的工作方式了,如果你查看它的源码,你会发现这个:
private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
onHandleIntent((Intent)msg.obj);
stopSelf(msg.arg1);
}
}
聊完了 started service,接下来我们聊聊 bound service。
继续我们上面的下载文件 Service
。现在我们不止希望它能够在后台悄悄帮我们下载文件,还想要有一个下载管理页面。在这个下载管理页面,我们需要看到下载的进度。
这时我们就可以使用 bound service 了。这里的 “bound” 其实是 bind 的过去式,指的是被绑定过的 service
。和 started service 类似,我们通过 intent 来绑定它(注:这里假定我们现在 Service
跟 Activity
处于同一个进程中。不同进程的情况下,需要使用 AIDL,不在本篇讨论)
private ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
MyService.MyBinder binder = (MyService.MyBinder) service;
MyService myService = binder.getService();
// put your code here
}
@Override
public void onServiceDisconnected(ComponentName name) {
// put your code here
}
};
Intent intent = new Intent(this, MyService.class);
bindService(intent, mServiceConnection, BIND_AUTO_CREATE);
由于在我们绑定 Service
的时候,Service
可能还没有启动,所以,这里需要使用一个回调 ServiceConnection
。
加下来,我们需要实现 Service
的 onBind()
方法。,我们可以这样:
private IBinder mBinder = new MyBinder();
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
public class MyBinder extends Binder {
public MyService getService() {
return MyService.this;
}
}
这样一来,我们能够在 Activity
中拿到 Service
的引用。由于他们处于同一个进程,Activity
中可以调用 Service
的任意方法(别忘了,Service
也是一个 Java 对象,我们持有它的引用,当然可以调用它的方法!)
对于 bound service,service
相当于一个服务的提供者,activity
在这里则是客户。这种情况下,往往只有客户知道什么时候需要使用服务、什么时候不再需要。跟 started service 不同,bound service 需要客户显式地 unbind:
unbindService(mServiceConnection);
这个时候,你可能会有点疑惑,对于我们的文件下载服务,我们既 startService
,又 bindService
。那么,unbindService
的时候,服务会停止吗?stopSelf
呢?
其实,这个问题完全是我们多虑了,Android 已经帮我们考虑到了这种情况。当既有 startService()
,又有 bindService
时,仅当我们 stopSelf()
并且 unbindService
时,服务才会停止。
最后,对于一些初学者可能无法理解的是,即便我们使用了一个文件下载服务,但实际的下载过程,还是需要在后台线程进行(Service
的所有方法都运行在主线程)。那为什么不直接在 activity
启动线程来下载就可以了?
问题在于,当 activity
退出后,即便还有后台线程在运行,如果所有的 Android 组件都退出了,系统完全有可能会杀死这个进程,以回收设备的资源。而如果还有 service
在运行,则可以避免这种情况。
使用广播的时候,也可能会遇到类似的问题。Android 系统对广播的执行时间有严格的要求。如果收到某个广播后,需要做一些很耗时的操作(比如,还是下载文件),不能直接 new Thread
,而因为 startService
然后在 Service
里面处理。