十六、广播接收器和长期服务

除了活动、内容提供者和服务,广播接收器是 Android 流程中的另一个组件。广播接收器是可以响应客户端发送的广播消息的组件。该消息被建模为意图。此外,广播消息(intent)可以由一个以上的接收器来响应。

活动或服务等客户端组件使用上下文类中可用的 sendBroadcast(intent) 方法来发送广播。广播意图的接收组件将需要从 Android SDK 中可用的 BroadcastReceiver 类继承。这些广播接收器需要通过一个 receiver 组件标签在清单文件中注册,以表明接收器对响应某种类型的广播意图感兴趣。

发送广播

清单 16-1 显示了发送广播事件的示例代码。这段代码创建了一个具有唯一意图动作字符串的意图,在其上放置了一个名为消息的额外字段,并调用 sendBroadcast() 方法。将额外的放在目标上是可选的。

清单 16-1 。传播意图

//This code is in class: TestBCRActivity.java
//Project: TestBroadcastReceiver, Download: ProAndroid5_Ch16_TestReceivers.zip
private void testSendBroadcast(Activity activity) {
    //Create an intent with a unique action string
    String uniqueActionString = "com.androidbook.intents.testbc";
    Intent broadcastIntent = new Intent(uniqueActionString);

    //Allow stand alone cross-processes that have broadcast receivers
    //in them to be started even though they are in stopped state.
    broadcastIntent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);

    broadcastIntent.putExtra("message", "Hello world");
    activity.sendBroadcast(broadcastIntent);
}

清单 16-1 中,动作是一个满足您需求的任意标识符。为了使这个动作字符串唯一,您可能希望使用一个类似于 Java 包的名称空间。此外,我们将在本章后面的“进程外接收者”一节中讨论跨进程 FLAG _ INCLUDE _ STOPPED _ PACKAGES

编码一个简单的接收器

清单 16-2 显示了一个广播接收器,它可以响应来自清单 16-1 的广播意图。

清单 16-2 。示例广播接收器代码

//This class is in TestBroadcastReceiver project in the download
//The download for this chapter is: ProAndroid5_Ch16_TestReceivers.zip
public class TestReceiver extends BroadcastReceiver {
    private static final String tag = "TestReceiver";
    @Override
    public void onReceive(Context context, Intent intent)     {
        Log.d("TestReceiver", "intent=" + intent);
        String message = intent.getStringExtra("message");
        Log.d(tag, message);
    }
}

创建一个广播接收器非常简单。扩展 BroadcastReceiver 类并覆盖 onReceive() 方法。我们能够看到接收者的意图并从中提取信息。接下来,我们需要在清单文件中将广播接收器注册为接收器。

在清单文件中注册接收方

清单 16-3 展示了如何将一个接收者声明为意图的接收者,其动作是 com . androidbook . intents . testbc。

清单 16-3 。清单文件中的接收器定义

<!--
In filename: AndroidManifest.xml
Project: TestBroadcastReceiver, Download: ProAndroid5_Ch16_TestReceivers.zip
-->
<manifest>
<application>
...
<activity>...</activity>
...
<receiver android:name=".TestReceiver">
    <intent-filter>
        <action android:name="com.androidbook.intents.testbc"/>
    </intent-filter>
</receiver>
...
</application>
</manifest>

与活动等其他组件节点一样, receiver 元素是应用元素的子节点。

有了接收者(清单 16-2 )及其在清单文件(清单 16-3 )中的注册,您可以使用清单 16-1 中的客户端代码来调用接收者。我们已经在本章末尾提供了本章的可下载 ZIP 文件 pro Android 5 _ Ch16 _ test receivers . ZIP 的参考。这个 ZIP 文件有两个项目。到目前为止引用的代码在项目 TestBroadcastReceiver 中。

容纳多个接收器

广播的概念是可以有一个以上的接收器。让我们将 TestReceiver (参见清单 16-2 )复制为 TestReceiver2 ,看看两者是否能够响应相同的广播消息。 TestReceiver2 的代码在清单 16-4 中给出。

清单 16-4 。测试接收 2 的源代码

//Filename: TestReceiver2.java
//Project: TestBroadcastReceiver, Download: ProAndroid5_Ch16_TestReceivers.zip
public class TestReceiver2 extends BroadcastReceiver {
    private static final String tag = "TestReceiver2";
    @Override
    public void onReceive(Context context, Intent intent)     {
        Log.d(tag, "intent=" + intent);
        String message = intent.getStringExtra("message");
        Log.d(tag, message);
    }
}

将这个接收者添加到您的清单文件中,如清单 16-5 所示。

清单 16-5 。清单文件中的 TestReceiver2 定义

<!--
In filename: AndroidManifest.xml
Project: TestBroadcastReceiver, Download: ProAndroid5_Ch16_TestReceivers.zip
-->
<receiver android:name=".TestReceiver2">
    <intent-filter>
        <action android:name="com.androidbook.intents.testbc"/>
    </intent-filter>
</receiver>

现在,如果你像清单 16-1 中的那样启动事件,两个接收者都将被调用。

我们已经在第 13 章中指出,主线程运行所有属于单个进程的广播接收器。您可以通过打印出每个接收器中的线程签名来证明这一点,包括主线调用代码。您将看到相同的线程按顺序运行这段代码。 sendBroadcast() 对广播消息进行排队,并让主线程返回其队列。接收者对这个排队消息的响应是由同一个主线程按顺序执行的。当有多个接收者时,依赖于执行顺序来决定首先调用哪个接收者并不是一个好的设计。

使用进程外接收器

广播的意图更可能是响应它的进程是未知的,并且与客户端进程分离。你可以通过复制一个你目前展示的接收器并创建一个单独的来证明这一点。apk 文件。然后,当您从清单 16-1 中触发事件时,您将会看到两个进程内接收者(那些在同一个项目或中的接收者)。apk 文件)和进程外接收器(那些在单独的中的)。apk 文件)被调用。您还将通过 LogCat 消息看到进程内和进程外接收器在各自的主线程中运行。

然而,在 API 12 (Android 3.1)之后,外部进程中的广播接收器出现了一些问题。这是由于 SDK 出于安全考虑而采用的发布模式。您可以在本章提供的参考链接中了解更多信息。随着这一变化,应用在安装时将处于停止状态。可以启动组件的意图现在可以指定只针对那些处于启动状态的应用。默认情况下,旧的行为仍然存在。但是,对于广播意图,系统会自动添加一个标记来排除处于停止状态的应用。为了克服前一点,可以在广播意图上明确地设置意图标志,以将那些停止的应用作为有效目标包括在内。这就是你在清单 16-1 的代码中看到的。

我们在本章的可下载 ZIP 文件 proandroid 5 _ Ch16 _ test receivers . ZIP 中包含了一个额外的独立项目,名为 standalone broadcast receiver 来测试这个概念。要尝试它,您必须在模拟器上部署调用项目 TestBroadcastReceiver 和独立接收方的项目 StandloneBroadcastReceiver。然后,您可以使用 TestBroadcastReceiver 项目发送广播事件,并监视来自 standalone broadcastreceiver 的接收器响应的 LogCat。

使用来自接收者的通知

广播接收器通常需要向用户传达已发生的事情或状态。这通常是通过系统通知栏中的通知图标提醒用户来实现的。我们现在将向您展示如何从广播接收器创建通知、发送通知以及通过通知管理器查看通知。

通过通知管理器监控通知

Android 在通知区域显示通知图标作为提醒。通知区域位于装置顶部,呈一条带状,看起来像图 16-1 中的。通知区域的外观和位置可能会根据设备是平板电脑还是手机而变化,有时也会根据 Android 版本而变化。

9781430246800_Fig16-01.jpg

图 16-1 。安卓通知图标状态栏

图 16-1 中所示的通知区域称为状态栏 。它包含电池电量、信号强度等系统指标。当我们发送通知时,通知会以图标的形式出现在图 16-1 所示的区域。通知图标如图 16-2 所示。

9781430246800_Fig16-02.jpg

图 16-2 。状态栏显示通知图标

通知图标是向用户指示需要观察某些情况的指示器。要查看完整的通知,您必须用手指按住图标,像拉窗帘一样向下拖动图 16-2 中的标题栏。这将扩大通知区域,如图图 16-3 所示。

9781430246800_Fig16-03.jpg

图 16-3 。扩展通知视图

在图 16-3 的通知的放大视图中,您可以看到通知的详细信息。您可以单击通知详细信息来激发显示通知所属的完整应用的意图。您可以使用此视图来清除通知。此外,根据设备和版本,可能有打开通知的替代方式。现在让我们看看如何生成如图图 16-2图 16-3 所示的通知图标。

发送通知

当您创建一个通知对象时,它需要有以下元素:

  • 要显示的图标
  • 像“你好,世界”这样的文字
  • 交付的时间

一旦构造了通知对象,就可以通过询问名为 Context 的系统服务的上下文来获得通知管理器引用。通知 _ 服务。然后,您要求通知管理器发送通知。清单 16-6 有发送通知的广播接收器的源代码,如图图 16-2图 16-2 所示。

清单 16-6 。发送通知的接收者

//Filename: NotificationReceiver.java
//Project: StandaloneBroadcastReceiver, Download: ProAndroid5_Ch16_TestReceivers.zip
public class NotificationReceiver extends BroadcastReceiver {
    private static final String tag = "Notification Receiver";
    @Override
    public void onReceive(Context context, Intent intent) {
        Log.d(tag, "intent=" + intent);
        String message = intent.getStringExtra("message");
        Log.d(tag, message);
        this.sendNotification(context, message);
    }
    private void sendNotification(Context ctx, String message)   {
        //Get the notification manager
        String ns = Context.NOTIFICATION_SERVICE;
        NotificationManager nm =
            (NotificationManager)ctx.getSystemService(ns);
        //Prepare Notification Object Details
        int icon = R.drawable.robot;
        CharSequence tickerText = "Hello";
        long when = System.currentTimeMillis();
        //Get the intent to fire when the notification is selected
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setData(Uri.parse("[http://www.google.com](http://www.google.com)"));
        PendingIntent pi = PendingIntent.getActivity(ctx, 0, intent, 0);
        //Create the notification object through the builder
        Notification notification =
            new Notification.Builder(ctx)
                .setContentTitle("title")
                .setContentText(tickerText)
                .setSmallIcon(icon)
                .setWhen(when)
                .setContentIntent(pi)
                .setContentInfo("Addtional Information:Content Info")
                .build();
        //Send notification
        //The first argument is a unique id for this notification.
        //This id allows you to cancel the notification later
        //This id also allows you to update your notification
        //by creating a new notification and resending it against that id
        //This id is unique with in this application
        nm.notify(1, notification);
    }
}

展开通知时,会显示通知的内容视图。这就是你在图 16-2 中看到的。内容视图需要是一个 RemoteViews 对象。然而,我们不直接传递内容视图。根据传递给 Builder 对象的参数,Builder 对象创建一个适当的 RemoteViews 对象,并在通知上设置它。如果需要的话,构建器界面还有一个方法可以直接将内容视图设置为一个整体。

直接使用远程视图查看通知内容的步骤如下:

  1. 创建布局文件。
  2. 使用包名和布局文件 ID 创建一个 RemoteViews 对象。
  3. 在通知上调用 setContent() 。Builder 对象,然后调用 build()方法创建通知对象,然后将通知对象发送给通知管理器。

请记住,只有有限的一组控件可以参与远程视图,例如框架布局、线性布局、相对布局、模拟时钟、按钮、计时器、图像按钮、图像视图、进度条、文本视图。

清单 16-6 中的代码使用 Builder 对象创建一个通知来设置隐式内容视图(通过标题和文本)和触发意图(在我们的例子中,这个意图是浏览器意图)。可以创建一个新的通知,通过通知管理器重新发送,以便使用通知的唯一 ID 更新其先前的实例。通知的 ID 在清单 16-6 中设置为 1,在这个应用上下文中是惟一的。这种唯一性允许我们不断地更新通知发生了什么,并在需要时取消它。

您可能还想在创建通知时查看各种可用的标志,例如 FLAG_NO_CLEAR 和 FLAG _ understand _ EVENT,以控制这些通知的持久性。您可以使用以下 URL 来检查这些标志:

[http://developer.android.com/reference/android/app/Notification.html](http://developer.android.com/reference/android/app/Notification.html)

在广播接收器中启动活动

虽然我们建议你在需要通知用户时使用通知管理器,但 Android 确实允许你明确地产生一个活动。您可以通过使用常用的 startActivity() 方法来实现这一点,但是要将以下标志添加到用作 startActivity()参数的 intent 中:

  • 意图。FLAG_ACTIVITY_NEW_TASK
  • 意图。背景标志
  • 意图。FLAG_ACTIVITY_SINGLE_TOP

探索长期运行的接收器和服务

到目前为止,我们已经介绍了广播接收器的快乐之路,其中广播接收器的执行不太可能超过 10 秒。如果我们想执行超过 10 秒的任务,问题空间就有点复杂。

要了解原因,让我们回顾一下关于广播接收器的一些事实:

  • 与 Android 进程的其他组件一样,广播接收器运行在主线程上。因此,在广播接收器中拦截代码将会拦截主线程,并导致 ANR。广播接收器的时间限制是 10 秒,而活动的时间限制是 5 秒。这是一点缓刑,但不太多。
  • 承载广播接收器的进程将随着广播接收器的执行而开始和终止。因此,在广播接收方的 onReceive() 方法返回后,该过程将不再继续。当然,这是假设进程只包含广播接收器。如果流程包含其他已经在运行的组件,比如活动或服务,那么流程的生命周期也会考虑这些组件的生命周期。
  • 与服务进程不同,广播接收器进程不会重新启动。
  • 如果广播接收器要启动一个单独的线程并返回到主线程,Android 将假设工作已经完成,即使有线程在运行,也会关闭进程,使这些线程突然停止。
  • Android 在调用广播服务时自动获取部分唤醒锁,并在主线程中从服务返回时释放它。唤醒锁是 SDK 中可用的一种机制和 API 类,用于防止设备进入睡眠状态,或者在设备已经睡眠时将其唤醒。

给定这些谓词,我们如何执行长时间运行的代码来响应广播事件呢?

了解长期运行的广播接收器协议

答案在于解决以下问题:

  • 我们显然需要一个单独的线程,以便主线程可以返回并避免 ANR 消息。
  • 为了阻止 Android 杀死进程和工作线程,我们需要告诉 Android 这个进程包含一个组件,比如一个有生命周期的服务。因此,我们需要创建或启动该服务。服务本身不能直接完成超过 5 秒钟的工作,因为这发生在主线程上,所以服务需要启动一个工作线程并让主线程离开。
  • 在工作线程执行期间,我们需要保持部分唤醒锁,这样设备就不会进入睡眠状态。部分唤醒锁将允许设备在不打开屏幕等情况下运行代码,从而延长电池寿命。
  • 必须在接收器的主线代码中获得部分唤醒锁;否则就太晚了。例如,您不能在服务中这样做,因为在广播接收器发出的 startService() 和开始执行的服务的 onStartCommand() 之间可能太晚了。
  • 因为我们正在创建一个服务,所以服务本身可能会因为内存不足而关闭和重新启动。如果发生这种情况,我们需要再次获取唤醒锁。
  • 当服务的 onStartCommand() 方法启动的工作线程完成工作时,它需要告诉服务停止,这样它就可以被放在床上,而不是被 Android 起死回生。
  • 也有可能发生不止一个广播事件。考虑到这一点,我们需要小心我们需要产生多少工作线程。

鉴于这些事实,延长广播接收器寿命的推荐协议如下:

  1. 在广播接收器的 onReceive() 方法中获取(静态)部分唤醒锁。部分唤醒锁需要是静态的,以允许广播接收器和服务之间的通信。没有其他方法将唤醒锁的引用传递给服务,因为服务是通过不带参数的默认构造函数调用的。
  2. 启动一个本地服务,这样进程就不会被终止。
  3. 在服务中,启动一个工作线程来完成这项工作。不要在服务的 onStart() 方法中工作。如果你这样做了,你基本上是再次阻碍了主线。
  4. 当工作线程完成时,告诉服务直接或通过处理器停止自己。
  5. 让服务关闭静态唤醒锁。

了解 IntentService

认识到服务不阻塞主线程的需要,Android 提供了一个名为 IntentService 的工具本地服务实现,将工作卸载到工作线程,以便在将工作调度到子线程后释放主线程。在这种方案下,当你在一个 IntentService 上调用 startService() 时, IntentService 将使用一个循环和一个处理器将该请求排队到一个子线程,以便调用 IntentService 的一个派生方法在单个工作线程上完成实际工作。

下面是针对 IntentService 的 API 文档:

IntentService 是按需处理异步请求(表示为意图)的服务的基类。客户端通过 startService(Intent)调用发送请求;该服务根据需要启动,使用工作线程依次处理每个意图,并在工作耗尽时自行停止。这种“工作队列处理器”模式通常用于从应用的主线程中卸载任务。IntentService 类的存在是为了简化这种模式,并处理其中的机制。若要使用它,请扩展 IntentService 并实现 onhandleinent(Intent)。IntentService 将接收意图,启动一个工作线程,并在适当的时候停止服务。所有请求都在单个工作线程上处理—它们可能需要多长时间(并且不会阻塞应用的主循环),但是一次只会处理一个请求。

这个想法用清单 16-7 中的一个简单例子来演示。您扩展了 IntentService ,并在 onHandleIntent() 方法中提供了您想要做的事情。

清单 16-7 。使用 IntentService

//You can see file Test30SecBCRService.java for example
//Project: StandaloneBroadcastReceiver, Download: ProAndroid5_Ch16_TestReceivers.zip
public class MyService extends IntentService {
    public MyService()
    { super("some-java-package-like-name-used-for-debugging"); }
    protected void onHandleIntent(Intent intent)  {
        //log thread signature if you want to see that it is running on a separate thread
        //Ex: Utils.logThreadSignature("MyService");
        //do the work in this subthread
        //and return
    }
}

一旦有了这样的服务,就可以在清单文件中注册这个服务,并使用客户端代码调用这个服务作为 context . start service(new Intent(context,MyService.class)) 。这将导致调用清单 16-7 中的 onhandleinentent()。您会注意到,如果您在实际代码中使用清单 16-7 中注释掉的方法 utils . logthreadsignature(),它将打印工作线程而不是主线程的 ID。您可以在项目中看到 Utils 类,并下载在清单 16-7 的注释部分列出的引用。

为广播接收器扩展 IntentService

从广播接收器的角度来看,一个 IntentService 是一件美好的事情。它让我们在不阻塞主线程的情况下执行长时间运行的代码。不仅如此,作为一个服务, IntentService 提供了一个在广播代码返回时保持运行的进程。那么我们可以使用 IntentService 来满足长期运行操作的需求吗?是也不是。

是的,因为 IntentService 做两件事:首先,它保持流程运行,因为它是一个服务。第二,它让主线程离开并避免相关的 ANR 消息。

要理解“不”的答案,你需要多理解一点唤醒锁。当通过警报管理器调用广播接收器时,设备可能没有打开。因此,警报管理器通过调用电源管理器并请求唤醒锁来部分打开设备(仅足以在没有任何 UI 的情况下运行代码)。广播接收器一返回,唤醒锁就被释放。

这使得 IntentService 调用没有唤醒锁,因此设备可能会在实际代码运行之前进入睡眠状态。然而, IntentService ,作为服务的通用扩展,它不获取唤醒锁。所以我们需要在 IntentService 之上的进一步支持。我们需要一个抽象。

马克·墨菲创建了一个名为的 IntentService 的变体,它保留了使用 IntentService 的语义,但也获取了唤醒锁,并在各种条件下正确释放它。你可以在http://github.com/commonsguy/cwac-wakeful看看他的实现。

探索长期运行的广播服务抽象

wakeflintentservice 是一个很好的抽象。然而,我们想更进一步,使我们的抽象与扩展 IntentService 的方法并行,如清单 16-7 所示,做 IntentService 所做的一切,但也提供了一些好处:

  • 将传递给广播接收器的原始意图传递给被覆盖的方法 on handle identity。这允许我们在很大程度上隐藏广播接收器,模拟响应广播消息而启动服务的编程体验。这确实是这个抽象的目标,同时也有一些额外的东西。
  • 获取和释放唤醒锁(类似于唤醒服务)。
  • 处理正在重新启动的服务。
  • 允许在同一进程中以统一的方式处理多个接收器和多个服务的唤醒锁。

我们将这个抽象类称为 alongningnonstickybroadcastservice。顾名思义,我们希望这个服务允许长时间运行的工作。它也将专门为广播接收器而建造。这个服务也是非粘性的(我们将在本章后面解释这个概念,但简单来说,这表明如果队列中没有消息,Android 将不会启动该服务)。为了允许一个 IntentService 的行为,它将扩展 IntentService 并覆盖 on hand lenent 方法。

结合这些想法,抽象的 alongningnonstickybroadcastservice 服务将具有类似于清单 16-8 的签名。

清单 16-8 。长期运行的服务抽象概念

public abstract class ALongRunningNonStickyBroadcastService extends IntentService {
//...other implementation details
//the following method will be called by the onHandleIntent of IntentService
//this is where the actual work happens in this derived abstract class
protected abstract void handleBroadcastIntent(Intent broadcastIntent);
//...other implementation details

}

这个 alongningnonstickybroadcastservice 的实现细节有点复杂,在我们解释了为什么要追求这种类型的服务之后,我们将很快介绍它们。我们想首先展示它的实用性和简单性。

一旦我们有了清单 16-8 中的这个抽象类,清单 16-7 中的 MyService 示例就可以重写为清单 16-9 中的示例。

清单 16-9 。长期运行的服务示例用法

public class MyService extends ALongRunningNonStickyBroadcastService {
    //..other implementation details
    protected void handleBroadcastIntent(Intent broadcastIntent)  {
        //You can use the following method to see which thread runs this code
        //Utils.logThreadSignature("MyService");
        //do the work here
        //and return
    }
    //..other implementation details
}

清单 16-9 的简单之处在于,一旦客户端发出广播意图,这个代码就会被调用。尤其是事实上,你是直接接收,未经修改,同样的意图,调用广播接收器。好像广播接收器已经从解决方案中消失了。

正如你所看到的,你可以扩展这个新的长期运行的服务类(就像 IntentService 和 WakefulIntentService )并覆盖一个单独的方法,在广播接收器中做很少的事情甚至什么都不做。你的工作将在一个工作线程中完成(多亏了 IntentService ),不会阻塞主线程。

清单 16-9 是一个演示这个概念的简单例子。让我们来看一个更完整的实现,它实现了一个长时间运行的服务,可以运行 60 秒来响应一个广播事件(证明我们可以运行超过 10 秒并避免 ANR 消息)。我们将这个服务恰当地称为 Test60SecBCRService (BCR 代表广播接收器),其实现如清单 16-10 所示。

清单 16-10 。test60 secbcrservice 的源代码

//Filename: Test30SecBCRService.java
//Project: StandaloneBroadcastReceiver, Download: ProAndroid5_Ch16_TestReceivers.zip
public class Test60SecBCRService extends ALongRunningNonStickyBroadcastService {
   public static String tag = "Test60SecBCRService";
   //Required by IntentService to pass the classname for debug needs
   public Test60SecBCRService(){
      super("com.androidbook.service.Test60SecBCRService");
   }
   /* Perform long-running operations in this method.
    * This is executed in a separate thread.
    */
   @Override
   protected void handleBroadcastIntent(Intent broadcastIntent)  {
      //Utils class is in the download project mentioned
      Utils.logThreadSignature(tag);
      Log.d(tag,"Sleeping for 60 secs");
      //Use the thread to sleep for 60 seconds
      Utils.sleepForInSecs(60);
      String message =
         broadcastIntent.getStringExtra("message");
      Log.d(tag,"Job completed");
      Log.d(tag,message);
   }
}

正如您所看到的,这段代码成功地模拟了工作 60 秒,并且仍然避免了 ANR 消息。清单 16-10 中的实用方法是不言自明的,可以在本章的下载项目中找到。项目名和下载文件名在清单 16-10 中代码的注释部分。

设计长期运行的接收器

一旦我们有了清单 16-10 中的长期运行的服务,我们需要能够从广播接收器调用服务。同样,我们追求的是尽可能隐藏广播接收器的抽象。

长时间运行的广播接收器的第一个目标是将工作委托给长时间运行的服务。为此,长时间运行的接收者需要长时间运行的服务的类名来调用它。第二个目标是获得唤醒锁。第三个目标是将调用广播接收器的初衷传递给服务。我们将通过将原始意图作为一个可打包粘贴到意图附加中来实现这一点。我们将使用原意作为这个额外的名称。然后,长时间运行的服务提取 original_intent 并将其传递给长时间运行的服务的被覆盖的方法(稍后您将在长时间运行的服务的实现中看到这一点)。因此,这种设备给人的印象是,长时间运行的服务实际上是广播接收器的延伸。

让我们把这三样东西抽象出来,提供一个基类。这个长期运行的接收器抽象需要的唯一一点信息是通过一个名为 getLRSClass() 的抽象方法得到的长期运行的服务类的名称( LRSClass )。

将这些需求放在一起,实现抽象类的源代码在清单 16-11 中。

清单 16-11 。 单独运行接收方抽象

//Filename: ALongRunningReceiver.java
//Project: StandaloneBroadcastReceiver, Download: ProAndroid5_Ch16_TestReceivers.zip
public abstract class  ALongRunningReceiver extends BroadcastReceiver  {
    private static final String tag = "ALongRunningReceiver";
    @Override
    public void onReceive(Context context, Intent intent)  {
       Log.d(tag,"Receiver started");
       //LightedGreenRoom abstracts the Android WakeLock
       //to keep the device partially on.
       //In short this is equivalent to turning on
       //or acquiring the wake lock.
       LightedGreenRoom.setup(context);
       startService(context,intent);
       Log.d(tag,"Receiver finished");
    }
    private void startService(Context context, Intent intent)  {
       Intent serviceIntent = new Intent(context,getLRSClass());
       serviceIntent.putExtra("original_intent", intent);
       context.startService(serviceIntent);
    }
    /*
     * Override this method to return the
     * "class" object belonging to the
     * nonsticky service class.
     */
    public abstract Class getLRSClass();
}

在前面的广播接收器代码中,您可以看到对名为 LightedGreenRoom 的类的引用。这是一个静态唤醒锁的包装器。除了作为一个唤醒锁,这个类试图迎合多接收器,多服务等工作。,以便所有的 waki-ness 得到适当的协调。出于理解的目的,你可以把它当作一个静态的唤醒锁。这种抽象被称为 LightedGreenRoom,因为它旨在像各种“绿色”运动一样为设备节能。此外,它之所以被称为“点亮”,是因为它一开始就被“点亮”,因为广播接收器一启动它就打开它。最后一个使用它的服务将关闭它。

一旦接收器抽象可用,您将需要一个与清单 16-11 中的60 秒长时间运行的服务协同工作的接收器。在清单 16-12 中提供了这样一个接收器。

清单 16-12 。一个长期运行的广播接收器示例, Test60SecBCR

//Filename: Test60SecBCR.java
//Project: StandaloneBroadcastReceiver, Download: ProAndroid5_Ch16_TestReceivers.zip
public class Test60SecBCR extends ALongRunningReceiver {
   @Override
   public Class getLRSClass() {
      Utils.logThreadSignature("Test60SecBCR");
      return Test60SecBCRService.class;
   }
}

就像清单 16-10清单 16-11 中的服务抽象一样,清单 16-12 中的代码使用了广播接收器的抽象。接收者抽象启动由 getLRSClass() 方法返回的服务类所指示的服务。

到目前为止,我们已经展示了为什么我们需要两个重要的抽象来实现由广播接收器调用的长期运行的服务:

用明亮的温室提取唤醒锁

如前所述, LightedGreenRoom 抽象的主要目的是简化与唤醒锁的交互,唤醒锁用于在后台处理期间保持设备开启。您真的不需要 LightedGreenRoom 的实现细节,而只需要它的接口和针对它的调用。请记住,它只是 Android SDK 唤醒锁的一个薄薄的包装。在其最简单的实现中,它可以像打开(获取)和关闭(释放)唤醒锁一样简单。清单 16-13 展示了唤醒锁是如何被使用的,如 SDK 中所述。

清单 16-13 。使用唤醒锁 API 的伪代码

//Get access to the power manager service
PowerManager pm =
   (PowerManager)inCtx.getSystemService(Context.POWER_SERVICE);

//Get hold of a wake lock
PowerManager.WakeLock wl =
   pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, tag);

//Acquire the wake lock
wl.acquire();

//do some work
//while this work is being done the device will be on partially

//release the Wakelock
wl.release();

考虑到这种交互,广播接收器应该获得锁,当长时间运行的服务结束时,它需要释放锁。如前所述,没有好的方法将唤醒锁变量从广播接收器传递给服务。服务知道这个唤醒锁的唯一方式是使用静态或应用级变量。

获取和释放唤醒锁的另一个困难是引用计数。由于广播接收器被多次调用,如果调用重叠,将会有多次调用来获取唤醒锁。类似地,将有多个调用要释放。如果获取和释放调用的数量不匹配,我们最终会得到一个唤醒锁,在最坏的情况下,它会让设备保持比需要的时间长得多的时间。此外,当不再需要该服务并且垃圾收集运行时,如果唤醒锁计数不匹配,LogCat 中将出现运行时异常。这些问题促使我们尽最大努力将唤醒锁抽象为一个亮室以确保正确使用。每个进程都有一个这样的对象来保持一个唤醒锁,并确保它被正确地打开和关闭。包含的项目有这个类的实现。如果您发现代码由于要考虑的条件太多而太复杂,您可以从一个简单的静态变量开始,在服务启动和关闭时打开和关闭它,并对其进行优化以适应您的特定条件。

广播接收器和服务相互通信的合理方法是通过静态变量。我们没有将唤醒锁设为静态,而是将整个灯光温室设为静态实例。然而,灯光温室中的其他变量都是局部的,不稳定的。

为了方便起见, LightedGreenRoom 的每个公共方法也被公开为静态方法。我们使用了以“s_”开始命名这些方法的惯例。相反,您可以选择摆脱静态方法,直接调用 LightedGreenRoom 的单个对象实例。

实现长期运行的服务

为了呈现长期运行的服务抽象,我们必须再绕一圈来解释服务的生命周期以及它如何与 onStartCommand 的实现相关联。这是最终负责启动工作线程和服务语义的方法。

当一个服务通过 startService 启动时,该服务首先被创建,其 onStartCommand 方法被调用。Android 规定将这个过程保存在内存中,这样即使在服务多个传入的客户端请求时,服务也可以完成。但是,在内存要求苛刻的情况下,Android 可能会选择回收进程,并调用服务的 onDestroy() 方法。

注意当服务没有执行其 onCreate() 、 onStart() 或 onDestroy() 方法时,或者换句话说,当服务空闲时,Android 试图为服务调用 onDestroy() 方法来回收其资源。

然而,与被关闭的活动不同,如果队列中有未决的 startService 意图,则服务被调度为在资源可用时再次重启。服务将被唤醒,下一个意图将通过 onStartCommand() 传递给它。当然,服务带回时会调用 onCreate() 。

因为如果服务没有被显式停止,它们会自动重启,所以有理由认为,与活动和其他组件不同,服务组件从根本上来说是一个粘性组件。

了解非粘性服务

如果客户端明确调用 stopService ,服务不会自动重启。根据仍然连接的客户端数量,这个 stopService 方法可以将服务转移到停止状态,此时服务的 onDestroy 方法被调用,服务生命周期结束。一旦服务被它的最后一个客户端像这样停止,该服务将不会恢复。

当所有事情都按照设计发生时,这个协议工作得很好,其中 start 和 stop 方法被依次调用和执行,没有遗漏。在 Android 2.0 之前,即使没有工作要做,设备也会看到许多服务在周围徘徊并占用资源,这意味着即使队列中没有消息,Android 也会将服务带回内存。当停止服务因为异常或者因为在 onStartCommand 和停止服务之间的流程被取消而没有被调用时,就会发生这种情况。

Android 2.0 引入了一个解决方案,这样我们可以向系统表明,如果没有未决的意图,它不应该麻烦重启服务。这是通过返回非粘性标志(服务来完成的。来自的 START _ NOT _ STICKYonstart command。

然而,不粘并不是真的不粘。即使我们将这项服务标记为非粘性的,如果有悬而未决的意图,Android 将使这项服务起死回生。此设置仅适用于没有待定意向的情况。

理解粘性服务

那么一个服务真正的粘性是什么意思呢?粘旗(服务。START_STICKY 意味着 Android 应该重启服务,即使没有未决的意图。当服务重新启动时,调用 o nCreate 和 onStartCommand 的空意图。这将给服务一个机会,如果需要的话,调用 stopSelf 如果合适的话。这意味着粘性服务需要在重启时处理无效意图。

了解重新交付意图选项

尤其是本地服务遵循一种模式,即成对调用 onStart 和 stopSelf 。一个客户端调用 onStart 。当服务完成这项工作时,它调用 stopSelf 。如果一个服务需要 30 分钟来完成一个任务,那么它在 30 分钟内不会调用 stopSelf 。同时,由于低内存条件和更高优先级的作业,服务被回收。如果我们使用非粘性标志,服务将不会被唤醒,我们也不会调用 stopSelf 。

很多时候,这样是可以的。但是,如果你想确定这两个调用是否确实发生,你可以告诉 Android 在调用 stopSelf 之前不要 unqueue 这个 start 事件。这确保了当服务被回收时,除非调用了 stopSelf ,否则总会有一个挂起的事件。这被称为重新交付模式,可以通过返回服务来回复 onStartCommand 方法。START _ rede deliver _ INTENT 标志。

为长期运行的服务编码

现在您已经有了关于 IntentService 的背景、服务启动标志和亮着灯的绿色房间,我们准备看看清单 16-14 中的长期运行的服务。

清单 16-14 。长期运行的服务抽象

//Filename: ALongRunningNonStickyBroadcastService.java
//Project: StandaloneBroadcastReceiver, Download: ProAndroid5_Ch16_TestReceivers.zip
public abstract class ALongRunningNonStickyBroadcastService
extends IntentService  {
    public static String tag = "ALongRunningBroadcastService";
    //This is what you override to do your work
    protected abstract void
    handleBroadcastIntent(Intent broadcastIntent);

    public ALongRunningNonStickyBroadcastService(String name){
        super(name);
    }
    /*
     * This method can be invoked under two circumstances
     * 1\. When a broadcast receiver issues a "startService"
     * 2\. when android restarts this service due to pending "startService" intents.
     *
     * In case 1, the broadcast receiver has already
     * set up the "lightedgreenroom" and thereby gotten the wake lock
     *
     * In case 2, we need to do the same.
     */
    @Override
    public void onCreate()   {
        super.onCreate();

        //Set up the green room
        //The setup is capable of getting called multiple times.
        LightedGreenRoom.setup(this.getApplicationContext());

        //It is possible that more than one service of this type is running.
        //Knowing the number will allow us to clean up the wake locks in ondestroy.
        LightedGreenRoom.s_registerClient();
    }
    @Override
    public int onStartCommand(Intent intent, int flag, int startId)   {
        //Call the IntentService "onstart"
        super.onStart(intent, startId);

        //Tell the green room there is a visitor
        LightedGreenRoom.s_enter();

        //mark this as nonsticky
        //Means: Don't restart the service if there are no
        //pending intents.
        return Service.START_NOT_STICKY;
    }
    /*
     * Note that this method call runs in a secondary thread setup by the IntentService.
     *
     * Override this method from IntentService.
     * Retrieve the original broadcast intent.
     * Call the derived class to handle the broadcast intent.
     * finally tell the lighted room that you are leaving.
     * if this is the last visitor then the lock
     * will be released.
     */
    @Override
    final protected void onHandleIntent(Intent intent)    {
        try {
            Intent broadcastIntent
            = intent.getParcelableExtra("original_intent");
            handleBroadcastIntent(broadcastIntent);
        }
        finally {
            //release the wake lock if you are the last one
            LightedGreenRoom.s_leave();
        }
    }
    /* If Android reclaims this process, this method will release the lock
     * irrespective of how many visitors there are.
     */
    @Override
    public void onDestroy() {
        super.onDestroy();
        //Do any cleanup, if needed, when a service no longer needs a wake lock
        LightedGreenRoom.s_unRegisterClient();
    }
}

这个类扩展了 IntentService ,并获得了由 IntentService 设置的工作线程的所有好处。此外,它进一步特殊化了 IntentService ,使其成为一个非粘性服务。从开发人员的角度来看,主要关注的方法是抽象的 handleBroadcastIntent()方法。清单 16-15 展示了如何在清单文件中设置接收者和相应的服务。

清单 16-15 。长期运行的接收者和服务定义

<!--
In filename: AndroidManifest.xml
Project: StandaloneBroadcastReceiver, Download: ProAndroid5_Ch16_TestReceivers.zip
-->
<manifest...>
......
<application....>
<receiver android:name=".Test60SecBCR">
    <intent-filter>
       <action android:name="com.androidbook.intents.testbc"/>
    </intent-filter>
</receiver>
<service android:name=".Test60SecBCRService"/>
</application>
.....
<uses-permission android:name="android.permission.WAKE_LOCK"/>
</manifest>

请注意,您将需要 wake lock 权限来运行这个长期运行的接收器抽象。本章的可下载项目中提供了所有接收器和长期运行服务的完整源代码。清单 16-15 展示了广播接收器调用的长期运行服务的本质。这个抽象声明你写几行代码来创建一个类似于 Test60SecBCR ( 清单 16-12 )的接收器,然后写一个类似于代码 Test60SecBCRService ( 清单 16-10 )中的 java 方法。给定接收者和想要长时间运行的 java 方法,您可以执行该方法来响应广播事件。这种抽象确保了该方法可以运行尽可能长的时间,而不会产生 ARM。该抽象负责 a)保持流程活动,b)调用服务,c)负责唤醒锁,以及 d)将广播意图传递给服务。最后,这种抽象模拟了从广播事件中“调用一个可以无限制执行的方法”。

广播接收器中的附加主题

由于篇幅所限,我们无法在本书中涵盖广播接收器的所有方面。我们还没有涉及的一个话题是限制发送和接收广播的安全机会。您可以在接收方上使用 export 属性来决定是否可以从外部进程调用它。您还可以通过清单文件或以编程方式启用或禁用接收器。我们还没有介绍一个叫做 sendOrderBroadcast 的方法,它可以帮助我们按照包括链接在内的顺序调用广播接收器。你可以从 BroadcastReceiver 类的主要 API 文档中了解这些方面。

此外,在 Android 支持库 SDK 的版本 4 中,有一个名为 LocalBroadcastManager 的类,用于优化对严格本地的广播接收器的调用。由于是本地的,所以不需要考虑所有的安全限制。根据 SDK,当使用这个类时,还有系统级的优化。

同样在 Android 支持库 SDK 的版本 4 中,有一个名为 WakefulBroadcastReceiver 的类,它封装了一些我们为长期运行的服务需求所涵盖的相同概念。

参考

以下是本章所涵盖主题的有用参考:

摘要

在这一章中,我们已经讨论了广播接收器、通知管理器以及服务抽象在最大限度地利用广播接收器中的作用。我们也给出了一个实用的抽象来模拟广播接收器长期运行的广播服务。