总所周知,Service是四大组件之一,它没有用户操作界面,运行于系统之中不易被用户发现,它不是一个单独的进程也不是一个线程,那我们可以用其来做些什么呢。
1. 应用内或者应用间的数据通信
2. 执行长时间运行的操作,例如开发监控之类的东西
这两点就是服务的用途。
一. 生命周期
二 . 服务的启动方式
既然是四大组件,那我们就需要在清单文件中注册它,接下来就是如何使用,服务的启动分为两种启动方式:
1. 启动型 startService(可通过stopService停止服务)
当我们重复启动服务时,发现服务只执行一次的onCreate和onDestroy方法,不会重复的去启动只会调用onStartCommond,我们可以在其中拿到activity传递过来的数据。启动服务的一个特点是其生命周期不依赖启动它的宿主Activity,当我们跳转到其它acivity时它仍然继续在运行当中。那是不是我们启动了服务就万事大吉了呢,答案时否定的,当我们启动服务后,如果Service长期运行在后台,就是我们切换到其它应用或者锁屏,发现过一会Service就被系统杀死了。原来从Android8.0开始,谷歌粑粑对长时间运行在后台的服务做了限制,看Service的如下源码:
简单解释就是,已启动服务的方式开启的Service,要是Service保持运行,需要调用StartForeground方法,开启一个通知让用户假假的知道这个服务一直在前台运行,这样才能保持服务的一直运行。就像上面的第四行源码所述,如果你开启一个后台服务,
就是使用到其它app,当前的应用进入后台,服务自动也变成后台服务,CPU会认为杀死这个服务不会有什么危害,所以你的服务就被偷偷的干掉了,全然不知。那我该咋整呢,其实很简单,就是
这样你开启的服务就提升了一个档次变成了前台服务,就算应用在后台运行。这样可以确保服务不被体统当成无用的后台服务轻易干掉。有人可能会说我用startForegroundService启动服务不就可以了,亲自验证,这样时无效的,在8.0以上设备必须在服务内调用startForeground才能使Service保持前台服务,不被杀死。
2. 绑定型 bindService(通过unbindService解除绑定)
绑定服务不像启动服务,它的生命周期依赖于宿主,如果activity销毁,服务也会跟着执行unbind,ondestroy自动销毁。
绑定服务需要在actvity销毁时解除绑定,否则会出现ServiceConnectionLeaked泄露。
绑定服务的优势在于,只要宿主在,它就跟着在,不需要像启动服务一样去调用startForeground方法
绑定服务中的onRebind(想要执行它必须重写onUnbind并返回true),如果同一个服务先启动了,接着去绑定服务,再解除绑定,此时不会调用ondestroy因为服务已经先使用启动服务了,之后再去绑定服务就会调用onRebind来绑定服务。相当于启动服务代替了绑定服务的效果,这时就算跳转界面销毁服务也不会依据绑定服务的原则而去销毁服务。
绑定服务会使你的服务和activity建立关联,因为绑定服务需要传入serviceConnection对象
需要注意的是绑定服务会与当前调用bindService的activity绑定,即使传入的intent中使用的是应用的Context,启动它的activty销毁就会使service解绑并销毁(除非该服务已经使用了startService方法启动)。
三. IntentService的应用
IntentService是service的子类,默认会运行在其它线程,可用其去执行一些耗时的操作(不像Service运行在UI线程,无法直接执行耗时操作,需开辟子线程去执行)
IntentService执行完任务后会自动的销毁,连续启动服务的话,onHandleIntent中的任务会依次执行。
当我连续两次点击启动服务后打的日志如上,任务会先后执行两次。
四. AIDL进程间通信
AIDL(Android Interface Definition Language)用于生成可以在两个进程之间通信的应用。
我们先规定提供Service的APP为服务层,调用的APP为客户端。
首先,我们在服务APP项目里面新建一个AIDL文件叫Book,如下
接在在java里也新建一个类叫Book如下,让其实现parcelable接口如下
这三个tag(inout/out/in)的作用稍后见分晓。
接着编写service类如下:
public class AIDLService extends Service {
private final String TAG = "AIDLService";
private List bookList;
private final String CHANNELID = "Notification_ID";//渠道id
private final int NotificationID = 11221;//Notification id
private int inout = 0;
private int in = 0;
private int out = 0;
@Override
public void onCreate() {
super.onCreate();
if (Build.VERSION.SDK_INT >= 26) {
Notification notification = new Notification.Builder(this, CHANNELID).build();
startForeground(NotificationID, notification);
}
initData();
}
private void initData() {
bookList = new ArrayList<>();
Book book = new Book();
book.setName("first book");
Book book2 = new Book();
book2.setName("second book");
bookList.add(book);
bookList.add(book2);
}
//无法获取时检查下项目再claer一下项目
BookControl.Stub stub=new BookControl.Stub() {
@Override
public List getBookList() throws RemoteException {
Log.e(TAG, "书数量:" + bookList.size());
return bookList;
}
@Override
public void addBookInOut(Book book) throws RemoteException {
if (book != null) {
inout++;
book.setName("服务器改了新书的名字 InOut "+inout);
bookList.add(book);
} else {
Log.e(TAG, "接收到了一个空对象 InOut");
}
}
@Override
public void addBookOut(Book book) throws RemoteException {
if (book != null) {
out++;
Log.e(TAG, "客户端传来的书的名字:" + book.getName());
book.setName("服务器改了新书的名字 Out "+out);
bookList.add(book);
} else {
Log.e(TAG, "接收到了一个空对象 Out");
}
}
@Override
public void addBookIn(Book book) throws RemoteException {
if (book != null) {
in++;
book.setName("服务器改了新书的名字 In:"+in);
bookList.add(book);
} else {
Log.e(TAG, "接收到了一个空对象 In");
}
}
};
@Nullable
@Override
public IBinder onBind(Intent intent) {
return stub;
}
@Override
public void onDestroy() {
super.onDestroy();
}
}
往其中加入几本书,供客户端获取,关键在于BookControl.Stub,因为进程间通信起作用的实际上是它,获取不到的话clear一下项目。然后需要注意一下清单文件的service配置,提供隐式action供外部调用
接着去启动服务就ok啦。
下面我们来讲讲客户端,首先需要复制服务端的整个AIDL文件夹到main目录下,然后去把book.java也同样复制来,包名也要一致,在主activity中去启动服务器提供的service:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button bindBtn, inOutBtn, inBtn, outBtn, getBtn;
private final String TAG = "MainActivity";
private BookControl bookControl;
private boolean connected;
private List bookList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
bindBtn = findViewById(R.id.btn_bind);
inOutBtn = findViewById(R.id.btn_inout);
inBtn = findViewById(R.id.btn_in);
outBtn = findViewById(R.id.btn_out);
getBtn = findViewById(R.id.btn_get);
bindBtn.setOnClickListener(this);
inOutBtn.setOnClickListener(this);
inBtn.setOnClickListener(this);
outBtn.setOnClickListener(this);
getBtn.setOnClickListener(this);
}
private ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
bookControl = BookControl.Stub.asInterface(service);
connected = true;
}
@Override
public void onServiceDisconnected(ComponentName name) {
connected = false;
}
};
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_bind:
Intent intent = new Intent();
intent.setPackage("com.hdy.bookservice");//服务器报名
intent.setAction("com.hdy.aidlservice.action");
bindService(intent, serviceConnection, BIND_AUTO_CREATE);
break;
case R.id.btn_get:
if (connected) {
try {
bookList = bookControl.getBookList();
for (Book book : bookList) {
Log.i(TAG, book.toString());
}
} catch (RemoteException e) {
e.printStackTrace();
}
}
break;
case R.id.btn_inout:
if (connected) {
Book book = new Book();
book.setName("inout book");
try {
bookControl.addBookInOut(book);
} catch (RemoteException e) {
e.printStackTrace();
}
}
break;
case R.id.btn_in:
if (connected) {
Book book = new Book();
book.setName("in book");
try {
bookControl.addBookIn(book);
} catch (RemoteException e) {
e.printStackTrace();
}
}
break;
case R.id.btn_out:
if (connected) {
Book book = new Book();
book.setName("out book");
try {
bookControl.addBookOut(book);
} catch (RemoteException e) {
e.printStackTrace();
}
}
break;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
}
}
重点在于bookControl的获取。四个按钮分别是获取书名,以三种方式(inout/out/in)向服务器添加书。
现在开始我们跑一下项目看看日志。(红色服务器,白色客户端)
1.获取书名日志如下:
获取到了服务APP提供的书名
2.就是前面所说的inout这个tag的作用
客户端点击一下加书然后服务器获取书名。连续两次后日志如上
发现服务器先获取到了客户端传来的书,并打印出来书名,之后看客户端获取书名发现比原来多了一本,服务其显示的书数量也变成了三,说明intout标签可以实现服务器和客户端双向通信。
3.分析int标签的作用
观察日志,发现其效果竟然和inout标签的日志出来是一样的,并没有是只准服务器收客户端的消息而不能更改获取到的消息。
4.分析outt标签的作用
发现客户端向服务器传来的对象是有了,可是获取对象的属性却获取不到,说明只准服务器更改消息发给客户端,客户端传来的消息对象属性获取不到,有点只出不进的感觉。
AIDL部分参考大神的博客如下:https://www.jianshu.com/p/29999c1a93cd
只是我得出的结论有点不一样。
四. JobScheduler浅析
JobScheduler作为Android5.0开始出现的新API,被理解为定时任务,即系统在满足一定条件时候才执行的一种服务。
那需要完成一个任务需要分为三部分,打起精神开干:
1.获取JobScheduler的实例,这个简单一句话分分钟搞定
JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
2.完成JobInfo的配置,就是完成这个任务的启动条件配置。
谷歌粑粑为我们内置了几种常用的条件
1)设备充电 setRequiresCharging(true)设置 true待机false不待机
2)设备待机闲置 setRequiresDeviceIdle(false) 设置 true待机闲置false不待机闲置
3)连接网络 setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
其中包括5种网络状态:NETWORK_TYPE_NONE 默认状态
NETWORK_TYPE_ANY 联网状态
NETWORK_TYPE_UNMETERED 连接WIFI状态(手机网络不行)
NETWORK_TYPE_NOT_ROAMING 漫游状态,基本很少出现这种,除非出省然后自己是省内流量
NETWORK_TYPE_CELLULAR 手机网络(WIFI无效)
插曲:setOverrideDeadline(5000)设置在条件不满足时最大延时时间,只要时间一到,就算条件不满足也会执行一次我们的任务。执行完之后任务如果被设置成会重启动,在条件满足后依旧会执行我们的任务。
JobInfo jobInfo = new JobInfo.Builder(123456, new ComponentName(getPackageName(), MyJobService.class.getName()))
.setMinimumLatency(1000)//任务最少延迟
.setOverrideDeadline(5000)//任务deadline,当到期没达到指定条件也会开始执行
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)// 网络条件,默认值NETWORK_TYPE_NONE
// .setPeriodic(100)
.setRequiresCharging(true)//是否充电
.setRequiresDeviceIdle(false)//是否闲置
//initialBackoffMillis自定义的时间如果小于10秒,将默认设置成为10秒
.setBackoffCriteria(3000,JobInfo.BACKOFF_POLICY_LINEAR) //设置退避/重试策略
.build();
如果任务设置了重启需要注意下这个方法setBackoffCriteria,系统内置了最小的启动时间间隔10秒,我们设置的第一个参数小于10秒将被替换成10秒。第二个参数有两种情况,请看源码如下
/**
* Linearly back-off a failed job. See
* {@link android.app.job.JobInfo.Builder#setBackoffCriteria(long, int)}
* retry_time(current_time, num_failures) =
* current_time + initial_backoff_millis * num_failures, num_failures >= 1
*/
public static final int BACKOFF_POLICY_LINEAR = 0;
/**
* Exponentially back-off a failed job. See
* {@link android.app.job.JobInfo.Builder#setBackoffCriteria(long, int)}
*
* retry_time(current_time, num_failures) =
* current_time + initial_backoff_millis * 2 ^ (num_failures - 1), num_failures >= 1
*/
public static final int BACKOFF_POLICY_EXPONENTIAL = 1;
第一种时线性增加下次启动任务的时间,每次启动都会延时10秒,20秒。。。一直加,(⊙﹏⊙)。
第二种更夸张直接是以指数的形式加延时时间的。(使用java反射或许可以更改系统内置的静态常量10秒起始值,有去验证的小伙伴可以给我留言,一起学习)
3.设置需要执行的任务
这是重点,毕竟搞了那么多就是为了执行任务,我们需要自定义一个类继承JobService,然后重写里面的onStartJob和onstopJob,
并配置清单文件:
我先写好了一个:
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class MyJobService extends JobService {
private static final String TAG = MyJobService.class.getSimpleName();
@Override
public void onCreate() {
super.onCreate();
Log.i(TAG, "onCreate");
}
//此方法不会执行
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "onStartCommand");
return super.onStartCommand(intent, flags, startId);
}
/**
* 运行于UI线程
*
* @return True if your service needs to process the work (on a separate thread). False if
* there's no more work to be done for this job.
* true 情况1:任务会保持执行状态,即使任务已经完成,需要手动调用jobFinished通知系统任务完成,才会执行 onStopJob》onDestroy
* 情况2:就是任务的条件已经不满足了,例如本来要求联网的突然断开就会达到类似情况1手动调用jobFinished效果结束任务
* false 任务处理完毕,直接调用onDestroy,不执行onStopJob
*/
@Override
public boolean onStartJob(JobParameters params) {
Log.e(TAG, "onStartJob:"+" currentThread:"+Thread.currentThread().getName());
Toast.makeText(this, "onStartJob", Toast.LENGTH_SHORT).show();
jobFinished(params,true);//返回true去再次启动任务,前提条件是onStartJob返回true,否则设置无效。我将它理解为手动调用停止任务
return true;
}
/**
* 执行条件: 1.需要onStart返回true 2.未调用jobFinished方法 缺一不可
* 一旦执行系统将释放这个job持有的资源
* @return True to indicate to the JobManager whether you'd like to reschedule this job based
* on the retry criteria provided at job creation-time. False to drop the job. Regardless of
* the value returned, your job must stop executing
* true 是否从新执行任务 false:停止任务
*/
@Override
public boolean onStopJob(JobParameters params) {
Toast.makeText(this, "onStopJob", Toast.LENGTH_LONG).show();
Log.i(TAG, "onStopJob");
return true;
}
@Override
public void onDestroy() {
Log.i(TAG, "onDestroy");
super.onDestroy();
}
}
onstartJob负责执行我们的任务,运行于主线程。开始分析其几种情况
1)直接返回false 任务处理完毕,直接调用onDestroy,不执行onStopJob,执行jobFinished也无效果
2)返回true 2.1 此时onstopjob登场,这是分为两种情况,
2.1.1 在onstopjob里面返回true,条件一旦中断,会立刻调用onstopjob,由于此时onstopjob返回true,任务会被替换成循环的任务,在条件满足时会被再次启动,然后一直保持处理任务的状态。
2.1.2 如果在onstopjob返回false,对任务没影响,会一直都在运行,条件中断任务自动onDestroy,当条件满足也不会再次启动,标志任务已经完成。
2.2 如果在onstartJob里调用了jobFinished(params,true),那2.1里面的设置将全部失效
2.2.1 jobFinished里面返回了true,告诉系统任务完成并需要在一定时间后再次启动,不论条件是否中断,中断后条件满足就会再次启动任务,变成了一个循环任务。时间间隔会从10秒一直线性增加,20秒30秒40秒。。。中断期间的时间也会被算在这个时间间隔中,如果中断时间大于这个间隔时间,条件一满足就执行任务。
那有没办法结束呢,有,手动调用jobScheduler.cancelAll()或者jobScheduler.cancel(任务id);
2.2.2 jobFinished里面返回了false,任务被执行一次就会结束。
总结,调用onstartJob返回false适用于执行一次的任务(强调执行一次任务)
调用onstartJob返回true并且onstopJob返回true时为的是保持任务的运行状态,就算条件终止待其条件恢复会再次启动任务保持任务的运行。(强调任务的一直运行)
调用onstartJob返回true,并且调用jobFinished(params,true)使任务循环运行(强调任务循环运行)
4.启动任务
jobScheduler.schedule(jobInfo);
jobScheduler的讲解到此为止,本文使用的条件限制为网络连接与否,有兴趣的小伙伴可以组合条件去试一下。
关于Android服务的相关知识就到此结束。
项目已上传到GitHub:https://github.com/Somewereb/AndroidServiceExample