【Android】Service完全解析之必知必会

想必对于Android开发者来说,对Service一定不陌生了,作为大名鼎鼎的四大组件之一的service,在Android中有着不可替代的作用,它不像Activity那么光鲜亮丽,一般都是默默躲在后台执行着一些“见不得人的”任务,比如下载文件,音乐播放等等,即使退出应用了,它还是很顽强的在后台运行着,虽然随着android版本的不断提高,安全性的要求也越来越高,Service的一些黑科技也变得越来越难。

最近在学习Service,做个记录,希望能给您一些帮助,我们都知道service有两种启动方式,分别是startService和bindService,它们有各自的生命周期方法,盗张官网的图:

生命周期

下面分别来看下它的使用吧,我们先创建一个Service:

public class MyService extends Service {
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        Log.e("wangkeke","------onBind");
        return new MyBinder();
    }

    public interface MyIBinder{
        void showGetMyService();
    }

    public class MyBinder extends Binder implements MyIBinder{

        public void stopService(ServiceConnection serviceConnection){
            unbindService(serviceConnection);
        }

        @Override
        public void showGetMyService() {
            Log.e("wangkeke","------获取远程服务");
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.e("wangkeke","------onCreate");
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.e("wangkeke","------onStartCommand");
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public boolean onUnbind(Intent intent) {
        Log.e("wangkeke","------onUnbind");
        return super.onUnbind(intent);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.e("wangkeke","------onDestroy");
    }
}

我们简单定义了一个MyService,重写了几个比较重要的方法,别忘了在AndroidManifest.xml里面注册:


1.startService

然后在Activity里我们先通过startService来启动它:

Intent intent = new Intent(MainActivity.this,MyService.class);
startService(intent);

很简单传递给它一个intent,指定要启动的Service就可以了,运行看看结果:


第一次启动

执行了onCreate,onStartCommand,那么如果我们多次startService呢?看看效果:


多次执行startService

可以看出只有第一次执行才会调用onCreate,之后只会调用onStartCommand方法,启动是成功了,但我们该如何停止Servcie呢?一种方式是在自定义的Service中使用stopSelf()停止自己,另一种是在外部通过stopService(intent)的方式停止服务。
现在我们调用如下停止服务的代码:
Intent intent = new Intent(MainActivity.this,MyService.class);
stopService(intent);

看控制台打印日志:


停止服务

停止服务只会调用onDestory方法,多次调用stopService时,onDestory只会调用一次,这个也很好理解,比较服务都不在了,调用也就无效了。

startService适用于执行后台任务,但是要注意service默认是在主线程执行的,执行耗时操作的话请务必开启子线程去处理。

2.bindService

下面我们换种方式启动Service,通过bindService的方式,bindService方法需要三个参数:

    @Override
    public boolean bindService(Intent service, ServiceConnection conn,
            int flags) {
        return mBase.bindService(service, conn, flags);
    }

service就是我们的intent,ServiceConnection是个接口,当我们bindservice之后,AMS会回调ServiceConnection接口对象的onServiceConnected()方法,onServiceConnected方法会把远端service的代理binder传递过来,这样就可以通过这个代理binder跨进程调用service中的方法了。
我们先创建一个ServiceConnection对象:

    public ServiceConnection conn =  new ServiceConnection(){
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.e("wangkeke","-------onServiceConnected");
            serviceIsConnected = true;
            MyService.MyBinder myBinder = (MyService.MyBinder) service;
            myBinder.showGetMyService();

        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.e("wangkeke","-------onServiceDisconnected");
            serviceIsConnected = false;
        }
    };

然后通过bindService启动它:

Intent intent = new Intent(MainActivity.this,MyService.class);
bindService(intent, conn, Context.BIND_AUTO_CREATE);

运行结果如下:


第一次运行bindService

依次调用了onCreate,onBind,之后远程服务连接成功,调用了onServiceConnected,可以看到所有的回调都在主线程,当服务连接成功后,通过onServiceConnected拿到IBinder对象,就可以调用远程service的方法了。

bindService的第三个参数的含义:bindservice在建立连接时,如果发现service还没启动,会根据flag是否设置BIND_AUTO_CREATE,决定是否启动这个service。

那么绑定后的service如何解绑呢,通过unbindService方法,传入绑定service时所创建的ServiceConnection对象即可:

unbindService(conn);

生命周期打印如下:


unbindService

ServiceConnection接口的onServiceDisconnected()方法并不会在unbindService()操作断开逻辑连接时执行。而是在远端service进程终止时,AMS才会回调onServiceDisconnected()。

上面的例子,在onBind方法中,我们返回了new MyBinder(),如果我们直接返回null的话,就不会回调ServiceConnection的onServiceConnected方法了。

注意:bindservice是和组件绑定的,依赖于组件而存在,所以在页面退出的时候一定要调用unbindService解绑,不然会抛出异常,而startService则不受启动服务组件的影响,可以继续在后台继续执行。

另外要注意,服务是在主线程中运行的,如果要进行网络操作,音乐播放等CPU密集型工作或者阻塞性操作,请开启新线程来处理。

3.startService之后调用bindService

现在我们看这么一种情况,我们startService之后调用bindService,看看运行结果:

先点击startService,再点击bindService

可以看到当bindService已经启动的服务时,依然可以bind成功,只是不会重新调用onCreate方法,接着我们调用stopService看看,咦,点击之后并没有回调onDestory,看来仅仅通过
stopService是无法停止服务了,因为它和其他组件还绑定着呢,现在调下unbindService试试:


unbindService

unBindService之后,服务才会销毁,所以说如果同时使用了start和bind启动服务,必须要stopservice和unbindservice都执行后服务才会销毁。
至于先bind再start也是同样的道理,服务的创建onCreate方法只会调用一次,销毁同样需要调用stopservice和unbindservice后才能成功,具体大家可以自己实验感受下。

4.Service模拟下载任务

我们来模拟一个下载任务,具体代码如下:

public class DownloadService extends Service {

    private ServiceHandler serviceHandler;
    private NotificationManager mNotificationManager;

    private final class ServiceHandler extends Handler{

        public ServiceHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            //开始处理耗时任务
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Toast.makeText(DownloadService.this, "下载任务完成···", Toast.LENGTH_SHORT).show();
            mNotificationManager.cancel(100);
            //任务执行完毕,根据startId停止服务
            stopSelf(msg.arg1);
        }
    }

    @Override
    public void onCreate() {
        HandlerThread thread = new HandlerThread("downloadTest", Process.THREAD_PRIORITY_BACKGROUND);
        thread.start();

        serviceHandler = new ServiceHandler(thread.getLooper());
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        Toast.makeText(this, "开始任务···", Toast.LENGTH_SHORT).show();

        Message msg = Message.obtain();
        msg.arg1 = startId;
        serviceHandler.sendMessage(msg);
        showNotifycation();
        return super.onStartCommand(intent, flags, startId);
    }

    private void showNotifycation(){
        NotificationCompat.Builder mBuilder =
                new NotificationCompat.Builder(this,"downloadfile")
                        .setSmallIcon(R.mipmap.ic_launcher)
                        .setContentTitle("温馨提醒")
                        .setContentText("当前正在下载中···");
        mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        mNotificationManager.notify(100, mBuilder.build());
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }
}

我们在onCreate中创建了HandlerThread,开启消息循环,然后创建了Handler用来处理耗时任务,此时Handler取的是HandlerThread的Looper,运行在子线程中,在onStartCommand中我们发送了message并显示了个通知,任务sleep 5秒后结束,关闭通知并停止服务。运行效果如下:


模拟耗时任务

5.onStartCommand的返回值

Service作为一个组件,很难保证一直存在,当service进程被kill掉的时候,service会如何响应呢?onStartCommand的返回值其实就是对应的响应策略:

  1. START_STICKY:service进程被kill掉后,会重新创建service,并且调用onStartCommand(Intent,int,int)方法,但Intent将为null。

2.START_NOT_STICKY:如果在执行完onStartCommand后,服务被异常kill掉,系统不会自动重启该服务。

3.START_REDELIVER_INTENT:如果在执行完onStartCommand后,服务被异常kill掉,系统会自动重启该服务,并将Intent的值传入。

4.START_STICKY_COMPATIBILITY:兼容低版本,与 START_STICKY 作用相同,但不保证每次都能重启成功。

6.启动activity和弹出dialog

我们可能在某个合适的时机需要在Service里弹出Dialog,来测试下,我们启动服务5秒后弹出dialog,代码如下:

public class DialogService extends Service {

    private ServiceHandler serviceHandler;
    private NotificationManager mNotificationManager;

    private final class ServiceHandler extends Handler{

        public ServiceHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            //弹出dialog
            showDialog();
        }
    }

    private void showDialog() {

        AlertDialog.Builder dialog = new AlertDialog.Builder(this);

        dialog.setTitle("我是Service里的弹窗")
                .setIcon(R.mipmap.ic_launcher)
                .setMessage("没想到吧!!")
                .setPositiveButton("取消", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        Toast.makeText(DialogService.this, "点击了取消", Toast.LENGTH_SHORT).show();
                        dialog.dismiss();
                    }
                }).setNegativeButton("确定", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                Toast.makeText(DialogService.this, "点击了确定", Toast.LENGTH_SHORT).show();
                dialog.dismiss();
            }
        }).create().show();

    }

    @Override
    public void onCreate() {
        serviceHandler = new ServiceHandler(Looper.getMainLooper());
    }

    @Override
    public int onStartCommand(Intent intent, int flags, final int startId) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Message msg = Message.obtain();
                msg.arg1 = startId;
                serviceHandler.sendMessage(msg);
            }
        }).start();


        return super.onStartCommand(intent, flags, startId);
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }
}

然后运行后,5秒后,果然········崩溃了,异常如下:


弹出dialog崩溃

好吧,既然要主题那就给你设置个:

    

    AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this,R.style.myDialogTheme);

再次运行:


继续崩溃

崩溃也很正常,咱们想脱离activity弹出dialog肯定不是那么简单,经过一番调研搜索,要想弹出系统级别的dialog需要设置dialog的窗口类型为TYPE_SYSTEM_ALERT,提高dialog的窗口等级,我们添加如下代码再次尝试:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {//8.0新特性
      diaclog.getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY - 1);
} else {
      diaclog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
}

记得添加权限:

运行后,终于成功弹出dialog,启动服务后5秒钟弹出dialog,效果如下:


弹出全局dialog

当然在Service中弹出Dialog也可以通过启动透明Activity的方式,在Activity上弹出Dialog,这里就不具体实现了,有兴趣的同学可以自己实现感受下。

至于在Service中启动Activity就很简单了,只需要注意要设置flag为FLAG_ACTIVITY_NEW_TASK,即在新的任务栈中启动:

Intent intent = new Intent(this, YourActivity.class);   
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);   
startActivity(intent);

至于为什么要添加newtask大家可以点这里查看。

7.前台服务

服务虽然会在后台运行,但系统在内存不足的时候,也会考虑将其终止,所以为了提高Service的存活率,我们可以通过启动前台服务的方式来提高它的优先级,前台服务其实就是在通知栏常驻一个通知,一般音乐播放器都是这种方式来处理的。

注意:前台服务startForeground是android8.0特有的,也就是说8.0前后需要进行适配,8.0之前继续使用原来的NotificationCompat.Builder来创建通知,并设置setOngoing(true)来保持通知常驻,8.0以及8.0之后直接startForeground启动即可。

这里不介绍通知的使用,具体大家可以自行查看,8.0通知改动挺大,新增了channel的概念,大家可以自行google了解。

下面来看下它的具体用法:

public class ForegroundService extends Service {
    private NotificationManager notificationManager;

    @RequiresApi(api = Build.VERSION_CODES.O)
    @Override
    public void onCreate() {

        notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        Intent intentForeSerive = new Intent(this, MainActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intentForeSerive, 0);
        //8.0以及8.0之后
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            if (null == notificationManager.getNotificationChannel("fore_service")) {
                NotificationChannel channel = new NotificationChannel("fore_service", "前台服务", NotificationManager.IMPORTANCE_HIGH);
                notificationManager.createNotificationChannel(channel);
            }
            Notification notification = new NotificationCompat.Builder(this, "fore_service")
                    .setContentTitle("This is content title")
                    .setContentText("This is content text")
                    .setWhen(System.currentTimeMillis())
                    .setSmallIcon(R.mipmap.ic_launcher)
                    .setContentIntent(pendingIntent)
                    .build();
            startForeground(1, notification);
        }else {
            //8.0之前的版本
            Notification notification = new NotificationCompat.Builder(this, "fore_service")
                    .setContentTitle("This is content title")
                    .setContentText("This is content text")
                    .setWhen(System.currentTimeMillis())
                    .setAutoCancel(false)
                    .setOngoing(true)
                    .setSmallIcon(R.mipmap.ic_launcher)
                    .setContentIntent(pendingIntent)
                    .build();
            notificationManager.notify(1,notification);
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.O)
    @Override
    public int onStartCommand(Intent intent, int flags, final int startId) {

        int flag = intent.getIntExtra("flag",0);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            if(flag == 0){
                //关闭前台服务
                stopForeground(true);
                stopSelf();
            }
        }else {
            if(flag == 0){
                //关闭通知
                notificationManager.cancel(1);
                stopSelf();
            }
        }
        return super.onStartCommand(intent, flags, startId);
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }
}

我通过一个按钮来启动服务和关闭服务:

private int currentFlag = 1;

//点击按钮之后执行如下代码
Intent intent = new Intent(MainActivity.this, ForegroundService.class);
intent.putExtra("flag",currentFlag);
startService(intent);
if(currentFlag == 1){
     currentFlag = 0;
}else {
     currentFlag = 1;
}

运行效果如下:


打开和关闭前台服务

8.Android 5.0之后必须显式启动Service

在上面的例子中,我们都是使用Intent指定Service.class的方式显式启动的,那么我们在5.0以上的模拟器上运行如下代码:

    
            
                
                
            
        

    Intent intent = new Intent();
    intent.setAction("com.wangkeke.service.test");
    startService(intent);

话不多说,直接运行并点击启动Service,正如我们"期待的那样",崩溃出现了!!


5.0之后不能隐式启动Service

注意注意:Android 5.0之后必须显式启动Service!!!

你可能感兴趣的:(【Android】Service完全解析之必知必会)