相关文档:
Android服务——service使用浅析(一)
1. IntentService 基本使用
如果直接把耗时线程放到Service中的onStart()
方法中,容易引起**ANR(Application Not Responding)**异常.Service不是一个单独的进程,与应用程序同在一个进程中;Service不是一个线程,避免在Service中进行耗时操作。
我们可以用到之前的多线程技术,在服务每个具体的方法里开启一个子线程,在这里去处理耗时逻辑,则一个标准的逻辑可以写成:
public class MyService extends Service {
...
@override
public int onStartCommand(Intent intent, int flags, int startId) {
new Thread(new Runnable() {
@override
public void run() {
//处理具体逻辑
stopself();//执行完毕自动停止服务
}
}).start();
return super.onStartCommand(intent, flags, startId);
}
}
但是这种方法很容易忘记开启线程或者忘记调用stopSelf()
IntentService
继承于Service并处理异步请求的一个类,在IntentService中有一个工作线程来处理耗时操作,请求的Intent记录会加入队列。
IntentService
会执行以下操作:
- 创建默认工作线程,用于在应用的主线程外执行传递给
onStartCommand()
的所有Intent - 创建工作队列,用于将Intent逐一传递给
onHandleIntent()
实现 - 在处理完所有启动请求后停止服务,因为不必自己调用
stopSelf()
- 提供
onBind()
的默认实现(返回null) - 提供onStartCommand()的默认实现,可将Intent依次发送到工作队列和
onHandleIntent()
因此,只需实现构造函数与 onHandleIntent()
方法即可
综上所述:客户端通过startService(Intent)来启动IntentService,不需要手动地区控制IntentService,当任务执行完后,IntentService会自动停止; 可以启动IntentService多次,每个耗时操作会以工作队列的方式在IntentService的 onHandleIntent回调方法中执行,并且每次只会执行一个工作线程,执行完一,再到二这样!
简单示例:
修改Android服务——service使用浅析(一)的代码
- 新建一个MyIntentService类继承IntentService
public class MyIntentService extends IntentService {
public MyIntentService() {
super("MyIntentService");
}
@Override
protected void onHandleIntent(Intent intent) {
// 在这里执行具体的逻辑,做你想要的处理
Log.d("MyIntentService", "Thread id is" + Thread.currentThread().getId());
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d("MyIntentService", "onDestory ececuted");
}
}
- 修改activity_main.xml,加入一个用于启动MyIntentService服务按钮
<Button
android:id="@+id/start_intent_service"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start IntentService" />
- 最后修改MainActivity中代码
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
//...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//...
Button startIntentService = (Button) findViewById(R.id.start_intent_service);
startIntentService.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
//...
case R.id.start_intent_service:
Log.d("MainActivity", "Thread id is" + Thread.currentThread().getId());
Intent intentService = new Intent(this, MyIntentService.class);
startService(intentService);
break;
default:
break;
}
}
}
- 最后在AndroidManifest.xml中注册服务
<service android:name=".MyIntentService" />
备注:
- 自定义的IntentService类需要先提供一个无参构造器,并且必须在其内部调用父类的有参构造函数。
- 要在子类中实现
onHandleIntent()
这个抽象方法,在这个方法中处理一些具体的逻辑,通过串行来处理任务 - 这个服务运行结束后自动停止,重写了
onDestroy()
方法。
1.1 IntentService为什么不需要新建线程
// IntentService 源码中的 onCreate() 方法
@Override
public void onCreate() {
super.onCreate();
// HandlerThread 继承自 Thread,内部封装了 Looper,在这里新建线程并启动,所以启动 IntentService 不需要新建线程。
HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
thread.start();
// 获得工作线程的 Looper,并维护自己的工作队列。
mServiceLooper = thread.getLooper();
// mServiceHandler 是属于工作线程的。
mServiceHandler = new ServiceHandler(mServiceLooper);
}
private volatile ServiceHandler mServiceHandler;
private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
// onHandleIntent 方法在工作线程中执行,执行完调用 stopSelf() 结束服务。
onHandleIntent((Intent)msg.obj);
stopSelf(msg.arg1);
}
}
@WorkerThread
protected abstract void onHandleIntent(Intent intent);
1.2 不建议通过bindService()启动IntentService
@Override
public IBinder onBind(Intent intent) {
return null;
}
IntentService的源码中onBind()默认返回null,不适合bindService()启动服务
1.3 多次启动IntentService会顺序执行,停止服务后,后续事件不执行
IntentService 中使用的 Handler、Looper、MessageQueue 机制把消息发送到线程中去执行的,所以多次启动 IntentService 不会重新创建新的服务和新的线程,只是把消息加入消息队列中等待执行,而如果服务停止,会清除消息队列中的消息,后续的事件得不到执行。
2. 前台服务
Service是Android四大组件之一,但是后台服务在系统内存不足的时候,可能会被系统杀死,如何让后台服务经可能不被杀死,有以下几种思路:
- 提高Service的优先级
<!-- 为防止Service被系统回收,可以尝试通过提高服务的优先级解决,1000是最高优先级,数字越小,优先级越低 -->
android:priority="1000"
-
把Service写成系统服务,将不会回收
在Manifest.xml文件中设置persistent属性为true,则可使该服务免受out-of-memory killer的影响。但是这种做法一定要谨慎,系统服务太多将严重影响系统的整体运行效率。 -
将服务改成前台服务
foreground service
重写onStartCommand方法,使用StartForeground(int,Notification)
方法来启动service。 -
利用Android系统广播
利用ANDROID的系统广播检查Service的运行状态,如果被杀掉,就再起来,系统广播是Intent.ACTION_TIME_TICK
,这个广播每分钟发送一次,我们可以每分钟检查一次Service的运行状态,如果已经被结束了,就重新启动Service
前台服务是那些被认为用户知道(用户所认可的)且在系统内存不足的时候不允许系统杀死的服务。前台服务必须给状态栏提供一个通知,它被放到正在运行(Ongoing)标题之下——这就意味着通知只有在这个服务被终止或从前台主动移除通知后才能被解除。
将服务设置为前台服务的时机一般有:
- 需要立即执行
- 重要(必须完成)
- 用户可感知(大部分情况由用户主动发起)
- 有明确的起始时间和结束时间
前台服务的典型应用包括播放音乐、完成购买交易、地理位置追踪等。前台服务与普通服务之间最大的区别在于:它会有一个正在运行的图标在系统的状态栏显示,下拉状态栏可以看到更加详细的信息。
2.1 Androd 8.0 之前前 前台服务使用
在Android 8.0 之前并未对后台服务做过多的限制,创建一个前台服务并不复杂,修改前述的MyService代码:
public class MyService extends Service {
//.......
@Override
public void onCreate() {
super.onCreate();
Log.d("MyService", "onCreate executed");
Intent intent = new Intent(this, MainActivity.class);
PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);
Notification notification = new NotificationCompat.Builder(this)
.setContentTitle("this is the content title")
.setContentText("this is the content text")
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
.setContentIntent(pi)
.build();
startForeground(1, notification);
}
//......
}
关于Notification可以查看
Android通知——Notification使用浅析(一)
Android通知——Notification使用浅析(二)
android通知——Notification 8.0适配详解
Android8.0以后Notification坐了较大的的改动,在Android9.0版本,如果想要设置前台服务,需要申请FOREGROUND_SERVICE
权限,这是一个普通权限,不需要动态申请,只需要在AndroidMenifest.xml文件中添加:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
public class ForeService extends Service {
private static final String CHANNEL_ID = "fore_Service";
public ForeService() {
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
//处理任务
return START_STICKY;
}
@SuppressLint("CommitPrefEdits")
@Override
public void onCreate() {
super.onCreate();
Log.d("foreground", "onCreate");
//如果API在26以上即版本为O则调用startForefround()方法启动服务
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
setForegroundService();
}
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
/**
*通过通知启动服务
*/
@TargetApi(Build.VERSION_CODES.O)
public void setForegroundService()
{
//设定的通知渠道名称
String channelName = "fore";
//设置通知的重要程度
int importance = NotificationManager.IMPORTANCE_HIGH;
//向系统注册通知渠道,注册后不能改变重要性以及其他通知行为
NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
//构建通知渠道
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, channelName, importance);
notificationManager.createNotificationChannel(channel);
//在创建的通知渠道上发送通知
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID);
builder.setSmallIcon(R.drawable.ic_launcher_foreground) //设置通知图标
.setContentTitle("前台服务标题")//设置通知标题
.setContentText("前台服务通知内容")//设置通知内容
.setAutoCancel(true) //用户触摸时,自动关闭
.setOngoing(true);//设置处于运行状态
//将服务置于启动状态 NOTIFICATION_ID指的是创建的通知的ID
startForeground(101,builder.build());
}
}
在Activity中开启服务即可:
Intent foreIntentService = new Intent(this, ForeService.class);
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
{
startForegroundService(foreIntentService);
}
else{
startService(foreIntentService);
}
备注:
- 在Android 8.0以后,开启前台服务是调用
startForegroundService()
,且在开启服务5秒内必须调用startForeground()
这个方法来将这个服务提升为前台服务,如果超过了这个时间,就会报IllegalArgumentException:Not allowed to start service Intent 错误。 - 注意
startForeground()
中第一个参数不能为0(被坑惨了,一把辛酸泪)