自学安卓编程权威指南(二十三)

对于这章来说,就是来学习如何运用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);

 

 

 

 

}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值