Android--Service之提高
前言
上一篇博客讲解了一下Android下Service组件的基本使用,对Service组件还不了解的朋友可以先去看看另外一篇Service基础的博客:Android--Service之基础。这篇博客讲解一下Service组件的一些需要注意的地方以及高级的应用,并用几个例子讲解一下本文中提到的功能,最后依然会提供示例源码下载。
既然是深入讲解Service,本片博客涉及的内容有点杂乱,这里列个导航方便查看。
对于Service而言,它依然是运行在主线程之上,所以一些无法在主线程上完成的功能,依然需要另外开启工作线程来完成,并且一些耗时操作,如果直接放在Service的主线程中完成的话,会影响设备的运行流畅度。对于这样的问题,有两种解决方案,一种是在Service主线程中额外开启一条工作线程,如何开启工作线程的方法在以前的博客中已经介绍过了,这里不再重复介绍;另外一个方法就是使用IntentService这个父类来实现Service业务类,这里着重讲解这个IntentService。
IntentService是一个服务基类,直接继承于Service,在需要的时候通过异步调用的方式处理请求。要使用IntentService启动一个服务进行异步调用,需要实现它的一个抽象方法:onHandleIntent(Intent intent),在这个方法里,可以获得访问这传来的Intent对象,并在其中进行异步操作的实现。对于IntentService而言,因为其继承自Service类,所以其他的Service的声明周期方法在此类中也适用,访问者可以通过调用Context.startService(Intent intent)方法来启动这个服务。
使用IntentService作为服务基类,在其内部其实也是重新开启了一条线程来完成操作,只是这里使用IntentService进行了封装,并且它自己管理服务的结束。使用IntentService的服务类,在执行结束后执行结束,无需人为的结束它,比较适用于一些无需管理,但是又比较耗时的操作!
IntentService-Demo
下面通过一个小示例演示IntentService的操作,在这个示例中,下载一张网络上的图片,保存在设备的本地目录下。其中涉及到的本地存储和访问网络下载图片的内容,如果你不了解的朋友,可以查看另外两篇博客:Android--数据持久化、Android--HTTPClient。
IntentSer.java:
1 package cn.bgxt.servicedemohigh; 2 3 import java.io.File; 4 import java.io.FileOutputStream; 5 import java.io.IOException; 6 import java.io.InputStream; 7 import java.net.MalformedURLException; 8 import java.net.URL; 9 10 import android.app.IntentService; 11 import android.content.Intent; 12 import android.util.Log; 13 import android.widget.Toast; 14 15 public class IntentSer extends IntentService { 16 private final static String TAG = "main"; 17 private String url_path="http://ww2.sinaimg.cn/bmiddle/9dc6852bjw1e8gk397jt9j20c8085dg6.jpg"; 18 public IntentSer() { 19 super("IntentSer"); 20 } 21 @Override 22 public void onCreate() { 23 Log.i(TAG, "Service is Created"); 24 super.onCreate(); 25 } 26 27 @Override 28 public int onStartCommand(Intent intent, int flags, int startId) { 29 Log.i(TAG, "Service is started"); 30 return super.onStartCommand(intent, flags, startId); 31 } 32 33 @Override 34 public void onDestroy() { 35 Log.i(TAG, "Service is Destroyed"); 36 super.onDestroy(); 37 } 38 @Override 39 protected void onHandleIntent(Intent intent) { 40 Log.i(TAG, "HandleIntent is execute"); 41 try { 42 // 在设备应用目录下创建一个文件 43 File file=new File(this.getFilesDir(), "weibo.jpg"); 44 FileOutputStream fos=new FileOutputStream(file); 45 // 获取网络图片的输入流 46 InputStream inputStream = new URL(url_path).openStream(); 47 // 把网络图片输入流写入文件的输出流中 48 byte[] date=new byte[1024]; 49 int len=-1; 50 while((len=inputStream.read(date))!=-1){ 51 fos.write(date, 0, len); 52 } 53 54 fos.close(); 55 inputStream.close(); 56 Log.i(TAG, "The file download is complete"); 57 } catch (MalformedURLException e) { 58 e.printStackTrace(); 59 } catch (IOException e) { 60 e.printStackTrace(); 61 } 62 } 63 }
编写好服务类,仅需要适用一个Android组件即可启动服务,这里使用一个Activity中来启动服务,下面是主要代码。
1 btnDownload=(Button)findViewById(R.id.btnDownload); 2 btnDownload.setOnClickListener(new View.OnClickListener() { 3 4 @Override 5 public void onClick(View v) { 6 // TODO Auto-generated method stub 7 Intent service=new Intent(MainActivity.this,IntentSer.class); 8 startService(service); 9 } 10 });
执行结果均写入日志中,可以通过LoaCat查看。
下载完成后,可以在应用的安装目录下/files下看到下载的weibo.jpg文件。
对Service了解后,会发现它实现的大部分功能使用Thread也可以解决,并且Thread使用起来比Service方便的多,那么为什么还需要使用Service呢,下面来详细解释一下。
首先,Thread是程序执行的最小单元,它是分配系统资源的基本单位,主要用于执行一些异步的操作。而Service是Android的一种机制,当它使用bindService()被绑定的时候,是运行在宿主主进程的主线程上的,当使用startService()启动服务的时候,是独立运行在独立进程的主线程上的,因此它们的核心没有任何关系。
其次,对于Thread而言,它是独立于启动它的组件的,如使用一个Activity启动了一个Thread,当这个Activity被销毁前,没有主动停止Thread或者Thread的run()方法没有执行完毕的话,Thread也会一直执行下去,这样就很容易导致一些问题,当这个Activity被销毁之后,将不再持有这个Thread的引用,也就是说,无法再在另外一个Activity中对同一个Thread进行控制。而Service不同,在Android系统中,无论启动或绑定几次,只会创建一个对应的Service实例,所以只要这个Service在运行,就可以在能获取到Context对象的地方控制它,这些特点是Thread无法做到的。
上一篇博客简单讲解了一下Service的生命周期,但是讲解的不够详细,这里另外再解释一下,下图是官方API中给定的关于Service生命周期的图示:
从上图可以看出,对于启动服务和绑定服务存在不同的生命周期,但是大部分调用的生命周期方法是一样的,如onCreate()、onDestroy()等,并且无论是启动或是绑定多次同一个服务,onCreate()、onDestroy()等这些共用的生命周期方法也仅被调用一次。它们唯一的区别就是当使用startService()启动服务的时候,回调的是onStatrCommand()方法,而使用bindService()绑定服务的时候,回调的是onBind()方法,并且解除绑定的时候还会回调onUnbind()方法。
下面详细解说一下启动服务和绑定服务的生命周期:
- 启动服务:如果一个Service被Android组件调用startService()方法启动,那么不管这个Service对象是否使用bindService()方法被访问者绑定过,该Service都会在后台运行。因为Android系统只会为一个Service服务创建一个实例,所以无论启动几次,onCreate()方法仅执行一次,但是每次启动都会执行onStartCommand()方法,一旦服务启动,不管访问者是否被销毁,服务将一直执行下去,除非被调用stopService()方法或者服务自身的stopSelf()方法,当然系统资源不足的时候,也有可能回收结束服务回收资源。
- 绑定服务:如果一个Service被某个Android组件调用bindService()方法绑定服务,同样因为一个Service只会被创建一个实例,所以不管bindService()被不同的组件调用几次,onCreate()方法都只被回调一次,转而还会执行onBind()方法进行Binder对象的绑定。在绑定连接被建立后,Service将一直运行下去,除非宿主调用unbindService()方法断开连接或者之前的宿主被销毁了,这个时候系统会检测这个Service是否存在宿主绑定,当宿主绑定的数量为0的时候,系统将会自动停止服务,对应的onDestroy()将被回调。
正如上面提到的,Android系统仅会为一个Service创建一个实例,所以不管是使用启动服务或是绑定服务,都操作的是同一个Service实例。但是如果两种服务运行方式均被调用,那么绑定服务将会转为启动服务运行,这时就算之前绑定的宿主被销毁了,也不会影响服务的运行,而启动服务并不会因为有宿主调用了bindService()方法而把原本的启动服务转为绑定服务,但是还是会与宿主产生绑定,但这时即使宿主解除绑定后,服务依然按启动服务的生命周期在后台运行,直到有Context调用了stopService()或是服务本身调用了stopSelf()方法才会真正销毁服务。这样理解感觉启动服务的优先级要比绑定服务高,当然不管哪种情况,被系统回收的资源不在此讨论的范围内。
所以综上所述,对于一个既使用startService()启动又使用bindService()绑定的服务,除非这个服务的两条生命周期均完结,否则不会被销毁。也就是说,在不考虑系统在资源不足的时候,主动回收资源销毁服务的情况下,使用startService()启动的服务,必须使用stopService()或是服务本身的stopSelf()停止服务,使用bindService()绑定的服务,必须使用unbindService()或是销毁宿主来解除绑定,否则服务一直运行。
在实际项目中,经常会使用开始服务于绑定服务混合使用,这样既保证了一个有效的服务在后台长期运行,又可以在需要的时候通过bindService()绑定服务,从而与服务进行交互。
一个Service不管是被启动或是被绑定,默认是运行在后台的,但是有一种特殊的服务叫后台服务,它是一种能被用户意识到它存在的服务,因此系统默认是不会自动销毁它的,但是必须提供一个状态栏通知,Notification,在通知栏放置一个持续的标题,所以这个通知是不能被忽略的,除非服务被停止或从前台删除。关于通知栏的内容,可以查看另外一篇博客:Android--通知之Notification。
这类服务主要用于一些需要用户能意识到它在后台运行,并且随时可以操作的业务,如音乐播放器,设置为前台服务,使用一个Notification显示在通知栏,可以试用户切歌或是暂停之类的。
前台服务与普通服务的定义规则是一样的,也是需要继承Service,这里没有区别,唯一的区别是在服务里需要使用Service.startFroeground()方法设置当前服务为一个前台服务,并为其制定Notification。下面是startForeground()的完整签名。
public final void startForeground(int id,Notification notification)
其中的参数id是一个唯一标识通知的整数,但是这里注意这个整数一定不能为0,notification为前台服务的通知,并且这个notification对象只需要使用startForeground()方法设置即可,无需像普通通知一样使用NotificationManager对象操作通知。
前台服务可以通过调用stopService(boolean bool)来使当前服务退出前台,但是并不会停止服务,传递false即可。
有一点需要声明一下,startForeground()需要在Android2.0之后的版本才生效,在这之前的版本使用setForeground()来设置前台服务,并且需要NotificationManager对象来管理通知,但是现在市面上的设备基本上已经很少有2.0或一下的设备了,所以也不用太在意。
前台服务--示例
下面通过一个示例来演示一个简单的前台服务,这个前台服务展示为一个通知,并且点击通知的时候会开启一个新的Activity
ForegroundService.java
1 package cn.bgxt.servicedemohigh; 2 3 import android.app.Notification; 4 import android.app.NotificationManager; 5 import android.app.PendingIntent; 6 import android.app.Service; 7 import android.content.Context; 8 import android.content.Intent; 9 import android.os.IBinder; 10 import android.support.v4.app.NotificationCompat; 11 12 public class ForegroundSer extends Service { 13 private Notification notification; 14 @Override 15 public IBinder onBind(Intent intent) { 16 return null; 17 } 18 19 @Override 20 public void onCreate() { 21 super.onCreate(); 22 // 声明一个通知,并对其进行属性设置 23 NotificationCompat.Builder mBuilder=new NotificationCompat.Builder(ForegroundSer.this) 24 .setSmallIcon(R.drawable.ic_launcher) 25 .setContentTitle("Foreground Service") 26 .setContentText("Foreground Service Started."); 27 // 声明一个Intent,用于设置点击通知后开启的Activity 28 Intent resuliIntent=new Intent(ForegroundSer.this, IntentSerActivity.class); 29 PendingIntent resultPendingIntent=PendingIntent.getActivity(ForegroundSer.this, 0, resuliIntent, PendingIntent.FLAG_CANCEL_CURRENT); 30 mBuilder.setContentIntent(resultPendingIntent); 31 notification=mBuilder.build(); 32 // 把当前服务设定为前台服务,并指定显示的通知。 33 startForeground(1,notification); 34 } 35 36 @Override 37 public void onDestroy() { 38 super.onDestroy(); 39 // 在服务销毁的时候,使当前服务推出前台,并销毁显示的通知 40 stopForeground(false); 41 } 42 }
控制Service的Activity,与正常启动服务无异。
1 public class ForegroundActivity extends Activity { 2 private Button btnForeStart,btnForeStop; 3 private Intent service; 4 @Override 5 protected void onCreate(Bundle savedInstanceState) { 6 // TODO Auto-generated method stub 7 super.onCreate(savedInstanceState); 8 setContentView(R.layout.layout_foreground); 9 btnForeStart=(Button)findViewById(R.id.btnForeStart); 10 btnForeStop=(Button)findViewById(R.id.btnForeStop); 11 service=new Intent(ForegroundActivity.this, ForegroundSer.class); 12 btnForeStart.setOnClickListener(new View.OnClickListener() { 13 @Override 14 public void onClick(View v) { 15 // 开始服务 16 startService(service); 17 } 18 }); 19 btnForeStop.setOnClickListener(new View.OnClickListener() { 20 @Override 21 public void onClick(View v) { 22 // 停止服务 23 stopService(service); 24 } 25 }); 26 } 27 }
显示就是一个普通的通知栏,但是它代表的是一个前台服务:
众所周知,Android系统会在系统资源不足的时候回收系统资源,如CPU、内存。这个时候就会强制销毁一些优先级不高的后台组件,所以很可能用户启动的服务在还没有完成既定的业务的时候就被系统给回收了,这个时候需要制定当Service被意外销毁的时候,如何处理接下来的事情。
使用bindService()绑定的Service是与宿主组件相关的,所有如果宿主组件没有被系统回收销毁,这个服务除非宿主主动解除绑定,直到这个服务没有宿主绑定,即才会被销毁,这种情况只需要关心与它绑定的组件的是否被销毁即可。而对于使用startService()开启的服务而言,服务一旦开启,将与宿主无关,所以除了宿主调用stopService()或者服务自己调用stopSelf()外,它很可能在系统资源不足的时候被回收,一般如果一些比较重要的任务,比如说下载文件、发送备份数据等一些操作,是不允许服务被系统停止的,这里就需要设置服务被系统销毁后,如何处理的问题。
服务被系统销毁后,如何继续服务,可以使用Service.onStartCommand()方法的返回值设定,这个方法必须返回一个整型,用于设定系统在销毁服务后如何处理,这些返回的整型被系统封装成了常量,一般常用的有:
- START_NOT_STICKY:在系统销毁服务后,不重新创建服务,除非有额外的Intent启动服务。
- START_STICKY:在系统销毁服务后,重新创建服务和调用onStartCommand(),但会依照一个空的Intent对象执行任务,就如仅开始服务,但不执行命令,等待服务的继续工作。
- START_REDELIVER_INTENT:在系统销毁服务后,重新创建和调用onStartCommand()方法,依据之前启动它的Intent对象开启服务,进行之前未执行完的任务,如下载文件。
不被销毁的服务
如果想使一个Service对象常驻后台运行,在任何时候都不被销毁,这里涉及的内容比较多,有时间再细细的写。这里主要提一下思路,为了保持一个Service示例常驻后台,需要考虑几个问题:
- 开机启动:Android系统开启的时候会发送一个action为android.intent.action.BOOT_COMPLETED的广播,只需要一个广播接受者来接受这个Action,然后在其中启动服务即可。
- 意外停止:对于意外停止的服务,可以在服务的onDestory()方法中重新启动服务,这样刚销毁又重新启动。
- 意外销毁:当Service被意外销毁的时候,会发布一个action为android.intent.action.PACKAGE_RESTARTED的广播,只需监听这个广播,在其中重新启动服务即可。
- 系统回收:系统在资源不足的情况下会回收资源,但是也是有一定的规则的,回收的资源必定是优先级低的资源,所以提高Service的优先级,也可以保证一定的常驻系统后台,服务的优先级可以在清单文件中,配置<service/>节点下的<intent-filter/>节点的时候增加android:priority属性,将其的数值设定的比较高,此处数值越高,优先级越高,上线为1000。
上面介绍的思路基本上已经涵盖了让Service不被销毁的主要内容,还有一种极端的情况,可以使服务一旦启动,将不会被销毁,那在配置服务的清单文件的时候,在<application/>节点中增加android:persistent属性,并将其设置为true。但是这是最极端的情况,也不推荐使用,因为如果系统中安装了大量常驻组件,将会影响系统的使用。