Android内部分享[7]——系统广播和服务

什么是 Service

Service 是 Android 中一个可以长时间在后台运行的组件,Service 可由其他组件启动,而且就算用户切换到其它应用,服务仍然可以在后台继续运行。此外,组件可以绑定到服务,以与之进行交互,甚至是执行进程间通信 (IPC)。 例如,服务可以处理网络事务、播放音乐,执行文件 I/O 或与内容提供程序交互,而所有这一切均可在后台进行。

Service 基本上分为两种形式:

  1. startService

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

  1. bindService

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

而且我们的服务可以同时以上面的两种方式运行,也就是说,它既可以是启动服务(以无限期运行),也允许绑定。问题只是在于您是否实现了一组回调方法:onStartCommand()(允许组件启动服务)和 onBind()(允许绑定服务)。

使用线程还是服务

我们前面已经知道一些耗时操作可以单独放入非 UI 线程执行,那么线程和服务之间有什么关系?

线程的一个重要特点是不会阻塞 UI 线程,而服务的一个重要特点是不需要和用户交互(没有交互界面),但是服务默认还是在主线程中执行的,所以如果是比较耗时的密集型或阻塞式的服务,你应该在服务内部创建一个新线程来执行这些操作。

创建 Service

要创建服务,您必须创建 Service 的子类(或使用它的一个现有子类)。在实现中,您需要重写一些回调方法,以处理服务生命周期的某些关键方面并提供一种机制将组件绑定到服务(如适用)。 应重写的最重要的回调方法包括:

  • onStartCommand()

当另一个组件(如 Activity)通过调用 startService() 请求启动服务时,系统将调用此方法。一旦执行此方法,服务即会启动并可在后台无限期运行。 如果您实现此方法,则在服务工作完成后,需要由您通过调用 stopSelf() 或 stopService() 来停止服务。(如果您只想提供绑定服务,则无需实现此方法。)

  • onBind()

当另一个组件想通过调用 bindService() 与服务绑定(例如执行 RPC)时,系统将调用此方法。在此方法的实现中,您必须通过返回 IBinder 提供一个接口,供客户端用来与服务进行通信。请务必实现此方法,但如果您并不希望允许绑定,则应返回 null。

  • onCreate()

首次创建服务时,系统将调用此方法来执行一次性设置程序(在调用 onStartCommand() 或 onBind() 之前)。如果服务已在运行,则不会调用此方法。

  • onDestroy()

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

另外,Service 还有一个扩展类 IntentService, IntentService 是 Service 的子类,它使用工作线程逐一处理(而不是同时)所有启动请求。如果您不要求服务同时处理多个请求,这是最好的选择。 您只需实现 onHandleIntent() 方法即可,该方法会接收每个启动请求的 Intent,使您能够执行后台工作。

大多数情况我们都不需要服务同时处理多个请求(这样会存在线程安全问题),所以通常我们更应该使用 IntentService 来实现服务, 可以极大的简化我们创建服务的工作。

IntentService 帮助我们执行以下操作:

  • 创建默认的工作线程(非UI线程),用于在应用的主线程外执行传递给 onStartCommand() 的所有 Intent。
  • 创建工作队列,用于将 Intent 逐一传递给 onHandleIntent() 实现,这样您就永远不必担心多线程问题
  • 在处理完所有启动请求后停止服务,因此您永远不必调用 stopSelf()。
  • 提供 onBind() 的默认实现(返回 null)。
  • 提供 onStartCommand() 的默认实现,可将 Intent 依次发送到工作队列和 onHandleIntent() 实现。

所以我们在使用 IntentService 创建服务的时候只需要实现 onHandleIntent() 来完成客户端提供的工作。

public class HelloIntentService extends IntentService {

  /**
   * A constructor is required, and must call the super IntentService(String)
   * constructor with a name for the worker thread.
   */
  public HelloIntentService() {
      super("HelloIntentService");
  }

  /**
   * The IntentService calls this method from the default worker thread with
   * the intent that started the service. When this method returns, IntentService
   * stops the service, as appropriate.
   */
  @Override
  protected void onHandleIntent(Intent intent) {
      // Normally we would do some work here, like download a file.
      // For our sample, we just sleep for 5 seconds.
      try {
          Thread.sleep(5000);
      } catch (InterruptedException e) {
          // Restore interrupt status.
          Thread.currentThread().interrupt();
      }
  }
}

如果你还需要重写其他方法,例如 onCreate(), onStartCommand() 需要注意不要删除 super() 方法,以保证父类的方法不被覆盖掉。例如,onStartCommand() 必须返回默认实现(即,如何将 Intent 传递给 onHandleIntent()):

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show();
    return super.onStartCommand(intent,flags,startId);
}

声明 Service

如同 Activity(以及其他组件)一样,您必须在应用的清单文件(AndroidManifest.xml)中声明所有服务。

<manifest ... >
  ...
  <application ... >
      <service android:name=".ExampleService" />
      ...
  </application>
</manifest>

并行执行服务

上面的 IntentService 使用队列的形式对工作逐一处理,而如果我们需要同时执行某些工作就需要对 Service 进行扩展了,下面我们先使用 Service 模拟一下对 IntentService 的实现。

public class HelloService extends Service {
  private Looper mServiceLooper;
  private ServiceHandler mServiceHandler;

  // Handler that receives messages from the thread
  private final class ServiceHandler extends Handler {
      public ServiceHandler(Looper looper) {
          super(looper);
      }
      @Override
      public void handleMessage(Message msg) {
          // Normally we would do some work here, like download a file.
          // For our sample, we just sleep for 5 seconds.
          try {
              Thread.sleep(5000);
          } catch (InterruptedException e) {
              // Restore interrupt status.
              Thread.currentThread().interrupt();
          }
          // Stop the service using the startId, so that we don't stop
          // the service in the middle of handling another job
          stopSelf(msg.arg1);
      }
  }

  @Override
  public void onCreate() {
    // Start up the thread running the service.  Note that we create a
    // separate thread because the service normally runs in the process's
    // main thread, which we don't want to block.  We also make it
    // background priority so CPU-intensive work will not disrupt our UI.
    HandlerThread thread = new HandlerThread("ServiceStartArguments",
            Process.THREAD_PRIORITY_BACKGROUND);
    thread.start();

    // Get the HandlerThread's Looper and use it for our Handler
    mServiceLooper = thread.getLooper();
    mServiceHandler = new ServiceHandler(mServiceLooper);
  }

  @Override
  public int onStartCommand(Intent intent, int flags, int startId) {
      Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show();

      // For each start request, send a message to start a job and deliver the
      // start ID so we know which request we're stopping when we finish the job
      Message msg = mServiceHandler.obtainMessage();
      msg.arg1 = startId;
      mServiceHandler.sendMessage(msg);

      // If we get killed, after returning from here, restart
      return START_STICKY;
  }

  @Override
  public IBinder onBind(Intent intent) {
      // We don't provide binding, so return null
      return null;
  }

  @Override
  public void onDestroy() {
    Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show();
  }
}

上面的 onStartCommand() 方法中并没有同时执行多个请求,当然你可以更改这里,为每一个请求创建一个新线程,然后立即执行这些线程,实现并行执行。

onStartCommand

前面我们已经多次提到这个方法,这个方法有一个 Int 类型的返回值,当我们的服务在系统资源紧张后被杀死后重新启动的一些差异,我们来详细看一下这些返回值的含义:

  • START_NOT_STICKY

系统不会重新创建此服务,除非有挂起的 Intent 要传递。这是最安全的选项,可以避免在不必要时以及应用能够轻松重启所有未完成的作业时运行服务。

  • START_STICKY

系统会重新创建此服务,并调用 onStartCommand() 方法。这种适用于无限期运行的服务,比如等待作业的音乐播放器。

  • START_REDELIVER_INTENT

系统会重新创建服务,,并通过传递给服务的最后一个 Intent 调用 onStartCommand()。任何挂起 Intent 均依次传递。这适用于主动执行应该立即恢复的作业(例如下载文件)的服务。

使用 bindService

需要在 Service 中实现 IBinder 来绑定 Service 相关数据或对象。

public class MyBinder extends Binder{

    public TestService getService(){
        return TestService.this;
    }
}

在启动 Service 的 Activity 中实现 ServiceConnection 接口:

 private ServiceConnection mServiceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder binder) {
        TestService.MyBinder myBinder = (TestService.MyBinder)binder;
        service = myBinder.getService();
        service.getRandomNumber();
        Log.d(TestService.TAG, "onServiceConnection.getRandomNumber = " + service.getRandomNumber());
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        Log.d(TestService.TAG, "onServiceDisconnected");
    }
};

使用 bindService 和 unbindService 绑定和解绑 Service:

Intent intent = new Intent(this, TestService.class);
bindService(intent, mServiceConnection, Service.BIND_AUTO_CREATE);
unbindService(mServiceConnection);

当我们与之绑定的 Activity finish 后,对应的 Service 也会 onDestroy 掉。

系统广播

Android 系统中会为我们提供一些订阅服务,例如网络状态,飞行模式,电池电量等,我们可以去订阅这些广播,然后就会在发生变化时通知到应用内。但是要注意的是不要滥用广播,这样可能会造成系统性能降低。

Android 系统广播在版本不断更新的过程中做出了很多变化:

  • 从Android 9(API级别28)开始,NETWORK_STATE_CHANGED_ACTION广播不会接收有关用户位置或个人身份识别数据的信息。此外,如果您的应用安装在运行Android 9或更高版本的设备上,则来自Wi-Fi的系统广播不包含SSID,BSSID,连接信息或扫描结果。 要获取此信息,请调用getConnectionInfo()。

  • 从Android 8.0(API级别26)开始,系统对清单声明(AndroidManifest.xml)的接收广播施加了额外的限制。如果您的应用针对的是 Android 8.0 或更高版本,则无法使用清单文件的声明为大多数隐式广播声明接收方(广播不会专门针对您的应用发送)。 当用户主动使用您的应用时,您仍然可以使用上下文注册的接收器。

  • Android 7.0(API级别24)及更高版本不发送这些系统广播:ACTION_NEW_PICTURE, ACTION_NEW_VIDEO。此外,针对Android 7.0及更高版本的应用必须使用registerReceiver(BroadcastReceiver,IntentFilter)注册CONNECTIVITY_ACTION广播。 在清单中声明接收器不起作用。

如何注册广播

注册广播的方式有两种,一种是通过清单文件(AndroidMainfest.xml)配置,另一种是动态使用 registerReceiver 接收。

<receiver android:name=".MyBroadcastReceiver"  android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
        <action android:name="android.intent.action.INPUT_METHOD_CHANGED" />
    </intent-filter>
</receiver>
IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
this.registerReceiver(br, filter);

处理接收的广播

public class MyBroadcastReceiver extends BroadcastReceiver {
    private static final String TAG = "MyBroadcastReceiver";
    @Override
    public void onReceive(Context context, Intent intent) {
        StringBuilder sb = new StringBuilder();
        sb.append("Action: " + intent.getAction() + "\n");
        sb.append("URI: " + intent.toUri(Intent.URI_INTENT_SCHEME).toString() + "\n");
        String log = sb.toString();
        Log.d(TAG, log);
        Toast.makeText(context, log, Toast.LENGTH_LONG).show();
    }
}

这里的 onReceive() 方法被系统认为是前台进程,但是,一旦代码从 onReceive() 方法返回 BroadcastReceiver就不再处于活动状态,系统将其进程视为低优先级进程并且可能杀死它以使资源可用于其他更重要的过程。所以,我们不应该在这里上时间执行后台线程,否则可能会被杀死或释放,要避免这种情况,您应该调用goAsync()(如果您希望在后台线程中处理广播更多时间)或使用JobScheduler从接收器调度JobService,那么系统知道该进程继续执行活动工作。如下面示例:

public class MyBroadcastReceiver extends BroadcastReceiver {
    private static final String TAG = "MyBroadcastReceiver";

    @Override
    public void onReceive(Context context, Intent intent) {
        final PendingResult pendingResult = goAsync();
        Task asyncTask = new Task(pendingResult, intent);
        asyncTask.execute();
    }

    private static class Task extends AsyncTask<String, Integer, String> {

        private final PendingResult pendingResult;
        private final Intent intent;

        private Task(PendingResult pendingResult, Intent intent) {
            this.pendingResult = pendingResult;
            this.intent = intent;
        }

        @Override
        protected String doInBackground(String... strings) {
            StringBuilder sb = new StringBuilder();
            sb.append("Action: " + intent.getAction() + "\n");
            sb.append("URI: " + intent.toUri(Intent.URI_INTENT_SCHEME).toString() + "\n");
            String log = sb.toString();
            Log.d(TAG, log);
            return log;
        }

        @Override
        protected void onPostExecute(String s) {
            super.onPostExecute(s);
            // Must call finish() so the BroadcastReceiver can be recycled.
            pendingResult.finish();
        }
    }
}

发送广播

除了系统发送一些广播外,我们自己也可以发送一些广播给自己的应用内部或者其他应用,发送广播的方式有下面几种:

  • sendBroadcast(Intent) 最常用最简单的发送广播方式,此广播没有顺序。

    Intent intent = new Intent();
    intent.setAction("com.example.broadcast.MY_NOTIFICATION");
    intent.putExtra("data","Notice me senpai!");
    sendBroadcast(intent);
    
  • sendOrderedBroadcast(Intent, String) 可以发送一个传递广播(可拦截),并控制广播的优先级别。

  • LocalBroadcastManager.sendBroadcast 发送应用内广播,如果你是在同一个应用内发送本地广播,建议使用。