对于这章来说,就是来学习如何运用service来提供后台的服务
这边我们来完成一个功能,就是我们允许应用在后台上下载新的搜索结果,一旦有了新结果,那么用户就可以在状态栏上看到通知消息
一.创建IntentService
我们先继承IntentService来编写它的一个子类,它就是用来轮询搜索结果的服务
public class PollService extends IntentService{
private static final String TAG = "PollService";
//无论谁想使用它都应该去使用它
public static Intent newIntent(Context) {
return new Intent(context,PollService.class);
}
public PollService(){
super(TAG);
}
//它可以用来响应intent
protected void onHandleIntent(Intent intent) {
Log.i(TAG,"dd" + intent);
}
}
服务的intent也是叫坐命令,这个IntentService服务能按顺序执行队列中的命令,Intent启动后就会触发一个后台线程,然后将命令放入一个队列中,随后并针对每一条命令在后台线程上调用onHandleIntent(Intent)方法,新命令总是放在队尾上,执行完队列中的全部命令后服务就会随之停止和销毁
(1)必须在AndroidManifest文件中去声明它
<service android:name = ".PollService"/>
(2)添加服务启动代码,在PhotoGalleryFragment中的onCreate()方法中添加
Intent i = PollService.newIntent(getActivity());
getActivity().startService(i);
对于服务来说,服务就是Android应用的后台,用户不关心后台发生的一切,即使前台关闭,activity消失很久,后台服务也可以持续工作
二.后台连接网络安全
服务会在后台轮询Flickr网站,下面为了后台网络连接的安全性,我们需要进一步完善代码,Android有也有关闭后台应用连接网络的功能,在后台连接网络的时候需要使用ConnectivityManager确认网络连接是不是可以用
下面添加代码
protected void onHandleIntent(Intent intent) {
if(!isNetwordAvailableAndConnected()){
return;
}
Log.i(TAG,"dd" + intent);
}
}
private boolean isNetworkAvailableAndConnected(){
ConnectivityManager cm = (ConnectivityManager)getSystemService(CONNECTIVITY_SERVICE);
boolean isNetwordAvailable = cm.getActiveNetwordInfo() !=null;
boolean isNetwordConnected = isNetwordAvailable&&cm.getActiveNetwordInfo().isConnect();
return isNetwordConected;
}
检测网络是不是可以使用的逻辑就在isNetwordAvailableAndConnected()方法中,使用后台数据设置关闭后台数据下载后,所有的后台服务就无法联网了,如果后台服务可以使用网络,那么它就会得到一个代表当前网络连接的android.net.NetworkInfo实例,然后还要调用Network.isConnent()方法检测当前网络是不是已经连接了,如果找不到网络或是设备没有连上网,那么onHandleIntent()方法就会直接返回
注意:要使用getActiveNetwordInfo()方法,那么就还要在manifest配置文件中获取ACCESS_NETWORK_STATE权限
<uses-permission android:name = "android.permission.ACCESS_NETWORK_STATE"/>
三.查找最新的返回结果
后台服务会一直查看最新的返回结果,因此它要知道最近一次的获取结果,使用SharedPreferences保存结果值是最好的选择
private static final String PREF_LASE_RESULT_ID = "lastResultId";
public static String getLastResultId(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getString(PREF_LAST_RESULT_ID,null);
}
public static void setLastResultId(Context context,String lastResultId) {
PreferenceManager.getDefaultSharePreferences(context).edit().putString(PREF_LAST_RESULT_ID,lastResultId).apply();
}
接下来就是来完成代码了,
1.从默认的SharedPreferences中获取当前的查询结果和上一次结果的Id
2.使用FlickrFetchr类中获取结果集
3.如果有结果返回就捉取第一条结果
4.确认是不是不同于上一次的结果Id
5.将第一条结果存入SharedPreference
现在就回到PollService类中
//它可以用来响应intent
protected void onHandleIntent(Intent intent) {
String query = QueryPreferences.getStoredQuery(this);
String lastResultId = QueryPreferences.getLastResultID(this);
List<GalleryItem> items;
if(query == null) {
items = new FlickerFetchr().fetchRecentPhotos();
}else{
items = new FlickrFetchr().searchPhotos(query);
}
if(items.size() == 0) {
rerurn;
}
String resultId = items.get(0).getId();
if(resultId.equalts(lastResultId)) {
Log.t(TAG,"old photto" + resultId);
} else{
Log.i(TAG,"new phoot" + resultId);
}
QueryPreferences.setLastResultId(this,reulltId);
}
此时如果运行这个应用,那么就会发现应用首先获取了最新的结果,如果选择上次的搜索查询,提交搜索后有可能就会看到和上次同样的结果
四.使用AlarmManager延迟运行服务
在没有activity运行的情况下,为了在后台运行服务,那么我们就需要想方法去启动它,比如设置一个5分钟间隔的定时器
AlarmManager也可以发送Intent的系统服务,使用PendingIntent,使用PendingIntent打包一个intent,然后将其发送给系统的其他部件,如AlarmManager
在下面的代码实现一个setServiceAlarm(Context,boolean)方法
在PollService中添加
private static final long POLL_INTERVAL_MS = TimeUnit.MIMUTES.toMillis(1);
public static void setServiceAlarm(Context context,boolean isOn) {
Intent i = PollService.newIntent (context);
PendingIntent pi = PendingIntent.getService(context,0,i,0);//创建一个启动PollService的PendingIntent,后面的方法打包了一个Context.startService(Intent)的方法的调用
AlarmManager alarmManager (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
if(isOn) {
alarmManager.setPepeating(AlarmManager.ELAPSED_REALTIME,SystemClock.elapsedRealtime(),POLL_INTERVAL_MS,pi);
}else{
alarmManager.cancel(pi);
pi.cancel();
}
上面代码getService()的四个参数分别是:一个用来发送intent的Context,一个区分PendingIntent来源的请求代码,一个待发送的Intent对象,和一组用来决定如何创建PendingIntent来源的标记符
setRepeating()的四个参数:一个描述定时器的时间基准的常量,定时器启动的时间,定时器循环的时间间隔以及一个到时的发送的PendingIntent
AlarmManager.ELAPSED_REALTIME是基准的时间,这表明我们是以SystemClock.elapsedRealtime()走过的时间来确定何时启动,也就是过了3分钟再启动的意思,而对于AlarmManager.RTC的意思就是到了固定的时间就启动,也就是淡了下午三点启动的意思,
取消定时器可以调用AlarmManager.cancel(PendingIntent)的方法,通常也需要去取消PendingIntent,取消PendingIntent也有助于跟踪定时器的状态
(1)现在再PhotoGalleryFragment的onCreate()方法中把代码换成下面的代码
PollService.setServiceAlarm(getActivity(),true);
现在,我们运行应用然后退出应用,但是我们会发现后台服务过了60秒后还会执行的,即使进程结束了,AlarmManager还是会不断地发送intent去启动PollService服务
对于定时器来说,它通常是从前端的fragment或是其他控制层代码中去启停它的
对于setRepeating()方法可以设置一个重复的定时器,但不是太精确的,不精确就意味安卓可以自己去调控定时器的启动时间,如果想要精确的时间定位就需要使用到Alarm.setWindow()
(2)PendingIntent
Pending是一种token对象,调用getService方法时,我们告诉操作系统“我们需要使用startService(Intent)方法来发送这个Intent,随后调用PendingIntent的send()方法时,操作系统就会按照要求发送原来封装的intent”,
(3)使用PendingIntent管理定时器,一个PendingIntent只能登记一个定时器,这也就是isOn值为false时,setServiceAlarm()方法的原理:首先先调用AlarmManager.cancel(PendingIntent)方法来撤销PendingIntent的定时器,然后撤销PendingIntent
从上面可以知道撤销定时器的时候也会撤销PendingIntent,那么就可以检查PendingIntent是不是存在来确定定时器激活与否,具体代码实现是通过传入PendingIntent.FLAG_NO_CREATE标志给Pending.Intent.getService()方法,该标记表示如果PendingIntent不存在那么就会返回null,而不是创建它
创建一个判断定时器是不是开启的方法
public static boolean isServiceAlarmOn(Context context) {
Intent i = PollService.newIntent(context);
PendingIntent pi = PendingIntent.getService(context,0,i,PendingIntent.FLAG_NO_CREATE);
return pi!=null;
}
(4)控制定时器
从上面的代码中,我们可以判断定时器的启停,所以,我们这边就可以在图形界面中去控制其开关
1.首先需要添加另一个菜单项到menu/fragment_photo_gallery.xml
<item android:id="@id/menu_item_toggle_polling"
android:title="@string/start_polling"
app:showAsAction = "ifRoom"/>
然后添加一些字符串资源,一个用于启动polling,一个用于停止polling
2.菜单项的转换
在onOptionsItemSelected方法中添加选项
case R.id.menu_item_toggle_polling:
boolean shouldStartAlarm = !PollService.isServiceAlarmsOn(getActivity());
PollService.setServiceAlarm(getActivity(),shounldStartAlarm);
return true;
此时虽然可以代码上控制定时器的启动与暂停,但是界面上的变换还是没有显示出来,那么为了显示出界面的变化,那么就可以在onCreateOptionsMenu方法中添加下面的代码:
MenuItem toggleItem = menu.findItem(R.id.menu_item_toggle_polling);
if(PollService.isServiceAlarmOn(getActivity())) {
toggleItem.setTitle(R.string.stop_polling)
}else {
toggleItem.setTitle(R.string.start_polling);
}
然后在对应的启动和暂停定时器的那个选选项添加getActivity().invalidateOptionsMenu()方法
case R.id.menu_item_toggle_polling:
boolean shouldStartAlarm = !PollService.isServiceAlarmsOn(getActivity());
PollService.setServiceAlarm(getActivity(),shounldStartAlarm);
getActivity().invalidateOptionsMenu();
return true;
现在选项菜单的界面也可以转换了,但是后台服务要真正有用的话还应该需要有通知的功能
五.通知信息
如果服务需要与用户沟通那么,通知信息会是一个不错的选择,通知信息是显示在通知抽屉上的消息条目,用户可以向下滑动它读取,想要发送通信的信息,首先就需要去创建Notification对象,完成Notification对象创建后,就可以调用NotificationManager系统服务的notify(int,Notification)方法去发送它
(1)首先需要在PhotoGalleryActivity中添加一个newIntent()静态方法,如代码清单一样,该静态方法会返回一个用来启动PhotoGalleryActivity的Intent的实例(最后PollService会调用这个方法,把返回结果封装在一个PendingIntent中,然后设置给通知消息)
public static Intent newIntent(Context context) {
return new Intent(context,PhotoGalleryActivity.class);
}
(2)一旦有了结果,就让PollService通知用户,也就是创建一个Notification对象,然后调用NotificatonManager.notify(int,Notification)方法
Resources resources = getResources();//建立一个资源夹
Intent i = PhotoGalleryActivity.newIntent(this);
PendingIntent pi = PendingIntent.getActivity(this,0,i,0);
Notification notification = new NotificationCompat.Builder(this)
.setTicker(resources.getString(R.string.new_pictures_title))
.setSmallIcon(android.R.drawable.ic_menu_report_image);//设置图标
.setContentTitle()//设置标题
.setContextText()//设置文字显示区域
.setContentIntent(pi)//设置相应的intent
.setAutoCncel(true)
.build();
NotificationManagerCompat notificationManager = NotificationManager.from(this);
notificationManager.notify(0,nitification);//传入的整数参数是通知消息的标识符,这在整个应该应该是唯一的,如果使用统一Id发送两条消息,那么第二条消息就会去覆盖第一条的消息
在大多数服务中,推荐使用IntentService,但是IntentService模式不一定适合所有的架构,所以下面就需要来学习更多的服务
六.服务的了解
与activity一样,服务是一个有生命周期回调方法的应用组件,这些回调方法同样适合在主线程的UI线程上运行,原生的服务不能在后台线程上运行,但是IntentService却是可以,这也就是我们推荐使用IntentService的原因
(1)服务的生命周期
1.如果是startService(intent)方法启动的服务,其生命周期简单,只有三种生命周期
onCreate():创建服务时调用
2.onStartCommand(Intent,int,int):每次组件通过startService(Intent)方法启动服务的时候就调用一次,它有两个整数参数,一个时标识符集,一个是启动Id,标识符集用来表示当前intent发送究竟是一次重新发送还是一次没有成功过的intent,每次调用onStartCommand(Intent,int,int)方法,启动的Id都不太一样
3.onDestroy():服务停止时会调用这个方法,而服务停止的方式取决于服务的类型
服务的类型由onStartCommand()方法的返回值来决定,有下面的三种服务
Service.START_NOT_STICKY,//服务说没了就没了
Service.START_REDELIVER//当资源不再吃紧时,尝试再次启动服务
Service.START_STICKY
(2)non-sticky服务
IntentService是一种non-sticky服务,non-sticky服务在服务自己认为已经完成任务时停止,为获得non-sticky服务,应该返回START_NOT_STICKY或是START_REDELIVER
通过stopSelf()或是stopSelf(int)方法,我们告诉Android任务已经完成了,前者是个无条件的方法,不管onStartCommand()方法调用多少次,这个方法都会成功停止服务
stopSeft(int)是一个有条件的方法,该方法需要来自onStartCommand()方法启动的ID,只有结束到最新的ID该方法才会停止服务(这也是IntentService的后台原理)
(3)sticky服务
这个服务会持续运行,除非外部组件调用Context.stopService(Intent)方法让它停止,应该返回START_STICKY
如果由于某种需要去终止服务,那么就可以传入一个null intent给onStartCommand()方法重启服务,这种服务适合长时间运行的服务,像音乐播放器,
(4)绑定服务
除了上面的各类服务后,也可以使用bindService(Intent,ServiceConnection,int)方法来绑定一个服务,由此获得直接调用绑定服务方法的能力,ServiceConnection是一个代表服务绑定的一个对象,负责接受全部绑定回调方法
在fragment中,绑定代码可能如下面
private ServiceConnection mServiceConnection = new ServiceConnection(){
public void onServiceConnected(ComponentName className,IBinder service) {
MyBinder binder = (MyBinder)service;
}
}
public void onServiceDisconnected(ComponentName className) {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent i = new Intent(getActivity(),MyService.class);
getActivity().bindService(i, mServiceConnection,0);
}
}
public void onDestroy(){
super.onDestroy();
getActivity().unbindService(mServiceConnection);
}
对于服务来说,绑定引入了另外两个生命周期方法
onBind(Intent)方法:每次绑定服务时调用,返回来自ServiceConnention.onServiceConnected(Component,IBinder)方法中的IBinder对象
onUnbind(Intent)方法:服务绑定终止时调用
1.本地服务
如果服务是本地服务的话,MyBinder很有可能就是本地进程中的一个简单的Java对象,通常MyBinder用于提供一个句柄,以便可以直接调用服务方法
private class MyBinder extends IBinder {
public class MyService getService(){
return MyService.this;
}
}
public void onBind(Intent intent) {
return new MyBInder();
}
这是安卓系统中唯一支持组件间直接对话的地方
2.远程服务绑定
绑定更适合于远程服务绑定,因为它赋予了其他进程中应用调用服务的方法的能力,创建远程绑定服务可以查看AIDL和Messager类的细节
七.了解JobScheduler和JobService
在上面我们已经了解了如何让AlarmManager,IntentServicer和PendingIntent相互配合,创建周期性的后台服务,但是一个完全可用的后台服务需要(1)计划一个周期的任务(2)检查周期任务的运行状态(3)检查网络是不是可以用,这样完成下来需要有很多的工作量。所以为了更好地实现后台服务,Android引入了JobScheduler的api,它会:发现没有网络的时候它能禁止服务启动,如果请求失败或网络连接受限,它能提供稍后重试机制,还有许多功能,下面是使用JobScheduler的原理
(1)首先创建一个处理任务的服务(使用JobService子类)
public class PollService extends JobService{
public boolean onStartJob(JobParameters params) {
return false;
}
public boolean onStartJob(JobParameters params) {
return false;
}
}
android准备好执行任务的时候,服务就会启动,此时主线程就会收到onStartJob()方法的调用,该方法返回false表示:交代的任务我已经全力去做,已经做完了,返回true 表明:任务收到,正在做还没做完
与IntentService不同,JobService需要你单开新线程,这就可以使用AsyncTask按下面的方式去创建新的线程
private PollTask mCurrentTask;
public boolean onStartJob(JobParameters params) {
mCurrentTask = new PollTask();
mCurrentTask.execute(params);
return true;
}
private class PollTask extends AsyncTask<JobParameters,Void,Void> {
protected Void doInBackground(JobParameters...params) {
JobParameter jobParams = params[0];
jobFinished(jobParams,false);//任务结束后就会运用这个去通知结果,不过如果传入true的话那么就是等于说:这次事情做不完,请在下一次某个时间里面继续
return null;
}
}
任务运行时也可能会收到onStopJob()的调用,表明当前任务需要中断,例如某个服务需要WiFi但是现在离开了WiFi的地方,所以此时就会中断任务,那么此时的onStopJob()方法就会使用
//调用它就是意味着服务马上需要停止,不要有任何其他的想法
public boolean onStopJob(JobParameters params) {
if(mCurrentTask !=null) {
mCurrentTask.cancel(true);
}
return true;//表示任务应该计划在下次继续,返回false表示:不管怎么样,事情就到此结束,不要安排下次了
}
注意:在manifest中配置文件的时候必须导出并为它添加权限
<service
android:name = ".pollService"
android:permisson= "android.permission.BIND_JOB_SERVICE"
android:exported = "true"/>
这里有导出服务,也就是把服务导出来,但是添加的权限只有JobScheduler才能运行它,
一旦我们如同上面一样创建了JobService,那么启动它就会非常迅速了,我们可以使用JobScheduler,看看是不是已经计划好任务了
final int JOB_ID = 1;
JobScheduler scheduler = (JobScheduler)context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
boolean hasBeenScheduler = false;
for(JobInfo jobInfo : scheduler.getAllPendingJobs()) {
if(jobInfo.getId() == JOB_ID) {
hasBeenScheduled = true;
}
下面的代码是创建一个新的JobInfo来说明我们期望的任务的运行时间
JobInfo jobInfo = new JobInfo.Builder(JOB_ID,new ComponentName(context,PollService.class))
.setRequiredNetwordType(JobInfo.NETWORK_TYPE_UNMETERED)
.setPeriodic(1000*60*15)
.setPersisted(true)
.build();
scheduler.schedule(jobInfo);
}