分类:C#、Android、VS2015;
创建日期:2016-03-01
一、简介
Started Service是指被同一个应用程序的某个对象显式启动,或者在设备引导时就已经启动了(配置了服务的情况)。
二、Started Services的生命周期
前面我们说过,Service只是一种被分离出来的组件(例如从某个Activity中分离出来),可被单独启动启动和停止。因此不论是Started Services还是Bound Services,这些Services都有它自己独立的生命周期。
下图演示了Started Services的生命周期期间调用的方法。
一旦服务被启动(started),它就拥有了自己的生命周期,这是独立于启动它的组件的。并且它能够在后台一直运行下去,即使启动它的组件已被销毁也是如此。因此,服务应该能够在完成工作后自行终止,通过调用StopSelf()即可终止服务自身,或者由其它组件通过调用StopService()也可以终止服务。
对于activity之类的应用程序组件,可以通过调用StartService()启动服务,并传入一个给出了服务和服务所需数据的Intent对象。服务将在OnStartCommand()方法中接收到该Intent对象。举个例子,假定某activity需要把一些数据保存到在线数据库中,此activity可以启动一个守护服务并通过传入StartService()的一个intent把需要保存的数据发送给该服务,该服务在OnStartCommand()内接收intent、连接Internet,再进行数据库事务处理。当事务完成后,服务自行终止,并被系统销毁。
警告:默认情况下,运行服务的进程与应用程序相同,并且运行在应用程序的主线程中。 因此,如果你的服务要执行计算密集或阻塞的操作,而同时用户又需要与同一个应用程序中的activity进行交互,那么服务将会降低activity的性能。为了避免对应用程序性能的影响,你应该在服务中启动一个新的线程。
使用服务时,最重要的、需要重写的回调方法有下面几种。
1、OnStartCommand()
任何一个对象请求开始服务时,系统都会调用这个方法。比如一个activity通过调用StartService()请求服务时,系统将会调用本方法。调用StartService()、重启系统等也会调用该方法。
一旦本方法执行,服务就被启动,并在后台一直运行下去。 如果你的代码实现了本方法,你就有责任在完成工作后通过调用StopSelf()或StopService()终止服务。
OnStartCommand要求必须返回一个StartCommandResult枚举类型的值,它告诉安卓系统如果获取的服务已停止是否应该重新启动这个服务。例如,下面的代码返回StartResultCommand.Sticky枚举值,此时执行OnStartCommand方法时将自动重新启动该服务:
public override StartCommandResult OnStartCommand (……)
{
// start a task here
……
return StartCommandResult.Sticky;
}
StartCommandResult枚举值可以是下面的选项之一:
- Sticky – 此选项表示将重新启动指定的服务,同时传递给OnStartCommand方法一个值为null的Intent类型的参数。这种服务常用于不断执行一个需要长时间运行的操作(比如股票行情)。
- RedeliverIntent – 此选项用于正常执行服务时Intent包含有关键的附加信息(extra information)的情况。如果在最后一个Intent发送前停止了服务,此时将重新启动该服务,并将这个Intent传递给OnStartCommand方法。
- NotSticky –该服务不会自动重新启动。
- StickyCompatibility – 该选项仅仅是为了与API 5之前的版本兼容而提供的,其含义与Sticky的含义相同,现在的项目中很少用到它。
在这些返回的选项中,最常用的是StartCommandResult.Sticky。当然其他选项也会在不同的场合下用到,否则提供不同的选项就没有意义了。
注:Android 1.6及更低版本使用的是OnStart()方法而不是OnStartCommand()方法。从Android 2.0开始,OnStart()已经过时,改为用OnStartCommand()取而代之。
2、OnBind()
当其它组件需要通过BindService()绑定服务时(比如执行RPC),系统会调用本方法。 在本方法的实现代码中,你必须返回IBinder来提供一个接口,客户端用它来和服务进行通信。 你必须确保实现本方法,不过假如你不需要提供绑定,那就返回null即可。
3、OnCreate()
当首次启动服务时被调用一次,一般用它实现初始化工作。
注意仅在第一次启动服务时,才会调用一次这个方法。如果服务已经运行,则不会再调用本方法。
4、OnDestroy()
当服务用不上了并要被销毁时,系统会调用本方法。你的服务应该在这个方法中进行清理服务占用的资源,比如线程、已注册的侦听器listener和接收器receiver等等。这将是服务收到的最后一个调用。
如果组件通过调用StartService()(这会导致onStartCommand()的调用)启动了服务,那么服务将一直保持运行,直至自行用stopSelf()终止或由其它组件调用StopService()来终止它。
如果组件调用BindService()来创建服务(此时OnStartCommand()就不会被调用),则服务的生存期就与被绑定的组件一致。一旦所有客户端都对服务解除了绑定,系统就会销毁该服务。
仅当内存少得可怜、且必须覆盖拥有用户焦点的activity的系统资源时,Android系统才会强行终止一个服务。 如果服务被拥有用户焦点的activity绑定着,则它一般不会被杀死。 如果服务声明为“在前台运行服务”,则它几乎永远不会被杀死。 否则,如果服务已被启动并且已运行了很长时间,那么系统将会随时间推移而降低它在后台任务列表中的级别, 此类服务将很有可能会被杀死——如果服务已经启动,那你必须好好设计代码,使其能完美地应付被系统重启的情况。如果系统杀死了你的服务,只要资源再度够用,系统就会再次启动服务(当然这还取决于OnStartCommand()的返回值)。
关于系统可能会在何时销毁服务的详细信息,请参阅进程和线程。
三、创建、启动和停止Started服务
1、创建自定义的Service
创建服务的第一步是创建继承自Service的子类(Service类是所有服务的基类)。
与自定义的Activities相对应,通过ServiceAttribute特性声明(用C#声明特性时先去掉Attribute后缀然后再用中括号将其括起来)可告诉系统这是一个自定义的服务:
[Service]
public class MyService : Service
{
...
}
用ServiceAttribute类声明Service特性后,它就会自动在AndroidManifest.xml中注册这个服务,而不需要我们去手工配置AndroidManifest.xml文件。例如,假定项目名为ServiceDemo1,用[Service]声明后,它就会自动在AndroidManifest.xml中添加下面的代码:
<service android:name="servicedemo1.ServiceDemo1"></service>
当然也可以手工在AndroidManifest.xml中直接添加配置代码,但一般不这样做(18.1已经说过一遍了),这是因为在配置文件中添加时没有智能提示,特别是对于初学者来说比较容易出错。而用继承自Service的子类实现时,在.cs文件中添加特性时有智能提示,既免去了配置的麻烦,用起来也非常简单、直观、方便。
2、启动Service
在上下文中(例如某个Activity)调用StartService()方法可以初始化Started Services。
如果该服务正在从某项活动中启动,那么可以直接在该活动中调用StartService()方法,否则,可先通过Android.App.Application.Context获取当前上下文,然后再调用该方法。
要启动一个服务,需要传递一个Intent指定要启动的服务类型以及当前上下文。
例如,下面的代码在一个活动中启动MyService类型的服务:
this.StartService (new Intent (this, typeof(MyService)));
从Started Services生命周期中我们已经知道,调用StartService()方法将导致Android调用服务中提供的OnStartCommand()方法。同时也知道了OnStartCommand()要求必须返回一个StartCommandResult枚举类型的值,它告诉安卓系统如果获取的服务已停止是否应该重新启动这个服务。
例如,下面的代码返回StartResultCommand.Sticky枚举值,此时执行OnStartCommand方法时将自动重新启动该服务:
public override StartCommandResult OnStartCommand (Intent intent, StartCommandFlags flags, int startId)
{
// start a task here
new Task (() => {
// long running code
DoWork();
}).Start();
return StartCommandResult.Sticky;
}
【注意】:在该方法中必须使用Task或者自定义的Thread来执行服务的初始化工作。这是因为服务是运行在UI线程上的,任何长时间运行的任务都会让UI渲染停顿,从而导致应用程序无响应。而使用Task或自定义的Thread来执行服务的初始化工作,不会引起界面停顿的现象。
3、停止Service
除非任务开始后打算无限期地运行下去,否则一个已启动的服务应调用StopSelf方法停止它无休止地长时间运行。这很重要,因为Started Services是一个独立运行的组件,运行期间将继续占用系统的绘制资源,直到它被显式停止或被操作系统关闭。
下面的代码演示了如何在完成任务后调用StopSelf()方法停止服务:
public void DoWork ()
{
var t = new Task (() => {
Thread.Sleep (5000); //模拟长时间执行的任务
StopSelf ();
});
t.Start();
}
或者:
public void DoWork ()
{
var t = new Thread (() => {
Log.Debug ("DemoService", "Doing work");
Thread.Sleep (5000);
Log.Debug ("DemoService", "Work complete");
StopSelf ();
});
t.Start ();
}
另外,为了避免无限期地继续服务的可能性,调用方还可以通过调用StopService方法请求停止该服务,如下所示:
StopService (new Intent (this, typeof(MyService)));
当服务停止时,Started Service会自动调用服务中的OnDestroy方法,在这个方法中应该做一些清理服务所占用的资源的工作。
在服务类中,只需要重写OnDestroy方法即可:
public override void OnDestroy ()
{
base.OnDestroy ();
// 在此处编写清理资源的代码
}
多个调用方都可以请求启动服务,如果某个外部请求启动服务,也可以将startId传递到OnStartCommand方法,以防止该服务被过早地停止。StartId对应最后一次调用的StartService方法,每次执行OnStartCommand方法都会递增该值。因此,如果对StartService后面的请求还没有导致对OnStartCommand的调用,此时服务可以调用StopSelfResult方法并传递它收到的startId最新值。如果调用StartServic没有导致运行OnStartCommand,则系统不会停止该服务,因为startId调用中所使用的方法将不会对应于最新的StartService调用。
一个started服务必须自行管理生命周期。也就是说,系统不会终止或销毁这类服务,除非必须恢复系统内存并且服务返回后一直维持运行。 因此,服务必须通过调用stopSelf()自行终止,或者其它组件可通过调用stopService()来终止它。
再强调一遍:当服务完成工作后,你的应用程序应该及时终止它,这非常重要。因为这样可以避免系统资源的浪费,并能节省电池电量的消耗。必要时,其它组件可以通过调用StopService()来终止服务。即使你的服务允许绑定,你也必须保证它在收到对OnStartCommand()的调用时能够自行终止。
用StopSelf()或StopService()的终止请求一旦发出,系统就会尽快销毁服务。
不过,如果你的服务要同时处理多个OnStartCommand()请求,那么,在处理启动请求的过程中,你就不应该去终止服务,因为你可能接收到了一个新的启动请求(在第一个请求处理完毕后终止服务将停止第二个请求的处理。为了避免这个问题,你可以用StopSelf(int)来确保终止服务的请求总是根据最近一次的启动请求来完成。也就是说,当你调用StopSelf(int) 时,你把启动请求ID(发送给OnStartCommand()的startId)传给了对应的终止请求。这样,如果服务在你可以调用StopSelf(int)时接收到了新的启动请求,则ID将会不一样,服务将不会被终止。
四、示例1--StartedServiceDemo1
运行截图
主要设计步骤
(1)添加ch1601_Main.axml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <Button android:id="@+id/ch1601StartService" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="启动服务" /> <Button android:id="@+id/ch1601StopService" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="停止服务" /> </LinearLayout>
(2)添加ch1601ServiceDemo.cs
using Android.App; using Android.Content; using Android.OS; using Android.Runtime; using Android.Widget; using System.Threading; namespace MyDemos.SrcDemos { [Service] public class ch1601ServiceDemo : Service { Thread thread; [return: GeneratedEnum] public override StartCommandResult OnStartCommand(Intent intent, [GeneratedEnum] StartCommandFlags flags, int startId) { //获取主线程的消息循环后,就可以在主线程中显示来自服务的消息了 var myHandler = new Handler(MainLooper); //在此处执行需要长时间处理的服务 thread = new Thread(() => { //处理过程中,还可以告诉用户处理的状态 //这里用每隔3秒显示一次消息来模拟,此服务可随时被MainActivity终止 for (int i = 1; i <= 10; i++) { var msg = string.Format("这是来自服务的第{0}个消息", i); Thread.Sleep(3000); myHandler.Post(() => { Toast.MakeText(this, msg, ToastLength.Long).Show(); }); } StopSelf(); }); thread.Start(); return StartCommandResult.NotSticky; } public override void OnDestroy() { base.OnDestroy(); thread.Abort(); var myHandler = new Handler(MainLooper); myHandler.Post(() => { Toast.MakeText(this, "服务已停止", ToastLength.Long).Show(); }); } //基类要求实现的接口 public override IBinder OnBind(Intent intent) { return null; } } }
注意,如果你在运行中发现中文显示为乱码,别忘了你需要在AssemblyInfo.cs文件中指定区域语言(前面章节已经介绍过),即修改下面的语句(在参数中指定“zh-CN”):
[assembly: AssemblyCulture("zh-CN")]
网上介绍的什么更改高级保存选项都是挖坑的,千万别信。
(3)添加ch1601MainActivity.cs
using Android.App; using Android.Content; using Android.OS; using Android.Widget; namespace MyDemos.SrcDemos { [Activity(Label = "ch1601MainActivity")] public class ch1601MainActivity : Activity { protected override void OnCreate(Bundle savedInstanceState) { base.OnCreate(savedInstanceState); SetContentView(Resource.Layout.ch1601_Main); Intent intent = new Intent(this, typeof(ch1601ServiceDemo)); var start = FindViewById<Button>(Resource.Id.ch1601StartService); start.Click += delegate { StartService(intent); Toast.MakeText(this, "服务已启动!", ToastLength.Short).Show(); }; var stop = FindViewById<Button>(Resource.Id.ch1601StopService); stop.Click += delegate { StopService(intent); Toast.MakeText(this, "服务被强行请求停止!", ToastLength.Short).Show(); }; } } }