Android - 聊聊 Service

学习 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() 的时候,系统会回调 ServiceonStartCommand 方法:

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    return super.onStartCommand(intent, flags, startId);
}

这里我们关心的是 startId 参数。多次 startService() 后,系统每次调用 onStartCommand()startId 都不同。在 stopSelf() 的时候,我们还可以把 startId 传递给它:

stopSelf(startId);

在这种情况下,只有当传入的 startIdonStartCommand() 最后收到的那个时,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 来绑定它(注:这里假定我们现在 ServiceActivity 处于同一个进程中。不同进程的情况下,需要使用 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

加下来,我们需要实现 ServiceonBind() 方法。,我们可以这样:

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 里面处理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章