返回博客首页
← 所有文章

在 NativeScript 中使用 Android 后台服务

2016 年 8 月 15 日 — 作者:Peter Kanev

重要:本文不会教你什么是 Android 后台服务,以及如何使用它们。如果你想了解服务,请前往Android API 指南

大家好,由于这是我的第一篇博客文章,所以需要做一个适当的介绍。我的名字是 Peter,我是一名软件开发人员,并且是 NativeScript 核心 Android 运行时团队的一员。今天我将向您展示 Android 后台服务在 NativeScript 应用程序中的行为,并提供编写您自己的实现的指南。

Android 中的服务

当您的应用程序需要执行繁重的非 UI 工作时,您必须注意不要阻塞 UI 线程并破坏其他平滑的用户体验。在 Android 上,使用后台服务是这项工作的正确技术。

繁重的非 UI 工作的常见示例包括

  • 从远程后端获取数据(邮件、聊天消息)*
  • 将数据写入本地 DB**
  • 下载/上传图像*
  • 触发计划通知
  • 获取当前 GPS 位置
  • 与设备上的其他应用程序通信

* - 核心模块包已经包含了用于异步 HTTP 客户端、图像的异步处理等的现成解决方案

** - NativeScript 开源社区创建了插件,使开发人员能够使用 SQLite 和其他 NOSQL 数据库

NativeScript 中的服务

几乎所有在使用 Android 开发时可以做的事情,您也可以在 NativeScript 中完成。由于 NativeScript Android 运行时的性质 - JavaScript 代码在主 UI 线程上运行,在使用 Android 服务时,存在某些已知限制

  • IntentServiceonHandleIntent 将在主 UI 线程上执行(与在纯 Android 中实现时在专用工作线程上执行相反)
  • IntentService 的实现需要使用一个不接受参数的构造函数,但这目前无法通过 Java 实现。
  • 基本 Service 类 及其所有后代默认情况下在 Android 的主 UI 线程上执行。实现应该在后台线程上运行的服务必须由开发人员在 Java 中进行管理(请参阅注意)。
  • BroadcastReceivers 不应在 AndroidManifest 中使用 android: process =":remote" 属性注册,因为接收器将无法在独立进程中执行 JavaScript

以下是如何使用 Android Alarm Manager 来安排调用 IntentService 的示例,该服务将创建通知,即使应用程序的 Activity 已被销毁(应用程序已关闭)。我们将利用 NativeScript 的一项优势 -直接使用 JavaScript 访问原生 (Java) API,以便编写尽可能接近你在网上查找 Android 指南时找到的代码。原始代码SO

  1. 创建一个默认的 NativeScript 应用程序
    tns create my-app
    cd my-app
     
  2. 扩展 android.app.IntentService 类并描述需要在 onHandleIntent 方法回调中完成的工作。在这种情况下,我们希望该类使用 Notification Builder 创建一个通知对象,并将其发送到通知服务。创建一个名为 NotificationIntentService.js 的新文件,并编写以下内容
    android.app.IntentService.extend("com.tns.notifications.NotificationIntentService" /* 为你的类指定一个有效的名称,因为它需要在 AndroidManifest 中声明 */, {
       onHandleIntent: function (intent) {
           var action = intent.getAction();
           if ("ACTION_START" == action) {
               postNotification();
           } else if (“ACTION_STOP” == action) {
        /* 获取系统 Alarm Manager 并取消所有挂起的警报,这将阻止服务定期执行  */
           }
        
           android.support.v4.content.WakefulBroadcastReceiver.completeWakefulIntent(intent);

     

       }
    });
    function postNotification() {
       // 做一些事情。例如,从后端获取最新数据以创建丰富的通知?
       var utils = require("utils/utils");
       var context = utils.ad.getApplicationContext(); // 获取 Android 中应用程序上下文的引用
       var builder = new android.app.Notification.Builder(context);
       builder.setContentTitle("Scheduled Notification")
           .setAutoCancel(true)
           .setColor(android.R.color.holo_purple) // 可选
           .setContentText("This notification has been triggered by Notification Service")
           .setVibrate([100, 200, 100]) // 可选
           .setSmallIcon(android.R.drawable.btn_star_big_on);
           // 当通知被按下时,将打开主 NativeScript Activity
       var mainIntent = new android.content.Intent(context, com.tns.NativeScriptActivity.class);
       var pendingIntent = android.app.PendingIntent.getActivity(context,
           1,
           mainIntent,
           android.app.PendingIntent.FLAG_UPDATE_CURRENT);
       builder.setContentIntent(pendingIntent);
       builder.setDeleteIntent(getDeleteIntent(context));
       var manager = context.getSystemService(android.content.Context.NOTIFICATION_SERVICE);
       manager.notify(1, builder.build());
    }
    /* 仅适用于从通知屏幕中删除通知 */
    function getDeleteIntent(context) {
           var intent = new android.content.Intent(context, com.tns.broadcastreceivers.NotificationEventReceiver.class);
           intent.setAction("ACTION_DELETE_NOTIFICATION");
           return android.app.PendingIntent.getBroadcast(context, 0, intent, android.app.PendingIntent.FLAG_UPDATE_CURRENT);
    }
     
  3. 创建一个 BroadcastReceiver,它监听从系统或 Alarm Manager 发出的事件,并启动服务。我们只实现具有 action(ACTION_START)的 intent,用于发布通知。如果你想停止服务,也可以应用相同的逻辑。使用 WakefulBroadcastReceiver 而不是 BroadcastReceiver 可以保证 CPU 不会休眠,直到服务中的请求处理完成(直到 IntentService 调用 completeWakefulIntent
    android.support.v4.content.WakefulBroadcastReceiver.extend("com.tns.broadcastreceivers.NotificationEventReceiver", {
       onReceive: function (context, intent) {
           var action = intent.getAction();
           var serviceIntent;
           if ("ACTION_START_NOTIFICATION_SERVICE" == action) {
               serviceIntent = createIntentStartNotificationService(context);
           } else if ("ACTION_DELETE_NOTIFICATION" == action) {
               serviceIntent = createIntentDeleteNotification(context);
           }
           if (serviceIntent) {
               android.support.v4.content.WakefulBroadcastReceiver.startWakefulService(context, serviceIntent);
           }
       }
    })
    var Intent = android.content.Intent;
    function createIntentStartNotificationService(context) {
       var intent = new Intent(context, com.tns.notifications.NotificationIntentService.class);
       intent.setAction("ACTION_START");
       return intent;
    }
    function createIntentDeleteNotification(context) {
       /* 与上面类似,只是动作不同 */
    }
     
  4. 现在,我们需要创建实际的闹钟,它将定期向我们的WakefulBroadcastReceiver发送意图。 创建一个名为 *service-helper.js* 的新文件。创建一个 *setupAlarm* 函数并导出它,以便可以在我们代码中的任何地方调用它。
    function getStartPendingIntent(context) {
       var alarmIntent = new android.content.Intent(context, com.tns.broadcastreceivers.NotificationEventReceiver.class);
       intent.setAction("ACTION_START_NOTIFICATION_SERVICE");
       return android.app.PendingIntent.getBroadcast(context, 0, intent, android.app.PendingIntent.FLAG_UPDATE_CURRENT);
    }
    function setupAlarm(context) {
       var alarmManager = context.getSystemService(android.content.Context.ALARM_SERVICE);
       var alarmIntent = getStartPendingIntent(context);
       alarmManager.setRepeating(android.app.AlarmManager.RTC_WAKEUP,
           java.lang.System.currentTimeMillis(),
           1000 * 60 * 60 * 24, /* 闹钟将每 24 小时发送一次 `alarmIntent` 对象 */
           alarmIntent);
    }
    函数 cancelAlarm(context) { … }
    module.exports.setupAlarm = setupAlarm;
     
  5. 下一步 - 让我们在按钮点击时调用 *setupAlarm*。 在应用程序的 *main-view-model.js* 脚本中,将 *onTap* 事件更改为以下内容
    function createViewModel() {
       var utils = require("utils/utils");
       var services = require(""./service-helper");
       /* … */
       viewModel.onTap = function () {
           services.setupAlarm(utils.ad.getApplicationContext());
       }
       /* 在用户交互时插入您取消闹钟管理器代码 */
       return viewModel;
    }
     
  6. 我们快完成了! 最后,我们需要在位于 *app/App_Resources/Android* 的 *AndroidManifest.xml* 中注册我们的 服务广播接收器
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="__PACKAGE__"
        android:versionCode="1"
        android:versionName="1.0">
        <supports-screens
            android:smallScreens="true"
            android:normalScreens="true"
            android:largeScreens="true"
            android:xlargeScreens="true"/>
        <uses-sdk
            android:minSdkVersion="17"
            android:targetSdkVersion="__APILEVEL__"/>
        <!-- 所有用户权限都在上面声明 -->
        <!-- 使用 WakefulBroadcastReceiver 需要授予 WAKE_LOCK 权限 -->
               <uses-permission android:name="android.permission.WAKE_LOCK" />
        <application ... >
            <activity
                ...
    </intent-filter>
            </activity>
            <activity android:name="com.tns.ErrorReportActivity"/>
            
    <service
               android:name="com.tns.notifications.NotificationIntentService"
               android:enabled="true"
               android:exported="false" />
            <receiver android:name="com.tns.broadcastreceivers.NotificationEventReceiver" />
        </application>
    </manifest>

现在您已经准备好测试您的通知调度程序了!

结论

我们使用 *IntentService* 在没有前台活动的情况下执行操作。 请记住,代码在 NativeScript Android 应用程序的主线程上执行。 一个更密集的操作,例如下载图像,可能会阻塞前台活动 - 从而冻结用户界面,或者因违反 Android 中的 严格线程策略 而引发异常。 考虑使用后台服务进行几乎立即产生结果的操作。

注意:在开发发送通知的应用程序时要非常小心。 一些用户可能会发现他们很烦人,如果他们经常收到它们,并且因此 - 删除您的应用程序。

注意:当您希望在不同的线程上卸载长时间运行的作业时,建议的方法是在 Java 类中编写实现,并在 NativeScript 中调用该作业。

下一步

在即将发布的版本中,我们将推出对 NativeScript 中 Web Workers 功能的支持,允许您在后台线程中执行代码,所有这些都通过 Angular/JavaScript 进行。

 

有用资源