Android 服务初学者指南






4.87/5 (9投票s)
探索和实现 Android 服务并发送通知。
- 下载 RemoteApplication.zip - 360.8 KB
- 下载 remoteapplication-release-apk.zip - 54.2 KB
- 下载 androidservices-release-apk.zip - 104.4 KB
- 下载 AndroidServices.zip - 609 KB
![]() |
引言
我们已经习惯了看到带有可见 UI 和 UI 控件的应用。这些是通过 Android Activities 创建的。然而,应用中还有其他一些在后台默默工作的不可见组件。它们就是那些“没有脸的”Android 服务!例如,当用户通过点击应用 UI 上的按钮发起从互联网下载视频的操作时;实际的下载任务就会被移交给一个服务在后台安静地执行;而用户可以继续与其他应用的交互,不受漫长的下载过程影响;当下载完成后,该服务可以通知用户。
Android 服务在帮助提供令人愉悦且无缝的用户体验方面发挥着重要作用。本文将带领您踏上探索和实现 Android 服务以及从服务发送通知的旅程。
了解基础知识
服务是具有以下特征的应用组件
-
没有 UI;
-
在后台运行;
-
在其宿主进程的主线程中运行。因此,如果服务正在处理 CPU 密集型或涉及 I/O 阻塞的任务,那么它应该在一个单独的线程中运行。
-
由同一应用或不同应用的其他活动或应用组件调用,以执行需要长时间运行的任务;以及
-
拥有独立于调用它们的活动和应用组件的生命周期。换句话说,服务可以比它们的调用者更长寿。
通常,服务被创建为扩展“服务”基类的 Java 类。根据实际实现,服务可以采取以下一种或两种形式——“启动服务”和“绑定服务”。详细信息将在表 1 中解释。
启动服务 | 绑定服务 | |
---|---|---|
启动服务 | 当一个应用组件(如活动)通过调用“startService()”来启动它时,就会创建一个“启动服务”。 | 当一个应用组件(如活动)通过调用“bindService()”绑定到它时,就会创建一个“绑定服务”。 |
Characteristics | “启动服务”用于执行长时间运行的操作,例如通过网络下载或上传媒体文件,除了初始请求服务外,不需要与调用者进行任何交互。 | “绑定服务”通过一个编程接口将调用者和服务“绑定”在一起,该接口使调用者能够与服务进行交互,例如发送请求和获取结果。一个很好的例子是 Android 系统中包含的定位服务,它公开了多个方法,任何了解位置的应用都可以使用这些方法,而无需重复造轮子。 |
停止服务 | “启动服务”即使在调用者停止后也会无限期地运行。当服务不再需要时,通过在服务内部调用“stopSelf()”或从其他组件调用“stopService()”显式停止服务是一种好习惯。 | “绑定服务”只要至少有一个应用组件绑定到它,就会运行。当所有客户端通过调用“unbindService()”解除绑定后,系统将销毁绑定服务。 |
生命周期 | ![]() |
![]() |
服务可以同时被“启动”和“绑定”。在这种情况下,调用“stopSelf()”或“stopService()”并不会立即停止服务,直到所有客户端都解除绑定。同样,如果服务仍处于“启动”状态,则将所有客户端从该服务解除绑定并不会立即导致其被销毁。
“服务”基类带有一些回调方法。创建服务时,不必实现所有回调方法。实际上,在服务中必须实现的回调方法是“onBind()”。但是,您可能需要重写一些回调方法才能使服务按您想要的方式运行。最重要的回调方法将在表 2 中给出。
回调 | 说明 |
---|---|
onCreate() |
系统在首次创建服务时调用此方法以执行一次性设置。例如,您可以在“onCreate”方法中创建一个“HandlerThread”对象来启动一个具有关联“Looper”(消息循环)的新线程。代码片段如下 package com.peterleow.androidservices; // ... private Looper looper; @Override public void onCreate() { HandlerThread thread = new HandlerThread("BoundService", android.os.Process.THREAD_PRIORITY_BACKGROUND); thread.start(); looper = thread.getLooper(); // ... } |
onStartCommand() |
当另一个组件(如活动)通过调用“startService(Intent)”显式启动服务并为其提供必要的参数(包括一个唯一的整数“startId”,表示当前启动请求)时,系统会调用此方法。“startId”是系统在“startService(Intent)”调用时生成的递增值。当服务尝试调用“stopSelf(startId)”方法根据当前“startId”停止自身,而此时一个新的“startService()”已被调用,生成了一个新的“startId”但尚未到达“onStartCommand()”,这将导致“startId”出现两个不同的版本。如果传递给“stopSelf(startId)”的“startId”与“startService(Intent)”生成的“startId”不匹配,则“stopSelf(startId)”将被中止。 “onStartCommand()”的代码片段如下 @Override public int onStartCommand(Intent intent, int flags, int startId) { Message msg = handler.obtainMessage(); msg.arg1 = startId; msg.what = intent.getExtras().getInt("MESSAGE_TYPE"); handler.sendMessage(msg); // Restart the service if got killed return START_STICKY; } “onStartCommand()”方法必须返回一个整数,该整数指示系统在方法返回时服务被终止时应如何处理服务。它必须采用以下常量之一
|
onBind() |
当另一个组件(如活动)希望通过调用“bindService()”绑定到服务时,系统会调用此方法。此方法必须返回一个“IBinder”,它表示客户端可以用来与服务通信的接口。例如 private final IBinder binder = new MyBinder(); public class MyBinder extends Binder { LocalBoundService getServiceInstance() { // Return this instance of LocalBoundService // from which the clients can call any public methods in here return LocalBoundService.this; } } @Override public IBinder onBind(Intent intent) { return binder; } 每个服务都必须实现此方法,无论它是“启动服务”还是“绑定服务”。对于纯粹的“启动服务”,请将此方法返回 null。 |
onDestroy() |
当服务被停止或解除绑定后,系统会调用此方法通知服务它将被移除。这是服务将收到的最后一个调用,它应该实现此方法来释放资源。 |
为了总结到目前为止我们对服务的普遍讨论,我已经提供了一个构建服务的模板,如下所示
package com.peterleow.androidservices; import android.app.Service; import android.content.Intent; import android.os.IBinder; /** * Created by Peter Leow on 20/9/2014. */ public class ServiceTemplate extends Service { // Dictate the behavior if the service is killed // when the onStartCommand returns int reStartMode; //interface for binding with clients IBinder binder; /** * The system calls this method when the service is first created. */ @Override public void onCreate() { } /** * The system call this method when a client want * to start the service by calling startService() */ @Override public int onStartCommand(Intent intent, int flags, int startId) { return reStartMode; } /** * The system call this method when a client want to bind * to the service by calling bindService() */ @Override public IBinder onBind(Intent intent) { return binder; } @Override public void onDestroy() { } }
动手实践
掌握了 Android 服务的基础知识后,您现在可以深入研究其中的每一个,通过一系列动手练习来实现它们,如下所示
准备工作
我准备了两个 Android 项目供下载——“AndroidServices.zip”和“RemoteApplication.zip”。“AndroidServices”是一个不完整的应用,包含各种练习所需的活动和资源,但没有实现服务的代码。您将通过本教程的学习,一点一点地添加代码来完成练习。“RemoteApplication”则是一个可以正常工作的应用,一旦完成,“AndroidServices”应用中的远程绑定服务将能够调用它。
您将通过以下步骤准备好
-
下载并解压“AndroidServices.zip”,您应该会在电脑上看到“AndroidServices”文件夹
-
启动 Android Studio。
-
如果它打开了现有项目,请单击文件 > 关闭项目切换到欢迎屏幕。
-
在欢迎屏幕的“快速入门”页面上,单击“打开项目”。
-
浏览到“AndroidServices”项目,然后单击“确定”在 Android Studio 中打开它。
-
对“RemoteApplication.zip”也执行相同的操作。
让我们看看不完整的“AndroidServices”项目提供了什么。
启动页面是“MainActivity”,它包含四个按钮,用于导航到四个其他活动,每个活动都将启动一种特定类型的服务。有关应用完全构建后的视觉构成,请参阅图 1。
![]() |
图 1:Android Services App 的视觉构成
|
启动服务
创建“启动服务”有两种方法:创建一个扩展以下两个类之一的 Java 类
-
“Service”基类,您将重写其部分回调方法以根据服务的目的产生期望的行为。您还必须负责在新线程中实现此项工作,以处理长时间运行的操作。
-
“IntentService”,它是“Service”基类的子类。此类消除了重写单个回调方法和创建新线程的繁琐工作。您只需实现“onHandleIntent()”,它将创建一个工作线程来处理所有启动请求。
如果使用“IntentService”子类实现“启动服务”比使用“Service”基类简单得多,那么为什么我们不whenever 需要创建“启动服务”时都使用“IntentService”类呢?别急!与“Service”基类可以处理并发调用“onStartCommand()”不同,“IntentService”一次只能处理一个启动请求。因此,请明智地选择它们。
稍后将在“通知服务”部分使用“IntentService”创建一个用于发送通知的启动服务。现在,您将基于“Service”基类创建一个启动服务。
创建启动服务
在“AndroidServices”项目中,打开“StartedService.java”,如下所示
package com.peterleow.androidservices; import android.app.Service; import android.content.Intent; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.Process; import android.widget.Toast; /** * Created by Peter Leow on 18/9/2014. */ public class StartedService extends Service { @Override public IBinder onBind(Intent intent) { return null; } }
请注意,您必须实现的回调方法是“onBind()”。由于此服务将被“启动”,因此只需使其返回 null。您将添加代码使其具有可用性。
创建一个名为“ServiceHandler”的内部类,该类扩展“Handler”类,以接收和处理由线程(稍后创建)的“Looper”(消息队列)分派的消息。
// ... // Message types to the service to display a message static final int MSG_STOP_SERVICE = 0; static final int MSG_HELLO = 1; static final int MSG_HAPPY_BIRTHDAY = 2; // 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) { switch (msg.what) { case MSG_STOP_SERVICE: Toast.makeText(getApplicationContext(), "Service is shutting down...", Toast.LENGTH_SHORT).show(); stopSelf(msg.arg1); break; case MSG_HELLO: Toast.makeText(getApplicationContext(), "Hello, Code Project! Greeting from Android Service.", Toast.LENGTH_SHORT).show(); break; case MSG_HAPPY_BIRTHDAY: Toast.makeText(getApplicationContext(), "Happy Birthday to you!", Toast.LENGTH_SHORT).show(); break; default: super.handleMessage(msg); } } }
在“onCreate()”回调方法中,基于“HandlerThread”类创建一个具有关联“Looper”(消息队列)的新线程,然后通过将其与新线程关联来实例化(上面的)“Handler”类。例如
// ... private ServiceHandler handler; // ... @Override public void onCreate() { Toast.makeText(getApplicationContext(), "Service is starting...", Toast.LENGTH_SHORT).show(); HandlerThread thread = new HandlerThread("StartedService", Process.THREAD_PRIORITY_BACKGROUND); thread.start(); // Get the HandlerThread's Looper and use it for our Handler looper = thread.getLooper(); handler = new ServiceHandler(looper); }
在“onStartCommand()”回调方法中,调用“obtainMessage()”方法从“Handler”获取一个新的“Message”对象,并将来自“Intent”对象的 extras 和“startId”分配给此“Message”对象的相应参数,然后调用“sendMessage()”方法将此“Message”对象排队,以便由“Handler”类中的“handleMessage(Message)”方法处理。例如
@Override public int onStartCommand(Intent intent, int flags, int startId) { Message msg = handler.obtainMessage(); msg.arg1 = startId; msg.what = intent.getExtras().getInt("MESSAGE_TYPE"); handler.sendMessage(msg); // Restart the service if it got killed return START_STICKY; }
您已经构建了一个“启动服务”组件。让我们来处理将调用此服务来执行工作的活动——“StartedServiceActivity”。
在 Manifest 中声明服务
与活动一样,每个服务类在使用前都必须在应用的清单文件中声明。例如
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.peterleow.androidservices" > <application ... > ... <activity ...> ... </activity> <service android:name=".StartedService" /> </application> </manifest>
创建客户端组件
打开“StartedServiceActivity.java”;在“onCheckedChanged()”事件处理程序中,添加以下代码,通过调用“startService(Intent)”方法启动“StartedService”服务,并将包含选定单选按钮的 tag 属性(由“Message_Type”键标识)的 bundle extra 传递给它。“startService(Intent)”方法会立即返回,而 Android 系统会调用目标服务——“StartedService”的“onStartCommand()”方法。如果服务尚未运行,系统将首先调用服务中的“onCreate()”方法来启动服务,然后调用“onStartCommand()”方法来启动服务。
boolean isServiceStarted = false; // ... @Override public void onCheckedChanged(RadioGroup radioGroup, int i) { int radioButtonId = radioGroup.getCheckedRadioButtonId(); RadioButton radioButton = (RadioButton)radioGroup.findViewById(radioButtonId); int messageType = Integer.parseInt(radioButton.getTag().toString()); Intent intent = new Intent(this, StartedService.class); Bundle bundle = new Bundle(); bundle.putInt("MESSAGE_TYPE", messageType); intent.putExtras(bundle); if (startService(intent) == null) return; isServiceStarted = true; }
在活动的“onStop()”方法(当活动在屏幕上不再可见时由系统调用)中添加以下代码,通过传递零的“Message_Type” extra 来指示“StartedService”停止自身。例如
@Override protected void onStop() { super.onStop(); if (isServiceStarted) { Intent intent = new Intent(this, StartedService.class); Bundle bundle = new Bundle(); bundle.putInt("MESSAGE_TYPE", 0); intent.putExtras(bundle); startService(intent); isServiceStarted = false; } }
测试 1, 2, 3, ...
在真实设备或 AVD 上启动应用,单击启动页面上的“Started Service”按钮导航到“StartedServiceActivity”页面(图 2);第一次单击其中一个单选按钮将启动并将选定的选项发送到“StartedService”服务(图 3);“StartedService”将响应一条消息(图 4)。
![]() |
![]() |
|
![]() |
![]() |
图 2:StartedServiceActivity | 图 3:启动并将选定的选项发送到服务 | 图 4:来自服务的响应 |
当您返回启动页面时,服务将被关闭。
本地绑定服务
如果要允许客户端组件与服务进行交互,那么该服务必须实现为“绑定服务”。“绑定服务”通过“Binder”类提供了一个编程接口,客户端组件可以与之交互。“绑定服务”的实现方式不同,具体取决于服务是在宿主应用内部使用,还是被其他应用使用,或跨进程使用。我将前者称为“本地绑定服务”,后者称为“远程绑定服务”。我们将在下一节处理“远程绑定服务”。现在,您将创建一个本地绑定服务。
创建本地绑定服务
在“AndroidServices”项目中,打开“LocalBoundService.java”,并创建一个名为“MyBinder”的内部类,该类扩展“Binder”类。在“MyBinder”类中,创建一个名为“getServiceInstance()”的方法,该方法返回该服务的一个实例。例如
public class MyBinder extends Binder { LocalBoundService getServiceInstance() { // Return this instance of LocalBoundService // from which the clients can call any public methods in here return LocalBoundService.this; } }
实例化一个“MyBinder”对象。例如
private final IBinder binder = new MyBinder();
在“onBind()”回调方法中,将“MyBinder”对象返回给想要与此服务绑定的客户端组件。例如
@Override public IBinder onBind(Intent intent) { return binder; }
创建一个名为“add()”的公共方法,它简单地返回其两个参数的和,如下所示
public int add(int first, int second) { return first + second; }
与此服务绑定的客户端组件将能够通过“MyBinder”对象访问服务内的此公共方法。换句话说,“MyBinder”对象充当“本地绑定服务”与其绑定客户端组件之间的接口。
在 Manifest 中声明服务
在应用的清单文件中声明“LocalBoundService”类。例如
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.peterleow.androidservices" > <application ... > ... <activity ...> ... </activity> <service android:name=".LocalBoundService" /> </application> </manifest
创建客户端组件
由于绑定过程是异步的,“bindService()”方法在调用后立即返回。为了接收“IBinder”,客户端必须创建一个“ServiceConnection”实例,并将其作为“bindService()”方法的一个参数传递,该方法启动绑定请求。“ServiceConnection”包含两个回调方法——“onServiceConnected()”和“onServiceDisconnected()”。当与服务的连接建立时,系统会调用“onServiceConnected()”回调方法来传递“IBinder”。当连接丢失时,它会调用“onServiceDisconnected()”方法。
打开“LocalBoundServiceActivity.java”,并创建一个“ServiceConnection”类的实例。重写“onServiceConnected()”回调方法以从“IBinder”获取“LocalBoundService”服务的一个实例,并在建立连接时将其绑定状态设置为“true”。相反,在“onServiceDisconnected()”方法中将绑定状态设置为“false”。
LocalBoundService localBoundService; // ... // Define the callbacks to monitor the state of a service private ServiceConnection serviceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName className, IBinder service) { // When bound, get an instance of the local bound service LocalBoundService.MyBinder binder = (LocalBoundService.MyBinder) service; localBoundService = binder.getServiceInstance(); isBound = true; } @Override public void onServiceDisconnected(ComponentName arg0) { isBound = false; } };
在活动的“onStart()”方法中添加以下代码。该代码将创建一个显式 Intent,该 Intent 标识要绑定的服务——“LocalBoundService”,并调用“bindService()”,该方法将此 Intent 作为第一个参数,并将上面创建的“ServiceConnection”实例作为第二个参数来启动对该服务的绑定请求。第三个参数——“BIND_AUTO_CREATE”——是一个标志,指示如果服务尚未运行,则应创建它。其他可能的标志值是“BIND_ABOVE_CLIENT”、“BIND_DEBUG_UNBIND”和“BIND_NOT_FOREGROUND”或 0 表示无。
“ServiceConnection”实例将在服务连接时收到服务对象,并通过其两个回调方法了解连接状态的变化。例如
@Override protected void onStart() { super.onStart(); // Bind to local bound service Intent intent = new Intent(this, LocalBoundService.class); bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); }
在“onStop()”方法中添加以下代码以解除绑定服务,这将触发“ServiceConnection”实例的“onServiceDisconnected()”回调方法,将绑定状态更改为“false”。
@Override protected void onStop() { super.onStop(); // Unbind from local bound service if (isBound) { unbindService(serviceConnection); } }
最后,为“Add them up...”按钮创建一个名为“add()”的“onClick”事件处理程序。单击此按钮时,它会将文本字段中的两个输入整数传递给“LocalBoundService”服务实例的“add()”方法,该方法将这两个整数相加并将结果返回给客户端组件——“LocalBoundServiceActivity”进行显示。例如
public void add(View view) { if (isBound) { // Call a method in the local bound service instance, e.g. add(int, int). // I have omitted inputs validation here. That is your homework int firstNumber = Integer.parseInt(((EditText) findViewById(R.id.editTextFirstNumber)).getText().toString()); int secondNumber = Integer.parseInt(((EditText) findViewById(R.id.editTextSecondNumber)).getText().toString()); int sum = localBoundService.add(firstNumber, secondNumber); ((TextView) findViewById(R.id.textViewAnswer)).setText(String.valueOf(sum)); } }
测试 1, 2, 3, ...
在真实设备或 AVD 上启动应用;单击启动页面上的“Local Bound Service”按钮导航到“LocalBoundServiceActivity”页面(图 5)并进行测试。请注意,输入验证代码已省略,您可以作为作业自己添加。
![]() |
图 5:LocalBoundServiceActivity |
当您返回启动页面时,服务将关闭。
远程绑定服务
除了本地绑定服务(在与调用组件相同的进程中运行)之外,您还可以实现服务以允许从远程客户端组件访问。Android 提供了“Messenger”类来进行进程间通信到服务。
创建远程绑定服务
在“AndroidServices”项目中,打开“RemoteBoundService.java”,并创建一个名为“ServiceHandler”的内部类,该类扩展“Handler”类,以接收和处理由线程(稍后创建)的“Looper”(消息队列)分派的消息。这部分代码已在“StartedService.java”中解释过。
// ... // Message types to the service to display a message static final int MSG_STOP_SERVICE = 0; static final int MSG_HELLO = 1; static final int MSG_HAPPY_BIRTHDAY = 2; // 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) { switch (msg.what) { case MSG_STOP_SERVICE: Toast.makeText(getApplicationContext(), "Service is shutting down...", Toast.LENGTH_SHORT).show(); stopSelf(msg.arg1); break; case MSG_HELLO: Toast.makeText(getApplicationContext(), "Hello, Code Project! Greeting from Android Service.", Toast.LENGTH_SHORT).show(); break; case MSG_HAPPY_BIRTHDAY: Toast.makeText(getApplicationContext(), "Happy Birthday to you!", Toast.LENGTH_SHORT).show(); break; default: super.handleMessage(msg); } } }
在“onCreate()”回调方法中,基于“HandlerThread”类创建一个具有关联“Looper”(消息队列)的新线程,然后通过将其与新线程关联来实例化(上面)的“Handler”类。接下来,实例化一个指向此“Handler”实例的“Messenger”对象。
// ... private ServiceHandler handler; Messenger messenger; // ... @Override public void onCreate() { Toast.makeText(getApplicationContext(), "Service is starting...", Toast.LENGTH_SHORT).show(); HandlerThread thread = new HandlerThread("StartedService", Process.THREAD_PRIORITY_BACKGROUND); thread.start(); // Get the HandlerThread's Looper and use it for our Handler looper = thread.getLooper(); handler = new ServiceHandler(looper); messenger = new Messenger(handler); }
在“onBind()”回调方法中,将“Messenger”对象使用的“IBinder”返回给想要与此服务绑定的客户端组件。例如
@Override public IBinder onBind(Intent intent) { Toast.makeText(getApplicationContext(), "Service is binding...", Toast.LENGTH_SHORT).show(); return messenger.getBinder(); }
在 Manifest 中声明服务
在应用的清单文件中声明“RemoteBoundService”类。例如
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.peterleow.androidservices" > <application ... > ... <activity ...> ... </activity> <service android:name=".RemoteBoundService" /> </application> </manifest
创建客户端组件
打开“RemoteBoundServiceActivity.java”,并创建一个“ServiceConnection”类的实例,该类有两个回调方法——“onServiceConnected()”和“onServiceDisconnected()”。“ServiceConnection”类的作用已在关于“本地绑定服务”的先前会话中解释过。在“onServiceDisconnected()”中,它使用服务“onBind()”回调方法返回的“IBinder”来实例化引用(上面)服务“Handler”实例的“Messenger”对象。客户端组件使用此“Messenger”对象将消息作为“Message”对象发送到服务,服务进而将其交给“Handler”实例的“handleMessage()”方法进行处理。例如
Messenger messenger = null; boolean isBound = false; // Define the callbacks on service binding to be pass to bindService() private ServiceConnection serviceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName className, IBinder service) { // Get a instance of Messenger from the IBinder object // return from onBind() messenger = new Messenger(service); isBound = true; } @Override public void onServiceDisconnected(ComponentName arg0) { messenger = null; isBound = false; } };
在“onStart()”方法中添加以下代码以绑定服务。
@Override protected void onStart() { super.onStart(); Intent intent = new Intent(this, RemoteBoundService.class); bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); }
在“onStop()”方法中添加以下代码以解除绑定服务。
@Override protected void onStop() { super.onStop(); if (isBound) { unbindService(serviceConnection); } }
在“onCheckedChanged()”事件处理程序中,添加以下代码,通过调用“Message.obtain()”方法将选定单选按钮的 tag 属性添加到“Message”对象中,然后通过“Messenger”对象的“send()”方法将此消息对象传递给远程绑定服务——“RemoteBoundService”。例如
@Override public void onCheckedChanged(RadioGroup radioGroup, int i) { int radioButtonId = radioGroup.getCheckedRadioButtonId(); RadioButton radioButton = (RadioButton)radioGroup.findViewById(radioButtonId); int messageType = Integer.parseInt(radioButton.getTag().toString()); if (!isBound) { bindService(new Intent(this, RemoteBoundService.class), serviceConnection, Context.BIND_AUTO_CREATE); } // Send a message to the service Message msg = Message.obtain(null, messageType, 0, 0); try { messenger.send(msg); } catch (RemoteException e) { e.printStackTrace(); } if (messageType == 0){ unbindService(serviceConnection); isBound = false; } }
测试 1, 2, 3, ...
在真实设备或 AVD 上启动应用;单击启动页面上的“Remote Bound Service”按钮导航到“RemoteBoundServiceActivity”页面,该页面将在页面加载时立即启动绑定过程(图 6 和 7);选择其中一个单选按钮会将选定的选项发送到“RemoteBoundService”服务,该服务将响应一条消息(图 8)。
![]() |
![]() |
|
![]() |
![]() |
图 6:onStart() | 图 7:onStart() | 图 8:来自服务的响应 |
到目前为止,此测试已在本地进行,客户端和服务都位于同一应用内。如何从远程客户端访问此服务?请继续阅读...
设置(真实)远程服务
在应用的清单文件中,在远程绑定服务——“RemoteBoundService”的
<service android:name=".RemoteBoundService" android:exported="true" > <intent-filter> <action android:name="com.peterleow.androidservices" /> </intent-filter> </service>
在想要访问此服务的外部客户端中,创建一个新的 Intent,该 Intent 接收服务的意图过滤器的操作名称,并将其作为参数传递给“startService()”或“bindService()”。例如
Intent intent = new Intent("com.peterleow.androidservices"); bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
![]() |
![]() |
|
![]() |
![]() |
图 9:onStart() | 图 10:onStart() | 图 11:来自服务的响应 |
Android 通知
与活动不同,活动会向用户提供任务的可视化提示,服务在后台运行,它们需要一些方法来通知用户新事件或更新用户有关服务正在处理的任务的进度。在这方面,Android 提供了两种通知选项——“Toast 通知”和“状态栏通知”。
Toast 通知
Toast 通知会短暂地出现在屏幕底部中心附近的一个小弹出窗口中,并显示一条简短的消息,持续时间有限,而当前活动保持可见和响应。您已经在前面的练习中使用 Toast 通知来显示来自服务的响应(例如,图 9 到 10)。
要发送 Toast 通知,首先通过调用其静态“makeText()”方法创建一个“Toast”视图,并为其传递三个参数——要使用的应用程序上下文、要显示的消息以及显示消息的持续时间——“LENGTH_SHORT”或“LENGTH_LONG”。然后可以通过调用“show()”来显示“Toast”视图。例如
Context context = getApplicationContext(); CharSequence text = "Service is binding..."; int duration = Toast.LENGTH_SHORT; Toast toast = Toast.makeText(context, text, duration); toast.show();
或者,您可以简单地将它们链接成一行语句,如下所示
Toast.makeText(context, text, duration).show();
大多数情况下,这就是创建和发送 Toast 通知所需的全部。但是,您可以使用“setGravity(int, int, int)”方法更改 Toast 的位置,而不是默认的底部中心。该方法接受三个参数——一个“Gravity”常量、一个 x 偏移量和一个 y 偏移量。“Gravity”参数表示屏幕上的预定义位置,x 偏移量和 y 偏移量分别提供由“Gravity”参数定义的位置的水平和垂直距离。例如,可以使用以下方法将 Toast 定位在屏幕的顶部中心
toast.setGravity(Gravity.TOP|Gravity.CENTER, 0, 0);
状态栏通知
虽然“Toast 通知”实现简单快捷,但它本质上是短暂的,并且不允许用户后续操作。“状态栏通知”则在状态栏中显示一个图标和一条消息,该消息会一直存在,直到用户选择它以发起后续操作(例如启动活动),因此更适合显示需要用户后续操作的通知。例如,作为状态栏通知实现的电子邮件警报允许收件人选择并展开通知以查看更多详细信息,并启动电子邮件应用以读取完整内容。当我们提到 Android 中的通知时,我们通常会隐式地指“状态栏通知”。
通知基础知识
当通知到达时,它会显示为一个图标,其中包含一个可选的跑马灯文本(图 12 中显示的“New Agenda Alert!”),位于通知区域(屏幕顶部的状态栏)。
![]() |
图 12:状态栏通知 |
当用户在图标上向下滑动时,系统会响应并打开通知抽屉,这是一个 UI 视图(图 13),其中包含有关通知的详细信息。其中一些详细信息是内容标题(“Agenda”)、内容文本(“Arrival of new agenda”)、一个较大的图标以及通知时间。
![]() |
图 13:正常视图抽屉示例 |
图 13 中通知抽屉的视觉样式称为“正常视图”。正常视图的高度限制为 64dp。从 Android 4.1(API 级别 16)开始,通知抽屉中引入了一种新的视觉样式,称为“大视图”(图 14)。两种视图之间最显著的区别是“大视图”抽屉具有更大的详细信息区域。
![]() |
图 14:大视图抽屉示例 |
设置通知优先级
通过为其分配五个优先级级别之一(如表 3 所示),您可以影响 Android 设备与其他通知相比如何以及在哪里显示特定通知。这是为了确保用户始终能看到更重要的通知。
回调 | 说明 |
---|---|
PRIORITY_DEFAULT |
表示默认优先级的整数常量。 Notification.Builder builder = new new Notification.Builder(this).setPriority(Notification.PRIORITY_DEFAULT) |
PRIORITY_MAX |
表示需要用户立即关注或后续操作的最高优先级的整数常量。 Notification.Builder builder = new new Notification.Builder(this).setPriority(Notification.PRIORITY_MAX) |
PRIORITY_HIGH |
表示比 PRIORITY_DEFAULT 标记的通知更重要的通知或警报的整数常量。这些通知可能比默认通知显示更大的尺寸或更高的位置。 Notification.Builder builder = new new Notification.Builder(this).setPriority(Notification.PRIORITY_HIGH) |
PRIORITY_LOW |
表示比 PRIORITY_DEFAULT 标记的通知不太重要的通知的整数常量。这些通知可能比默认通知显示更小的尺寸或更低的位置。 Notification.Builder builder = new new Notification.Builder(this).setPriority(Notification.PRIORITY_LOW) |
PRIORITY_MIN |
表示微不足道的通知(例如天气信息)的整数常量,因此没有紧迫性。系统不会在状态栏中显示它们。用户只有在展开通知抽屉时才会注意到它们。 Notification.Builder builder = new new Notification.Builder(this).setPriority(Notification.PRIORITY_MIN) |
创建和发送通知
要创建通知,首先在“Notification.Builder”对象中指定通知的 UI 信息和操作,例如设置图标、跑马灯文本、内容标题等。接下来,调用“Notification.Builder.build()”方法创建一个包含通知所有规格的“Notification”对象。要发送通知,请将此“Notification”对象通过调用“notify()”方法传递给“NotificationManager”对象。“notify()”方法接受两个参数——一个唯一标识此“Notification”对象的整数,以及“Notification”对象本身。下面的代码片段将产生从图 12 的状态栏图标到图 13 的正常视图抽屉的通知序列。
final Notification.Builder builder = new Notification.Builder(this) .setSmallIcon(R.drawable.ic_peterleow) .setTicker("New Agenda Alert!") .setContentTitle("Agenda") .setContentText("Arrival of new agenda.") .setPriority(Notification.PRIORITY_DEFAULT) .setAutoCancel(true); final Notification notification = builder.build(); final NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(0, notification);
要添加大视图通知抽屉,请调用“Notification.Builder.setStyle()”并为其提供一个“Notification.Style”子类对象,例如可以显示多达五个字符串项的“Notification.InboxStyle”对象。下面的代码片段将产生图 14 中显示的大视图通知抽屉。请注意,此大视图抽屉仅在 Android 4.1(API 级别 16)及更高版本中可用。
Notification.InboxStyle inboxStyle = new Notification.InboxStyle(); inboxStyle.setBigContentTitle("Agenda"); String[] items = new String[5]; items[0] = new String("1. Planning"); items[1] = new String("2. Analysis"); items[2] = new String("3. Design"); items[3] = new String("4. Implementation"); items[4] = new String("5. Maintenance"); for (int c = 0; c < items.length; c++) { inboxStyle.addLine(items[c]); } builder.setStyle(inboxStyle);
更新通知
当同一类事件需要多次发出通知时,您应该考虑更新之前的通知而不是发出新通知。要更新之前的通知,请创建一个通知对象,如上所述,然后调用“NotificationManager”对象的“notify()”方法,并使用与之前通知相同的通知 ID 来发出它。如果之前的通知仍然可见,系统将用新内容更新它。如果之前的通知已被清除,系统将创建一个新的通知。通知可以通过调用“Notification.Builder”对象的“setNumber(int updateCount)”方法在其通知抽屉上显示收到的更新次数。
以下示例代码演示了通过模拟循环更新通知,结果显示在图 15 中。请注意,随着新更新的到来,通知抽屉右下角显示的增量整数。
protected void displayNotification() { final Notification.Builder builder = new Notification.Builder(this) .setSmallIcon(R.drawable.ic_peterleow) .setTicker("New Mail Alert!") .setContentTitle("New Mail!") .setContentText("You've got new mail.") .setPriority(Notification.PRIORITY_DEFAULT) .setAutoCancel(true); final NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); // Start a long-running operation in a background thread new Thread( new Runnable() { @Override public void run() { int notificationID = 1; for (int i = 1; i <= 10; i++) { // Increment notification count builder.setNumber(i); notificationManager.notify(notificationID, builder.build()); // Put the thread to sleep // to simulating long-running operation try { // Sleep for 0.5 seconds Thread.sleep(500); } catch (InterruptedException e) { e.getStackTrace(); } } } } ).start(); }
![]() |
图 15:更新通知 |
为通知附加操作
虽然是可选的,但通常会为通知附加一个操作,这样当用户单击通知抽屉时,它可以直接启动一个活动来执行任何后续工作。但是,从通知抽屉启动新活动不应破坏用户在单击后退按钮时所期望的正常导航。为了保留用户的预期导航体验,我们必须借助“TaskStackBuilder”类来为启动的活动构建一个人工回溯栈,这样从该活动向后导航将可以退出应用并返回主屏幕。步骤如下
-
在“AndroidManifest.xml”中,修改要从通知抽屉启动的活动(例如“NotificationActivity”)的 activity 元素,以指定其父活动。
<activity android:name=".NotificationActivity" android:label="@string/title_activity_notification" android:parentActivityName=".MainActivity" > <meta-data android:name="android.support.PARENT_ACTIVITY" android:value="com.peterleow.androidservices.MainActivity" /> </activity>
-
在 Java 类文件中,创建一个 Intent 对象以启动目标活动,例如“NotificationActivity”
Intent notifyIntent = new Intent(this, NotificationActivity.class);
-
通过调用“TaskStackBuilder.create()”创建一个“TaskStackBuilder”对象。
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
-
通过调用“addParentStack()”将目标活动的整个父栈添加到“TaskStackBuilder”对象中。
stackBuilder.addParentStack(NotificationActivity.class);
-
通过调用“addNextIntent()”将 Intent 对象添加到“TaskStackBuilder”对象中。
stackBuilder.addNextIntent(notifyIntent);
-
通过调用“getPendingIntent()”为“TaskStackBuilder”对象获取一个“PendingIntent”。
PendingIntent notifyPendingIntent = stackBuilder.getPendingIntent( 0, PendingIntent.FLAG_UPDATE_CURRENT );
-
将“PendingIntent”传递给“Notification.Builder”对象的“setContentIntent()”方法,这样当用户单击通知抽屉中的通知文本时,将启动目标活动。
builder.setContentIntent(notifyPendingIntent);
就是这样。添加通知操作的完整代码如下所示
Intent notifyIntent = new Intent(this, NotificationActivity.class); TaskStackBuilder stackBuilder = TaskStackBuilder.create(this); stackBuilder.addParentStack(NotificationActivity.class); stackBuilder.addNextIntent(notifyIntent); PendingIntent notifyPendingIntent = stackBuilder.getPendingIntent( 0, PendingIntent.FLAG_UPDATE_CURRENT ); builder.setContentIntent(notifyPendingIntent);
通知服务
您现在已准备好创建一个用于从“启动服务”发送通知的通知服务。这次,您将通过扩展“IntentService”类来创建“启动服务”。
创建通知服务
在“AndroidServices”项目中,打开“NotificationService.java”,并添加构造函数,重写“onHandleIntent()”以执行服务所需的任务,例如发送通知。
// ... public class NotificationService extends IntentService { public NotificationService() { super("NotificationService"); } @Override protected void onHandleIntent(Intent intent) { // do work } // ... }
请注意,通过扩展“IntentService”类来实现“启动服务”要简单得多。“IntentService”的作用是创建一个工作线程来一次执行所有请求,并在所有工作完成后自行停止。它还处理“Service”基类所需的所有默认实现,例如实现返回 null 的“onBind()”方法。
接下来,创建一个名为“sendNotification()”的方法,其中包含创建和发送通知的代码
protected void sendNotification() { // Configure Normal View final Notification.Builder builder = new Notification.Builder(this) .setSmallIcon(R.drawable.ic_peterleow) .setContentTitle("Downloading...") .setTicker("New Agenda Alert!") .setAutoCancel(true); Intent notifyIntent = new Intent(this, NotificationActivity.class); TaskStackBuilder stackBuilder = TaskStackBuilder.create(this); stackBuilder.addParentStack(NotificationActivity.class); stackBuilder.addNextIntent(notifyIntent); PendingIntent notifyPendingIntent = stackBuilder.getPendingIntent( 0, PendingIntent.FLAG_UPDATE_CURRENT ); builder.setContentIntent(notifyPendingIntent); final NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); // Start a long-running operation in a background thread new Thread( new Runnable() { @Override public void run() { for (int i = 0; i <= 100; i+=5) { builder.setProgress(100, i, false); notificationManager.notify(0, builder.build()); // Put the thread to sleep // to simulating long-running operation try { // Sleep for 0.5 seconds Thread.sleep(500); } catch (InterruptedException e) { e.getStackTrace(); } } builder.setProgress(0,0,false); // Configure Big View Notification.InboxStyle inboxStyle = new Notification.InboxStyle(); String[] items = new String[5]; items[0] = new String("1. Planning"); items[1] = new String("2. Analysis"); items[2] = new String("3. Design"); items[3] = new String("4. Implementation"); items[4] = new String("5. Maintenance"); inboxStyle.setBigContentTitle("Agenda"); for (int c = 0; c < items.length; c++) { inboxStyle.addLine(items[c]); } builder.setStyle(inboxStyle); notificationManager.notify(0, builder.build()); } } ).start(); }
从“onHandleIntent”方法调用“sendNotification()”
@Override protected void onHandleIntent(Intent intent) { sendNotification(); }
在 Manifest 中声明服务
在应用的清单文件中将“NotificationService”声明为服务
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.peterleow.androidservices" > <application ... > ... <activity ...> ... </activity> <service android:name=".NotificationService" /> </application> </manifest>
您已经构建了通知服务。让我们来测试一下!
测试 1, 2, 3, ...
在真实设备或 AVD 上启动应用;单击启动页面上的“Notification”按钮调用“NotificationService”服务,该服务将向用户发送通知。请参阅图 16 到 19 中的操作序列。尽情享受吧!
![]() |
在图标上向下滑动 |
|
图 16:通知警报 | 图 17:正常视图抽屉 | |
下载完成 |
||
![]() |
单击内容区域 |
![]() |
图 19:已启动目标活动 | 图 18:大视图抽屉 |
运行前台服务
当内存不足时,Android 系统会开始关闭服务以回收活动所需的资源,而该活动具有用户焦点。绑定到具有用户焦点的活动的服务不太可能被杀死。避免被系统过早杀死的另一种方法是将服务设置为在前台运行。
将服务声明为在前台运行将提高其优先级,使其不太可能被杀死。权衡是,前台运行的服务必须在“正在进行”标题下(图 20)维护一个持续的通知,以便用户明确了解此服务。一个例子是音乐播放器用来播放歌曲的服务;除其他事项外,通知可能在状态栏中指示正在播放的当前音乐的标题,并提供一种方式供用户启动一个活动来与音乐播放器交互或更改音乐。
![]() |
图 20:正在进行的通知示例 |
要创建一个在前台运行的服务,请在“onCreate()”方法中创建一个“Notification”对象,然后调用“Service”类的“startForeground()”方法,并为其传递两个参数——一个唯一标识“Notification”对象的非零整数和“Notification”对象本身。例如
@Override public void onCreate() { final Notification.Builder builder = new Notification.Builder(this) .setSmallIcon(R.drawable.ic_peterleow) .setContentTitle("Foreground Service") .setContentText("Playing Background music..."); final Notification notification = builder.build(); startForeground(1, notification); }
上面的代码将创建一个在前台运行的服务,并带有一个正在进行的通知,如图 21 所示。
![]() |
图 21:前台服务的正在进行的通知 |
当一个前台运行的服务关闭时,其关联的正在进行的通知也将被移除。
要将服务从前台移除,只需调用“Service”的“stopForeground()”方法。这不会停止服务,但如果内存不足,允许系统将其杀死。要移除与该服务关联的正在进行的通知,请通过传递“false”参数来调用“stopForeground()”方法。
服务结束
在这段旅程中,您已经学习了 Android 服务的基础知识,并通过在应用中实现它们来付诸实践。您还学会了从应用中的服务发送通知。为了方便参考,我将它们放在一个链接中,以便快速跳转到本文中的相应主题。