Service相关知识

  • 1、Service简单概述
  • 2、Service在清单文件中的声明
  • 3、Service生命周期
  • 4、Service启动方式
  • 5、关于启动服务与绑定服务间的转换问题
  • 6、前台服务以及通知发送
  • 7、服务Service与线程Thread的区别
  • 8、Android 5.0以上的隐式启动问题及其解决方案

1.Service简单概述

Service(服务)是一个一种可以在后台执行长时间运行操作而没有用户界面的应用组件。服务可由其他应用组件启动(如Activity),服务一旦被启动将在后台一直运行,即使启动服务的组件(Activity)已销毁也不受影响。 此外,组件可以绑定到服务,以与之进行交互,甚至是执行进程间通信 (IPC)。 例如,服务可以处理网络事务、播放音乐,执行文件 I/O 或与内容提供程序交互,而所有这一切均可在后台进行,Service基本上分为两种形式:

  • 启动状态

当应用组件(如 Activity)通过调用 startService() 启动服务时,服务即处于“启动”状态。一旦启动,服务即可在后台无限期运行,即使启动服务的组件已被销毁也不受影响,除非手动调用才能停止服务, 已启动的服务通常是执行单一操作,而且不会将结果返回给调用方。

  • 绑定状态

当应用组件通过调用 bindService() 绑定到服务时,服务即处于“绑定”状态。绑定服务提供了一个客户端-服务器接口,允许组件与服务进行交互、发送请求、获取结果,甚至是利用进程间通信 (IPC) 跨进程执行这些操作。 仅当与另一个应用组件绑定时,绑定服务才会运行。 多个组件可以同时绑定到该服务,但全部取消绑定后,该服务即会被销毁。

2.Service在清单文件中的声明

前面说过Service分为启动状态和绑定状态两种,但无论哪种具体的Service启动类型,都是通过继承Service基类自定义而来,也都需要在AndroidManifest.xml中声明,那么在分析这两种状态之前,我们先来了解一下Service在AndroidManifest.xml中的声明语法,其格式如下:

<service android:enabled=["true" | "false"]
    android:exported=["true" | "false"]
    android:icon="drawable resource"
    android:isolatedProcess=["true" | "false"]
    android:label="string resource"
    android:name="string"
    android:permission="string"
    android:process="string" >
    . . .
</service>
  • android:exported:代表是否能被其他应用隐式调用,其默认值是由service中有无intent-filter决定的,如果有intent-filter,默认值为true,否则为false。为false的情况下,即使有intent-filter匹配,也无法打开,即无法被其他应用隐式调用。

  • android:name:对应Service类名

  • android:permission:是权限声明

  • android:process:是否需要在单独的进程中运行,当设置为android:process=”:remote”时,代表Service在单独的进程中运行。注意“:”很重要,它的意思是指要在当前进程名称前面附加上当前的包名,所以“remote”和”:remote”不是同一个意思,前者的进程名称为:remote,而后者的进程名称为:App-packageName:remote。

  • android:isolatedProcess :设置 true 意味着,服务会在一个特殊的进程下运行,这个进程与系统其他进程分开且没有自己的权限。与其通信的唯一途径是通过服务的API(bind and start)。

  • android:enabled:是否可以被系统实例化,默认为 true因为父标签 也有 enable 属性,所以必须两个都为默认值 true 的情况下服务才会被激活,否则不会激活。
      ok~,关于Service在清单文件的声明我们先了解这些就行,接下来分别针对Service启动服务和绑定服务进行详细分析

3.Service生命周期

image.png

其中左图显示了使用 startService() 所创建的服务的生命周期,右图显示了使用 bindService() 所创建的服务的生命周期。通过图中的生命周期方法,我们可以监控Service的整体执行过程,包括创建,运行,销毁,关于Service不同状态下的方法回调在前面的分析中已描述得很清楚,这里就不重复了,下面给出官网对生命周期的原文描述:

服务的整个生命周期从调用 onCreate() 开始起,到 onDestroy() 返回时结束。与 Activity 类似,服务也在 onCreate() 中完成初始设置,并在 onDestroy() 中释放所有剩余资源。例如,音乐播放服务可以在 onCreate() 中创建用于播放音乐的线程,然后在 onDestroy() 中停止该线程。
  无论服务是通过 startService() 还是 bindService() 创建,都会为所有服务调用 onCreate() 和 onDestroy() 方法。
  服务的有效生命周期从调用 onStartCommand() 或 onBind() 方法开始。每种方法均有 Intent 对象,该对象分别传递到 startService() 或 bindService()。
  对于启动服务,有效生命周期与整个生命周期同时结束(即便是在 onStartCommand() 返回之后,服务仍然处于活动状态)。对于绑定服务,有效生命周期在 onUnbind() 返回时结束。

相关生命周期详情
  • onCreate():首次创建服务时,系统将调用此方法来执行一次性设置程序(在调用 onStartCommand() 或onBind() 之前)。如果服务已在运行,则不会调用此方法,该方法只调用一次

  • onDestory():当服务不再使用且将被销毁时,系统将调用此方法。服务应该实现此方法来清理所有资源,如线程、注册的侦听器、接收器等,这是服务接收的最后一个调用。

  • onStartCommand(intent,flag,startId): 当另一个组件(如 Activity)通过调用 startService() 请求启动服务时,系统将调用此方法。一旦执行此方法,服务即会启动并可在后台无限期运行。 如果自己实现此方法,则需要在服务工作完成后,通过调用 stopSelf() 或 stopService() 来停止服务。(在绑定状态下,无需实现此方法。)

  • IBinder onBind(intent):当另一个组件想通过调用 bindService() 与服务绑定(例如执行 RPC)时,系统将调用此方法。在此方法的实现中,必须返回 一个IBinder 接口的实现类,供客户端用来与服务进行通信。无论是启动状态还是绑定状态,此方法必须重写,但在启动状态的情况下直接返回 null。

  • onUnbind(intent):当该Service上绑定的所有客户端都断开时会回调该方法!

4 启动方式

  • StartService()启动Service
  • BindService()启动Service
  • PS:还有一种,就是启动Service后,绑定Service!
4.1 StartService启动Service

首先我们自定义一个Service,重写相关的方法,用户在logcat上打印验证:

TestService1.java

public class TestService1 extends Service {
    private final String TAG = "TestService1";

//    必须要实现的方法
    @Override
    public IBinder onBind(Intent intent) {
        Log.i(TAG, "onBind方法被调用!");
        return null;
    }

//    service 被创建时调用
    @Override
    public void onCreate() {
        Log.i(TAG, "onCreate 方法被调用!");
        super.onCreate();
    }

//    service被启动时调用
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i(TAG, "onStartCommand 方法被调用!");
        return super.onStartCommand(intent, flags, startId);
    }

//    service被关闭之前回调
    @Override
    public void onDestroy() {
        Log.i(TAG, "onDestroy 方法被调用!");
        super.onDestroy();
    }
}

AndroidManifest.xml完成Service注册

   <!--注册一个Service 组件,同时配置一个action -->
        <service android:name=".TestService1">
            <intent-filter>
                <action android:name="cn.demo_for_service.TEST_SERVICE1"/>
            </intent-filter>
        </service>

再接着是简单的布局文件,两个按钮,再最后是MainActivity的编写,在按钮的点击事件中分别 调用startService( )和stopService( )!

public class MainActivity extends AppCompatActivity {

    private Button btn_start, btn_stop;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        btn_start = findViewById(R.id.btn_start);
        btn_stop = findViewById(R.id.btn_stop);

//        创建启动Service 的Intent,以及Intent属性
        final Intent intent = new Intent();
        intent.setAction("cn.demo_for_service.TEST_SERVICE1");
        intent.setPackage("cn.demo_for_service");

        btn_start.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startService(intent);
            }
        });

        btn_stop.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                stopService(intent);
            }
        });
    }
}

运行结果

点击开始

2019-06-24 15:38:21.527 6822-6822/cn.demo_for_service I/TestService1: onCreate 方法被调用!
2019-06-24 15:38:21.529 6822-6822/cn.demo_for_service I/TestService1: onStartCommand 方法被调用!

多点两下

2019-06-24 15:38:21.527 6822-6822/cn.demo_for_service I/TestService1: onCreate 方法被调用!
2019-06-24 15:38:21.529 6822-6822/cn.demo_for_service I/TestService1: onStartCommand 方法被调用!
2019-06-24 15:41:02.687 6822-6822/cn.demo_for_service I/TestService1: onStartCommand 方法被调用!
2019-06-24 15:41:03.647 6822-6822/cn.demo_for_service I/TestService1: onStartCommand 方法被调用!
2019-06-24 15:41:07.869 6822-6822/cn.demo_for_service I/TestService1: onStartCommand 方法被调用!

最后点停止

2019-06-24 15:38:21.527 6822-6822/cn.demo_for_service I/TestService1: onCreate 方法被调用!
2019-06-24 15:38:21.529 6822-6822/cn.demo_for_service I/TestService1: onStartCommand 方法被调用!
2019-06-24 15:41:02.687 6822-6822/cn.demo_for_service I/TestService1: onStartCommand 方法被调用!
2019-06-24 15:41:03.647 6822-6822/cn.demo_for_service I/TestService1: onStartCommand 方法被调用!
2019-06-24 15:41:07.869 6822-6822/cn.demo_for_service I/TestService1: onStartCommand 方法被调用!
2019-06-24 15:41:47.515 6822-6822/cn.demo_for_service I/TestService1: onDestroy 方法被调用!

结果分析:

  • ①首次启动会创建一个Service实例,依次调用onCreate()和onStartCommand()方法,此时Service 进入运行状态,如果再次调用StartService启动Service,将不会再创建新的Service对象, 系统会直接复用前面创建的Service对象,调用它的onStartCommand()方法!

  • ②但这样的Service与它的调用者无必然的联系,就是说当调用者结束了自己的生命周期, 但是只要不调用stopService,那么Service还是会继续运行的!

  • ③无论启动了多少次Service,只需调用一次StopService即可停掉Service

4.2 验证BindService启动Service的顺序:

我们先要来了解一些东西先: 首先是第一个大图下面给出的Context的bindService方法:

  • ServiceConnection对象:监听访问者与Service间的连接情况,如果成功连接,回调 onServiceConnected(),如果异常终止或者其他原因终止导致Service与访问者断开 连接则回调onServiceDisconnected方法,调用unBindService()不会调用该方法!

  • onServiceConnected方法中有一个IBinder对象,该对象即可实现与被绑定Service 之间的通信!我们再开发Service类时,默认需要实现IBinder onBind()方法,该方法返回的 IBinder对象会传到ServiceConnection对象中的onServiceConnected的参数,我们就可以 在这里通过这个IBinder与Service进行通信 !

接下来就是写代码验证了,这里的话我们定义一个用来计时的Service, 然后来演示BindService的用法以及方法调用流程!代码比较简单。

TestService2.java:

public class TestService2 extends Service {
    private final String TAG = "TestService2";
    private int count;
    private boolean quit;

    //定义Binder方法所返回的对象
    private MyBinder binder = new MyBinder();

    public class MyBinder extends Binder {
        public int getCount() {
            return count;
        }
    }

    //必须实现的方法,绑定Service 时回调该方法
    @Override
    public IBinder onBind(Intent intent) {
        Log.i(TAG, "onBind 方法被调用!");
        return binder;
    }

    //Service被创建时回调
    @Override
    public void onCreate() {
        super.onCreate();
        Log.i(TAG, "onCreate 方法被调用! ");
        new Thread() {
            @Override
            public void run() {
                super.run();
                while (!quit) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    count++;
                }
            }
        }.start();
    }

    //Service 断开时回调
    @Override
    public boolean onUnbind(Intent intent) {
        Log.i(TAG, "onUnbind 方法被调用!");
        return true;
    }

    //Service 被关闭前回调
    @Override
    public void onDestroy() {
        Log.i(TAG, "onDestroy 方法被调用!");
        super.onDestroy();
        this.quit = true;
    }

    @Override
    public void onRebind(Intent intent) {
        Log.i(TAG, "onRebind 方法被调用!");
        super.onRebind(intent);
    }
}

在AndroidManifest.xml中对Service组件进行注册:

   <service android:name=".TestService2">
            <intent-filter>
                <action android:name="cn.demo_for_service.TEST_SERVICE2"/>
            </intent-filter>
   </service>

MainActivity.java:

public class MainActivity extends Activity {  
  
    private Button btnbind;  
    private Button btncancel;  
    private Button btnstatus;  
      
    //保持所启动的Service的IBinder对象,同时定义一个ServiceConnection对象  
    TestService2.MyBinder binder;  
    private ServiceConnection conn = new ServiceConnection() {  
          
        //Activity与Service断开连接时回调该方法  
        @Override  
        public void onServiceDisconnected(ComponentName name) {  
            System.out.println("------Service DisConnected-------");  
        }  
          
        //Activity与Service连接成功时回调该方法  
        @Override  
        public void onServiceConnected(ComponentName name, IBinder service) {  
            System.out.println("------Service Connected-------");  
            binder = (TestService2.MyBinder) service;  
        }  
    };  
      
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
        btnbind = (Button) findViewById(R.id.btnbind);  
        btncancel = (Button) findViewById(R.id.btncancel);  
        btnstatus  = (Button) findViewById(R.id.btnstatus);  
        final Intent intent = new Intent();  
        intent.setAction("com.jay.example.service.TEST_SERVICE2");  
        btnbind.setOnClickListener(new OnClickListener() {            
            @Override  
            public void onClick(View v) {  
                //绑定service  
                bindService(intent, conn, Service.BIND_AUTO_CREATE);                  
            }  
        });  
          
        btncancel.setOnClickListener(new OnClickListener() {  
            @Override  
            public void onClick(View v) {  
                //解除service绑定  
                unbindService(conn);                  
            }  
        });  
          
        btnstatus.setOnClickListener(new OnClickListener() {  
            @Override  
            public void onClick(View v) {  
                Toast.makeText(getApplicationContext(), "Service的count的值为:"  
                        + binder.getCount(), Toast.LENGTH_SHORT).show();  
            }  
        });  
    }  
}  

绑定服务

2019-06-24 15:52:52.469 7194-7194/cn.demo_for_service I/TestService2: onCreate 方法被调用! 
2019-06-24 15:52:52.470 7194-7194/cn.demo_for_service I/TestService2: onBind 方法被调用!
2019-06-24 15:52:52.476 7194-7194/cn.demo_for_service I/TestService2: ------Service Connected-------

再次点击绑定服务:没有任何变化

2019-06-24 15:52:52.469 7194-7194/cn.demo_for_service I/TestService2: onCreate 方法被调用! 
2019-06-24 15:52:52.470 7194-7194/cn.demo_for_service I/TestService2: onBind 方法被调用!
2019-06-24 15:52:52.476 7194-7194/cn.demo_for_service I/TestService2: ------Service Connected-------

获取当前Service状态

2019-06-24 15:52:52.469 7194-7194/cn.demo_for_service I/TestService2: onCreate 方法被调用! 
2019-06-24 15:52:52.470 7194-7194/cn.demo_for_service I/TestService2: onBind 方法被调用!
2019-06-24 15:52:52.476 7194-7194/cn.demo_for_service I/TestService2: ------Service Connected-------
2019-06-24 15:53:55.185 7194-7194/cn.demo_for_service I/TestService2: binder.getCount() 值 为:  62

解除绑定

2019-06-24 15:52:52.469 7194-7194/cn.demo_for_service I/TestService2: onCreate 方法被调用! 
2019-06-24 15:52:52.470 7194-7194/cn.demo_for_service I/TestService2: onBind 方法被调用!
2019-06-24 15:52:52.476 7194-7194/cn.demo_for_service I/TestService2: ------Service Connected-------
2019-06-24 15:53:55.185 7194-7194/cn.demo_for_service I/TestService2: binder.getCount() 值 为:  62
2019-06-24 15:54:53.246 7194-7194/cn.demo_for_service I/TestService2: onUnbind 方法被调用!
2019-06-24 15:54:53.247 7194-7194/cn.demo_for_service I/TestService2: onDestroy 方法被调用!

如果我们再绑定后直接关掉Activity的话会报错, 然后会自动调用onUnbind和onDestory方法!

I/TestService2: onCreate 方法被调用! 
I/TestService2: onBind 方法被调用!
I/TestService2: ------Service Connected-------
D/EGL_emulation: eglMakeCurrent: 0x9ba850c0: ver 3 0 (tinfo 0x9ba831c0)
E/ActivityThread: Activity cn.demo_for_service.MainActivity2 has leaked ServiceConnection cn.demo_for_service.MainActivity2$1@892022a that was originally bound here
    android.app.ServiceConnectionLeaked: Activity cn.demo_for_service.MainActivity2 has leaked ServiceConnection cn.demo_for_service.MainActivity2$1@892022a that was originally bound here
        at android.app.LoadedApk$ServiceDispatcher.<init>(LoadedApk.java:1514)
        at android.app.LoadedApk.getServiceDispatcher(LoadedApk.java:1406)
        at android.app.ContextImpl.bindServiceCommon(ContextImpl.java:1589)
        at android.app.ContextImpl.bindService(ContextImpl.java:1541)
        at android.content.ContextWrapper.bindService(ContextWrapper.java:678)
        at cn.demo_for_service.MainActivity2$2.onClick(MainActivity2.java:56)
        at android.view.View.performClick(View.java:6256)
        at android.view.View$PerformClick.run(View.java:24701)
        at android.os.Handler.handleCallback(Handler.java:789)
        at android.os.Handler.dispatchMessage(Handler.java:98)
        at android.os.Looper.loop(Looper.java:164)
        at android.app.ActivityThread.main(ActivityThread.java:6541)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)
I/TestService2: onUnbind 方法被调用!
I/TestService2: onDestroy 方法被调用!

总结:
Step 1:在自定义的Service中继承Binder,实现自己的IBinder对象
Step 2:通过onBind( )方法返回自己的IBinder对象
Step 3:在绑定该Service的类中定义一个ServiceConnection对象,重写两个方法, onServiceConnected和onDisconnected!然后直接读取IBinder传递过来的参数即可!

注意事项
使用BindService绑定Service,依次调用onCreate(),onBind()方法, 我们可以在onBind()方法中返回自定义的IBinder对象;再接着调用的是 ServiceConnection的onServiceConnected()方法该方法中可以获得 IBinder对象,从而进行相关操作;当Service解除绑定后会自动调用 onUnbind和onDestroyed方法,当然绑定多客户端情况需要解除所有 的绑定才会调用onDestoryed方法进行销毁!

结果分析:

  • ①当首次使用bindService绑定一个Service时,系统会实例化一个Service实例,并调用其onCreate()和onBind()方法,然后调用者就可以通过IBinder和Service进行交互了,此后如果再次使用bindService绑定Service,系统不会创建新的Sevice实例,也不会再调用onBind()方法,只会直接把IBinder对象传递给其他后来增加的客户端!

  • ②如果我们解除与服务的绑定,只需调用unbindService(),此时onUnbind和onDestory方法将会被调用!这是一个客户端的情况,假如是多个客户端绑定同一个Service的话,情况如下 当一个客户完成和service之间的互动后,它调用 unbindService() 方法来解除绑定。当所有的客户端都和service解除绑定后,系统会销毁service。(除非service也被startService()方法开启)

  • ③另外,和上面那张情况不同,bindService模式下的Service是与调用者相互关联的,可以理解为 "一条绳子上的蚂蚱",要死一起死,在bindService后,一旦调用者销毁,那么Service也立即终止!
    通过BindService调用Service时调用的Context的bindService的解析 bindService(Intent Service,ServiceConnection conn,int flags)
    service:通过该intent指定要启动的Service
    conn:ServiceConnection对象,用户监听访问者与Service间的连接情况, 连接成功回调该对象中的onServiceConnected(ComponentName,IBinder)方法; 如果Service所在的宿主由于异常终止或者其他原因终止,导致Service与访问者间断开 连接时调用onServiceDisconnected(CompanentName)方法,主动通过unBindService() 方法断开并不会调用上述方法!
    flags:指定绑定时是否自动创建Service(如果Service还未创建), 参数可以是0(不自动创建),BIND_AUTO_CREATE(自动创建)

5.关于启动服务与绑定服务间的转换问题

通过前面对两种服务状态的分析,相信大家已对Service的两种状态有了比较清晰的了解,那么现在我们就来分析一下当启动状态和绑定状态同时存在时,又会是怎么的场景?
  虽然服务的状态有启动和绑定两种,但实际上一个服务可以同时是这两种状态,也就是说,它既可以是启动服务(以无限期运行),也可以是绑定服务。有点需要注意的是Android系统仅会为一个Service创建一个实例对象,所以不管是启动服务还是绑定服务,操作的是同一个Service实例,而且由于绑定服务或者启动服务执行顺序问题将会出现以下两种情况:

  • 先绑定服务后启动服务

如果当前Service实例先以绑定状态运行,然后再以启动状态运行,那么绑定服务将会转为启动服务运行,这时如果之前绑定的宿主(Activity)被销毁了,也不会影响服务的运行,服务还是会一直运行下去,指定收到调用停止服务或者内存不足时才会销毁该服务。

  • 先启动服务后绑定服务

如果当前Service实例先以启动状态运行,然后再以绑定状态运行,当前启动服务并不会转为绑定服务,但是还是会与宿主绑定,只是即使宿主解除绑定后,服务依然按启动服务的生命周期在后台运行,直到有Context调用了stopService()或是服务本身调用了stopSelf()方法抑或内存不足时才会销毁服务。

以上两种情况显示出启动服务的优先级确实比绑定服务高一些。不过无论Service是处于启动状态还是绑定状态,或处于启动并且绑定状态,我们都可以像使用Activity那样通过调用 Intent 来使用服务(即使此服务来自另一应用)。 当然,我们也可以通过清单文件将服务声明为私有服务,阻止其他应用访问。最后这里有点需要特殊说明一下的,由于服务在其托管进程的主线程中运行(UI线程),它既不创建自己的线程,也不在单独的进程中运行(除非另行指定)。 这意味着,如果服务将执行任何耗时事件或阻止性操作(例如 MP3 播放或联网)时,则应在服务内创建新线程来完成这项工作,简而言之,耗时操作应该另起线程执行。只有通过使用单独的线程,才可以降低发生“应用无响应”(ANR) 错误的风险,这样应用的主线程才能专注于用户与 Activity 之间的交互, 以达到更好的用户体验。

6.前台服务以及通知发送

前台服务被认为是用户主动意识到的一种服务,因此在内存不足时,系统也不会考虑将其终止。 前台服务必须为状态栏提供通知,状态栏位于“正在进行”标题下方,这意味着除非服务停止或从前台删除,否则不能清除通知。例如将从服务播放音乐的音乐播放器设置为在前台运行,这是因为用户明确意识到其操作。 状态栏中的通知可能表示正在播放的歌曲,并允许用户启动 Activity 来与音乐播放器进行交互。如果需要设置服务运行于前台, 我们该如何才能实现呢?Android官方给我们提供了两个方法,分别是startForeground()和stopForeground(),这两个方式解析如下:

  • startForeground(int id, Notification notification)
    该方法的作用是把当前服务设置为前台服务,其中id参数代表唯一标识通知的整型数,需要注意的是提供给 startForeground() 的整型 ID 不得为 0,而notification是一个状态栏的通知。

  • stopForeground(boolean removeNotification)
    该方法是用来从前台删除服务,此方法传入一个布尔值,指示是否也删除状态栏通知,true为删除。 注意该方法并不会停止服务。 但是,如果在服务正在前台运行时将其停止,则通知也会被删除。

下面我们结合一个简单案例来使用以上两个方法,ForegroundService代码如下:

package cn.demo_for_service;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;

/**
 * FileName: ForegroundService
 * Author: nanzong
 * Date: 2019-06-25 14:57
 * Description:
 * History:
 */
public class ForegroundService extends Service {

    /**
     * id不可设置为0,否则不能设置为前台service
     */
    private static final int NOTIFICATION_DOWNLOAD_PROGRESS_ID = 0x0001;

    private boolean isRemove=false;//是否需要移除

    /**
     * Notification
     */
    public void createNotification(){


        NotificationChannel channel = new NotificationChannel("xxx", "xxx", NotificationManager.IMPORTANCE_LOW);

        NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        if (manager == null)
            return;
        manager.createNotificationChannel(channel);

        Notification notification = new NotificationCompat.Builder(this, "xxx")
                .setAutoCancel(true)
                .setCategory(Notification.CATEGORY_SERVICE)
                .setOngoing(true)
                .setPriority(NotificationManager.IMPORTANCE_LOW)
                .build();

        startForeground(NOTIFICATION_DOWNLOAD_PROGRESS_ID,notification);

    }


    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        int i=intent.getExtras().getInt("cmd");
        if(i==0){
            if(!isRemove) {
                createNotification();
            }
            isRemove=true;
        }else {
            //移除前台服务
            if (isRemove) {
                stopForeground(true);
            }
            isRemove=false;
        }

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

    @Override
    public void onDestroy() {
        //移除前台服务
        if (isRemove) {
            stopForeground(true);
        }
        isRemove=false;
        super.onDestroy();
    }

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

在ForegroundService类中,创建了一个notification的通知,并通过启动Service时传递过来的参数判断是启动前台服务还是关闭前台服务,最后在onDestroy方法被调用时,也应该移除前台服务。以下是ForegroundActivity的实现:


public class ForegroundActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_foreground);

        Button btnStart = (Button) findViewById(R.id.startForeground);
        Button btnStop = (Button) findViewById(R.id.stopForeground);
        final Intent intent = new Intent(this, ForegroundService.class);


        btnStart.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                intent.putExtra("cmd", 0);//0,开启前台服务,1,关闭前台服务
                startService(intent);
            }
        });


        btnStop.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                intent.putExtra("cmd", 1);//0,开启前台服务,1,关闭前台服务
                startService(intent);
            }
        });
    }

}

ok~,以上便是有关于Service前台服务的内容,接下来再聊聊服务与线程的区别

7.服务Service与线程Thread的区别

  • 两者概念的迥异
  1. Thread 是程序执行的最小单元,它是分配CPU的基本单位,android系统中UI线程也是线程的一种,当然Thread还可以用于执行一些耗时异步的操作。

  2. Service是Android的一种机制,服务是运行在主线程上的,它是由系统进程托管。它与其他组件之间的通信类似于client和server,是一种轻量级的IPC通信,这种通信的载体是binder,它是在linux层交换信息的一种IPC,而所谓的Service后台任务只不过是指没有UI的组件罢了。

  • 两者的执行任务迥异
  1. 在android系统中,线程一般指的是工作线程(即后台线程),而主线程是一种特殊的工作线程,它负责将事件分派给相应的用户界面小工具,如绘图事件及事件响应,因此为了保证应用 UI 的响应能力主线程上不可执行耗时操作。如果执行的操作不能很快完成,则应确保它们在单独的工作线程执行。

  2. Service 则是android系统中的组件,一般情况下它运行于主线程中,因此在Service中是不可以执行耗时操作的,否则系统会报ANR异常,之所以称Service为后台服务,大部分原因是它本身没有UI,用户无法感知(当然也可以利用某些手段让用户知道),但如果需要让Service执行耗时任务,可在Service中开启单独线程去执行。

  • 两者使用场景
  1. 当要执行耗时的网络或者数据库查询以及其他阻塞UI线程或密集使用CPU的任务时,都应该使用工作线程(Thread),这样才能保证UI线程不被占用而影响用户体验。

  2. 在应用程序中,如果需要长时间的在后台运行,而且不需要交互的情况下,使用服务。比如播放音乐,通过Service+Notification方式在后台执行同时在通知栏显示着。

  • 两者的最佳使用方式

在大部分情况下,Thread和Service都会结合着使用,比如下载文件,一般会通过Service在后台执行+Notification在通知栏显示+Thread异步下载,再如应用程序会维持一个Service来从网络中获取推送服务。在Android官方看来也是如此,所以官网提供了一个Thread与Service的结合来方便我们执行后台耗时任务,它就是IntentService,(如果想更深入了解IntentService,可以看博主的另一篇文章:Android 多线程之IntentService 完全详解),当然 IntentService并不适用于所有的场景,但它的优点是使用方便、代码简洁,不需要我们创建Service实例并同时也创建线程,某些场景下还是非常赞的!由于IntentService是单个worker thread,所以任务需要排队,因此不适合大多数的多任务情况。

  • 两者的真正关系
  • 两者没有半毛钱关系。

8.Android 5.0以上的隐式启动问题

既然有隐式启动,那么就会有显示启动,那就先来了解一下什么是隐式启动和显示启动。

  • 显示启动
    直接上代码一目了然,不解释了。
//显示启动
Intent intent = new Intent(this,ForegroundService.class);
startService(intent);
  • 隐式启动
    需要设置一个Action,我们可以把Action的名字设置成Service的全路径名字,在这种情况下android:exported默认为true。
final Intent serviceIntent=new Intent(); 
serviceIntent.setAction("com.android.ForegroundService");
startService(serviceIntent);
  • 存在的意义
    如果在同一个应用中,两者都可以用。在不同应用时,只能用隐式启动。

  • Android 5.0以上的隐式启动问题
      Android 5.0之后google出于安全的角度禁止了隐式声明Intent来启动Service。如果使用隐式启动Service,会出没有指明Intent的错误,如下:

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: cn.demo_for_service, PID: 32300
    java.lang.IllegalArgumentException: Service Intent must be explicit: Intent { act=cn.demo_for_service.TEST_SERVICE1 }
        at android.app.ContextImpl.validateServiceIntent(ContextImpl.java:1784)
        at android.app.ContextImpl.startServiceCommon(ContextImpl.java:1825)
        at android.app.ContextImpl.startService(ContextImpl.java:1797)
        at android.content.ContextWrapper.startService(ContextWrapper.java:664)
        at cn.demo_for_service.MainActivity$1.onClick(MainActivity.java:29)
        at android.view.View.performClick(View.java:6652)
        at android.view.View.performClickInternal(View.java:6624)
        at android.view.View.access$3100(View.java:787)
        at android.view.View$PerformClick.run(View.java:26213)
        at android.os.Handler.handleCallback(Handler.java:891)
        at android.os.Handler.dispatchMessage(Handler.java:102)
        at android.os.Looper.loop(Looper.java:207)
        at android.app.ActivityThread.main(ActivityThread.java:7470)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:524)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:958)

主要原因我们可以从源码中找到,这里看看Android 4.4的ContextImpl源码中的validateServiceIntent(Intent service),可知如果启动service的intent的component和package都为空并且版本大于KITKAT的时候只是报出一个警报,告诉开发者隐式声明intent去启动Service是不安全的.

    private void validateServiceIntent(Intent service) {
        if (service.getComponent() == null && service.getPackage() == null) {
            if (true || getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.KITKAT) {
                Log.w(TAG, "Implicit intents with startService are not safe: " + service
                        + " " + Debug.getCallers(2, 3));
                //IllegalArgumentException ex = new IllegalArgumentException(
                //        "Service Intent must be explicit: " + service);
                //Log.e(TAG, "This will become an error", ex);
                //throw ex;
            }
        }
    }

而在android5.0之后呢?我们这里看的是android6.0的源码如下(sublime text查android各个版本源码就是爽呀!!):

 private void validateServiceIntent(Intent service) {
        if (service.getComponent() == null && service.getPackage() == null) {
            if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP) {
                IllegalArgumentException ex = new IllegalArgumentException(
                        "Service Intent must be explicit: " + service);
                throw ex;
            } else {
                Log.w(TAG, "Implicit intents with startService are not safe: " + service
                        + " " + Debug.getCallers(2, 3));
            }
        }
    }

从源码可以看出如果启动service的intent的component和package都为空并且版本大于LOLLIPOP(5.0)的时候,直接抛出异常,该异常与之前隐式启动所报的异常时一致的。那么该如何解决呢?

解决方式

设置Action和packageName

final Intent serviceIntent=new Intent(); serviceIntent.setAction("com.android.ForegroundService");
serviceIntent.setPackage(getPackageName());//设置应用的包名

将隐式启动转换为显示启动

public static Intent getExplicitIntent(Context context, Intent implicitIntent) {
    // Retrieve all services that can match the given intent
     PackageManager pm = context.getPackageManager();
     List<ResolveInfo> resolveInfo = pm.queryIntentServices(implicitIntent, 0);
     // Make sure only one match was found
     if (resolveInfo == null || resolveInfo.size() != 1) {
         return null;
     }
     // Get component info and create ComponentName
     ResolveInfo serviceInfo = resolveInfo.get(0);
     String packageName = serviceInfo.serviceInfo.packageName;
     String className = serviceInfo.serviceInfo.name;
     ComponentName component = new ComponentName(packageName, className);
     // Create a new intent. Use the old one for extras and such reuse
     Intent explicitIntent = new Intent(implicitIntent);
     // Set the component to be explicit
     explicitIntent.setComponent(component);
     return explicitIntent;
    }

调用方式如下:

Intent mIntent=new Intent();//辅助Intent
mIntent.setAction("com.android.ForegroundService");
final Intent serviceIntent=new Intent(getExplicitIntent(this,mIntent));
startService(serviceIntent);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345