适用于 Xamarin Android 的“粘性”Intent Service,用于长时间运行的任务
一个结合了 Intent Service 的易用性与粘性服务的长时间运行功能的类
引言
Android 服务是允许在后台执行工作的 Android 组件。虽然顾名思义,服务可以用于构建长时间运行的持久性后台任务,但这并非其默认行为。Android 服务主要有两种类型:
Service
类,以及- 派生的
IntentService
类。
虽然 IntentService
类非常易于使用,并且适用于大多数情况,但它无法支持需要持久 TCP 连接(例如 XMPP 连接)或长时间等待的后台服务的情况。为此,需要 Sticky
服务行为,该行为仅适用于基类 Service
。在本文中,我们为 Xamarin Android 提出了新的 StickyIntentService
类。该类结合了:
IntentService
的易用性和内置功能,即操作在单独的后台线程中运行,只需要实现OnHandleIntent
方法等。Service
类提供的Sticky
后台行为,这使得服务在被 Android 停止后可以重新启动。
StickyIntentService
适用于包含对长时间运行的后台监听 TCP 连接的引用,例如 XMPP 库(如 Sharp.Xmpp)所需的连接。该类具有类似 IntentService
的接口供使用,但缺少 IntentService 的“粘性”行为。
背景
Android 服务由 Service
类提供。默认情况下,Service
对象的运算在主线程上运行,因此必须为这些运算构造一个后台线程。因此,Service
类可能很难使用。作为附加选项,Android 提供了 IntentService
类。IntentService
通过将所有 Intent 发送到工作队列进行处理来工作。此队列在单独的线程上串行处理每个 Intent,并将 Intent 传递给 OnHandleIntent
方法。当所有 Intent 都处理完毕后,IntentService
会通过内部调用 StopSelf
来停止自身。
最后这一点对于在 IntentService
对象中拥有长时间运行的 TCP 连接或其他后台任务(例如 XMPP 所需的任务)至关重要。当处理完提供的 Intent 后,IntentService
会停止自身,此时 Android 可以销毁 IntentService。指向对象或长时间连接(例如 TCP 连接)的任何引用现在都可能被操作系统销毁。此外,由于无法使 IntentService
成为 Sticky
,因此服务稍后不会被重新启动。因此,尽管 IntentService 适用于各种应用程序,但它不适合作为长时间运行的后台服务,或用于支持长时间运行的 TCP 连接(例如使用 Sharp.Xmpp 的 XMPP 连接)。
需要注意的是,当系统内存不足时,Android 可能会停止任何正在运行的服务。但是,对于 Service
对象,可以传递 Sticky
或 RedeliverIntent
Intent 来重新启动后台服务。摘录自 Xamarin Guides:
“当服务被系统停止时,Android 将使用 OnStartCommand
返回的值来确定如何或是否应重新启动服务。该值是 StartCommandResult
类型,可以是以下任何一种:
Sticky
– 粘性服务将被重新启动,并在重启时向OnStartCommand
传递一个空 Intent。用于服务持续执行长时间运行的操作,例如更新股票信息。RedeliverIntent
– 服务将被重新启动,并将系统停止服务之前传递给OnStartCommand
的最后一个 Intent 重新传递。用于继续长时间运行的命令,例如完成大型文件上传。NotSticky
– 服务不会自动重新启动。-
StickyCompatibility
– 在 API 级别 5 或更高版本上,重启将表现为Sticky
,但在早期版本上将降级为 API 级别 5 之前的行为。”
有关服务的更详细解释,请参阅 Xamarin Android Guides 中的相关部分。
因此,本文的目的是构建一个类,该类结合了 IntentService
类的易用性和 Sticky
行为,从而使该类适用于在长时间运行的 TCP 连接中运行,例如 Xmpp 库(例如 Sharp.Xmpp)所需的连接。
关注点
StickyIntentService
类派生自 Service
类。我们要求 IntentService
类具有所有功能和行为,但除了:
- 将服务定义为
Sticky
, - 服务在提供的 Intent 处理完成时不会停止。
幸运的是,Android 的 IntentService
代码是 可用的,因此 Java Android 代码可以迁移到 C# 和 Xamarin,并进行修改,以显示所需行为。IntentService 本质上是一个 Service
public abstract class StickyIntentService : Service
private volatile Looper mServiceLooper; private volatile ServiceHandler mServiceHandler; private String mName; private bool mRedelivery;
该类将初始 Java 文件中的功能和代码(除了 OnStartCommand
和 HandleMessage 方法)从 Java 迁移到 C#。在那里,初始 Java 代码
@Override public int onStartCommand(Intent intent, int flags, int startId) { onStart(intent, startId); return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY; }
在 StickyIntentService
中被迁移到 C# 并进行了修改,如下所示:
public override StartCommandResult OnStartCommand(Intent intent, StartCommandFlags flags, int startId){ OnStart(intent, startId); return StartCommandResult.Sticky; }
因此,OnStartCommand
方法现在返回 Sticky
值。请注意,原始 IntentService 代码中定义的 *RedeliverIntent
* 不适用,因为它将重新传递最后一次传递给服务的 Intent。如果这是一个已处理并完成的消息,这可能会导致错误的结果。
此外,在 HanldeMessage
中处理完 Intent 后,服务不会停止,从而保留了后台运行的 Looper。更详细地说,初始 Java IntentService
代码
@Override public void handleMessage(Message msg) { onHandleIntent((Intent)msg.obj); stopSelf(msg.arg1); }
在 StickyIntentService
中被迁移到 C# 并进行了修改,如下所示:
public override void HandleMessage(Message msg) { sis.OnHandleIntent((Intent)msg.Obj); }
Using the Code
您可以完全按照使用 IntentService 类的方式来使用 StickyIntentServiceClass。有关更多信息,请参阅 Xamarin 的服务指南。整个 StickyIntentService
类代码(C#)可在下方找到。请注意,保留了最初的 Android 源代码注释。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Android.App; using Android.Content; using Android.OS; using Android.Runtime; using Android.Views; using Android.Widget; namespace Pgstath.Utils { /** * IntentService is a base class for {@link Service}s that handle asynchronous * requests (expressed as {@link Intent}s) on demand. Clients send requests * through {@link android.content.Context#startService(Intent)} calls; the * service is started as needed, handles each Intent in turn using a worker * thread, and stops itself when it runs out of work. * * <p>This "work queue processor" pattern is commonly used to offload tasks * from an application's main thread. The IntentService class exists to * simplify this pattern and take care of the mechanics. To use it, extend * IntentService and implement {@link #onHandleIntent(Intent)}. IntentService * will receive the Intents, launch a worker thread, and stop the service as * appropriate. * * <p>All requests are handled on a single worker thread -- they may take as * long as necessary (and will not block the application's main loop), but * only one request will be processed at a time. * * <div class="special reference"> * <h3>Developer Guides</h3> * <p>For a detailed discussion about how to create services, read the * <a href="{@docRoot}guide/topics/fundamentals/services.html">Services</a> developer guide.</p> * </div> * * @see android.os.AsyncTask */ public abstract class StickyIntentService : Service { private volatile Looper mServiceLooper; private volatile ServiceHandler mServiceHandler; private String mName; private bool mRedelivery; private sealed class ServiceHandler : Handler { private StickyIntentService sis; public ServiceHandler(Looper looper, StickyIntentService sis): base(looper) { this.sis = sis; } public override void HandleMessage(Message msg) { sis.OnHandleIntent((Intent)msg.Obj); } } /** * Creates an IntentService. Invoked by your subclass's constructor. * * @param name Used to name the worker thread, important only for debugging. */ public StickyIntentService(String name):base() { mName = name; } /** * Sets intent redelivery preferences. Usually called from the constructor * with your preferred semantics. * * <p>If enabled is true, * {@link #onStartCommand(Intent, int, int)} will return * {@link Service#START_REDELIVER_INTENT}, so if this process dies before * {@link #onHandleIntent(Intent)} returns, the process will be restarted * and the intent redelivered. If multiple Intents have been sent, only * the most recent one is guaranteed to be redelivered. * * <p>If enabled is false (the default), * {@link #onStartCommand(Intent, int, int)} will return * {@link Service#START_NOT_STICKY}, and if the process dies, the Intent * dies along with it. */ public void setIntentRedelivery(bool enabled) { mRedelivery = enabled; } public override void OnCreate() { // TODO: It would be nice to have an option to hold a partial wakelock // during processing, and to have a static startService(Context, Intent) // method that would launch the service & hand off a wakelock. base.OnCreate(); HandlerThread thread = new HandlerThread("IntentService[" + mName + "]"); thread.Start(); mServiceLooper = thread.Looper; mServiceHandler = new ServiceHandler(mServiceLooper,this); } public override void OnStart(Intent intent, int startId) { Message msg = mServiceHandler.ObtainMessage(); msg.Arg1 = startId; msg.Obj = intent; mServiceHandler.SendMessage(msg); } /** * You should not override this method for your IntentService. Instead, * override {@link #onHandleIntent}, which the system calls when the IntentService * receives a start request. * @see android.app.Service#onStartCommand */ public override StartCommandResult OnStartCommand(Intent intent, StartCommandFlags flags, int startId) { OnStart(intent, startId); return StartCommandResult.Sticky; } public override void OnDestroy() { mServiceLooper.Quit(); } /** * Unless you provide binding for your service, you don't need to implement this * method, because the default implementation returns null. * @see android.app.Service#onBind */ public override IBinder OnBind(Intent intent) { return null; } /** * This method is invoked on the worker thread with a request to process. * Only one Intent is processed at a time, but the processing happens on a * worker thread that runs independently from other application logic. * So, if this code takes a long time, it will hold up other requests to * the same IntentService, but it will not hold up anything else. * When all requests have been handled, the IntentService stops itself, * so you should not call {@link #stopSelf}. * * @param intent The value passed to {@link * android.content.Context#startService(Intent)}. */ protected abstract void OnHandleIntent(Intent intent); } }
历史
第一个版本