Delphi 开发Android Service四种模式初探

前言:

         本篇文章正经来说,其实算是我的学习履历,是我在不断的摸索过程中,总结的经验,不能算是一篇正经的学术文章。现在DELPHI的学习资料太少了,就算是有也都是基于老版本DELPHI,或VCL相关的内容,涉及FMX、Android编程就显得明显不足,我虽然也玩了几年的DELPHI,但说实话能力水平有限,下面的文章如果有错误的地方,也请同行的前辈们指点修正,先在此谢过。

         那这篇文章主要说点什么呢,请大家先参考下面的目录,我尽可能把我知道的这点小经验分享给大家,这里面有很多的知识都是从网上学习整理而来,我也只是一个资料的整理者而已,考虑到DELPHI目前的学习资料匮乏问题,也为了照顾新人,在本文中先科普一些关键知识点,我会将内容尽可能讲到重点与详细说明,希望每个人都能看懂和理解。

提示:安卓系统的更新版本变化比较大,且国内各品牌手机系统都存在定制化问题,本文中所有涉及的实例程序,均是以DELPHI 11版本编写,如果其它DELPHI版本按照同方法出现问题,请大家自行修改,而测试的手机品牌主要以小米为主。


目录

前言:

一、Android Service简介

1.1 什么是Android Service?

1.2 Android Service的生命周期

1.3 Android Service的类型与区别

1.4 Android Service能做什么?

二、Local Service程序

2.1 Local Service特点

2.2 Local Service实例

三、Intent Local Service程序

3.1 Intent Local Service特点

四、Remote Service程序

4.1 Remote Service特点

4.2 Remote Service实例

五、Intent Remote Service程序

六、结束语


一、Android Service简介

1.1 什么是Android Service?

本文中所写的Android Service是指运行于安卓系统后台的服务,主要的特点如下:

  • 它运行于系统后台,没有界面,无法单独存在,必须要与前台程序绑定。
  • 可长期运行,即使是前台的程序关闭或销毁,它依然可以运行于后台。
  • 优先级高于Activity(内存不足时先杀掉Activity)。
  • 运行在主线程,且不能做耗时操作(超时标准请参考下面的说明) 

 Activity:Activity是Android中四大组件之一,而我们的Android应用是由一个或多个Activity组成的。活动(Activity)是一个可视化的用户界面,负责创建一个屏幕窗口,放置 UI 组件,供用户交互。

安卓超时标准:Android中长时间无响应会出现ANR

  • 后台Service:200秒(本文重点所讲内容)
  • 前台Service:20秒(Foreground Service)后面有机会单独开一篇重点讲述
  • Activity:5秒
  • 后台广播:60秒
  • 前台广播:10秒

ANR:(Application Not Responding)是指在安卓系统上,响应不够灵敏时,系统会向用户显示的一个对话框。

1.2 Android Service的生命周期

我们先用一张图来说明Android Service的生命周期,然后再来进行解释,图示如下:

        从上图中我们可以看到,安卓服务有两种启动方式:StartService和BindService,下面针对这两种方式的生命周期,我们用两个表格来做简要说明,详细如下:

表1:StartService生命周期
步骤说明
OnCreate()1、如果 service 没被创建过,调用 startService() 后会执行 onCreate() 回调;
2、如果 service 已处于运行中,调用 startService() 不会执行 onCreate() 方法。
onStartCommand()如果多次执行了 startService() 方法,那么 Service 的 onStartCommand() 方法也会相应的多次调用
OnBind()Service中的onBind()方法是抽象方法,Service类本身就是抽象类,所以onBind()方法是必须重写的,即使我们用不到
onDestory()在销毁的时候会执行Service该方法

表2:BindService生命周期
步骤说明
OnCreate()当服务通过 onStartCommand() 和 onBind() 被第一次创建的时候,系统调用该方法。该调用要求执行一次性安装
OnBind()当其他组件想要通过 bindService() 来绑定服务时,系统调用该方法。如果你实现该方法,你需要返回 IBinder 对象来提供一个接口,以便客户来与服务通信。你必须实现该方法,如果你不允许绑定,则直接返回 null
onUnbind()当客户中断所有服务发布的特殊接口时,系统调用该方法
onRebind()当新的客户端与服务连接,且此前它已经通过onUnbind(Intent)通知断开连接时,系统调用该方法
onDestroy()当服务不再有用或者被销毁时,系统调用该方法。你的服务需要实现该方法来清理任何资源,如线程,已注册的监听器,接收器等

提示:我们先只做理论上的说明,暂不涉及具体的实例与实现的方法,在后面的案例中,我们将用到前面所讲的理论知识点。

1.3 Android Service的类型与区别

在DELPHI中,我们创建Android Service时(创建方式如下图所示)软件提供了四种类型供我们选择,那么这四种Service程序到底有什么区别呢?

表3:Android Service四种类型说明
服务类型区别优点缺点
Local Service该服务依附在主进程上服务依附在主进程上而不是独立的进程,这样在一定程度上节约了资源,另外Local服务因为是在同一进程因此不需要IPC,也不需要AIDL。相应BindService会方便很多主进程被Kill后,服务便会终止
Intent Local Service同上与上面的区别在于可以利用Intent处理异步请求
Remote Service该服务是独立的进程服务为独立的进程,对应进程名格式为所在包名加上你指定的android:process字符串。由于是独立的进程,因此在Activity所在进程被Kill的时候,该服务依然在运行,不受其他进程影响,有利于为多个进程提供服务具有较高的灵活性该服务是独立的进程,会占用一定资源,并且使用AIDL进行IPC稍微麻烦一点
Intent Remote Service同上与上面的区别在于可以利用Intent处理异步请求
  • Intent:Intent用于Android程序中各组件(Activity、BroadcastReceive、Service)的交互,并且可以在组件之间传递数据。
  • AIDL:Android Interface Definition Language是为了实现进程间通信而设计的Android接口语言
  • IPC:Inter-Process Communication的缩写,含义为进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程

 上面都是实现进程之间通讯的方式,详细内容太多,这里不详细说明,在后面的案例中,我们将会用到这些知识点。 

1.4 Android Service能做什么?

  • 网络事务:聊天(等待他人回复短信)、地图定位(熄屏后实时定位的播报语音)等
  • 本地资源:播放音乐(读取音乐文件)、文件IO(后台上传文件、下载文件)等
  • 定时任务:订单超时(未支付情况下,一定时间后自动销毁订单)、闹钟提醒等

小结:Android Service的简要知识点先介绍到这里,更多的内容或更详细的内容大家可以网上搜索,这里受篇幅问题,不过多介绍,下面我将按类型进行实例的演示介绍


二、Local Service程序

2.1 Local Service特点

如上面《表3:Android Service四种类型说明》所述,Local Service应该是使用最多的一种Service类型,其主要的特点如下:

  • 运行在主进程上,并且与前台应用程序是同一个进程
  • 与前台程序是一对一绑定,无法被其它程序调用
  • 与前台程序的绑定不需要IPC或AIDL

2.2 Local Service实例

根据Local Service的特点,我们创建一个通知类的服务程序,由运行于后台的Service根据条件,给前台的程序进行通知的发送,本例设计主要实现以下几个功能:

  • 如何创建一个Local Service服务
  • 前台程序如何启动服务
  • 后台如何发送通知给前台
  • 如何获取安卓系统通知权限
  • 如何确保锁屏或待机后仍然收到通知

说明:因为是第一个例子,所以在本例中有些公共的功能我会在此例中实现(如:权限申请,电源管理等),后面其它类型的案例我只针对差异性的地方进行案例说明

下面我们开始正式的案例编程:

后台Service编程:

首先,我们按照上面1.3所写的内容,创建一个Local Service的服务,然后在模块中放上一个NotificationCenter1控件,完成后所得到的界面如下图所示:

我们将工程与模块进行保存,这里有2个非常重要的地方要记住:

  • 工程名字一定要记住,因为这涉及到前台程序调用的问题,我这里将工程名保存为:lSysService。
  • 整个Service工程与模块放在独立的文件夹,最好不要与前台程序混在一起。

然后我们在DM模块中创建一个通知过程,我将其命名为:SetNotification,具体如下:

procedure TFData.SetNotification(MyTitle,MyBody:string);
var
   MyNotification:TNotification;
begin
   MyNotification:=TNotification.Create;
   try
      MyNotification.Name:='MyNotification1';
      MyNotification.Title:=MyTitle;
      MyNotification.AlertBody:=MyBody;
      MyNotification.FireDate:=Now;
      NotificationCenter1.PresentNotification(MyNotification);
   finally
      MyNotification.Free;
   end;
end;

因为只是做个简单的示例,所以通知相关的具体内容都是直接使用的文本赋值,大家可以使用其它的方式进行值的设定,我这里发送通知的条件设定为每隔5秒,后台给前台发送一次通知。

我们选中DM模块,在OnStartCommand事件中,加上如下代码:

function TFData.AndroidServiceStartCommand(const Sender: TObject;
  const Intent: JIntent; Flags, StartId: Integer): Integer;
begin
     while True do
     begin
     Sleep(5000);
     SetNotification('系统通知','这是一个测试AndroidService的程序,此信息来自Service后台发送');
     end;

   Result:=TJService.JavaClass.START_STICKY;
end;

说明:TJService.JavaClass.START_STICKY这一句代码的功能,是保持服务运行,这里还有其它几个选项如下:

  • START_NOT_STICKY:默认值,服务被系统杀死时,不再启动
  • START_STICKY:服务被系统杀死时,将尝试重新创建服务 

到这里为止,我们这个简单的通知服务程序功能基本就完成了,我们可以进行Build,然后给前台程序进行绑定调用试试,在下图位置按鼠标右键,选择Build。

当提示成功后,我们的Service就完成了。下面我们开始做前台程序。

前台APP编程:

第一步:我们新建一个FMX工程,并在FORM上也放上一个NotificationCenter1,Memo1,Button1控件,完成后所得到的界面如下图所示:

第二步:绑定我们刚刚做好的Service程序,按如下图片所示操作即可

在上图红框这里点击,并选择Service程序存放的目录,这也是我为什么要在Service程序创建前提醒大家一定要放在独立目录的原因

完成后,我们可以查看一下是否导入成功,按下图位置看看文件是否绑定完成

还记得你制作的Service工程名叫什么吗,我这个案例的名称就是ISysService。能够看到这个文件,我们的绑定就成功了,接下来就可以进行代码编写,并完成相应的功能了

第三步:启动Service

我们将启动Service服务的功能放在Button1的点击事件中,并赋上启动Service的功能代码,如下所示:

我们先声明一个变量,名称为:MyService,如下所示,同时我们还需要引用System.Android.Service单元

然后在Button1的点击事件中,加上以下代码,用来启动Service

procedure TForm2.Button1Click(Sender: TObject);
begin
   try
      MyService:=TLocalServiceConnection.Create;
      MyService.StartService('lSysService');
      Memo1.Lines.Add('服务已启动');
   except
      on E:Exception do
      begin
        Memo1.Lines.Add(e.Message);
      end;
   end;
end;

这里注意MyService.StartService('lSysService')这一句,其中lSysService就是我们之前创建的Service的工程名称,大家不要搞错了,这也是为什么前面一再强调要记住名称的原因。

为了更好的查看通知内容,我们再加上一个通知的响应,当收到通知后,我们点击通知就把内容提取到APP的Memo1中显示,我们选中NotificationCenter1控件,并在OnReceiveLocalNotificationg事件中,加上如下代码:

procedure TForm2.NotificationCenter1ReceiveLocalNotification(Sender: TObject;
  ANotification: TNotification);
begin
   Memo1.Lines.Add(ANotification.AlertBody);
end;

然后我们保存工程与模块,编译运行看看

如果我们启动APP时,系统有上面图片的询问,我们当然要选择允许,要不然我们不可能收到通知,然后进入APP以后,我们点击“启动服务”

等待几秒钟后,我们就看到上面会出现一个通知,当我们点击通知时,内容就会显示在主界面的Memo1里面

提示:如果我们启动APP时,可能有部分手机不会弹出是否允许推送消息的提示,那么我们就需要在手机设置的应用管理中,允许APP接受通知的手动设置,详细操作这里不细说,这个应该不难

到这里为止,我们的功能算是完成了,但有一个问题,当我们关闭前台APP,或者手机锁屏、待机了以后,我就无法收到消息了,不能像微信一样可以时时接收信息呢?

这涉及到一个非常复杂的问题,各个软件商也为此头疼并付出了很多精力,就是为了让服务能够永驻,下面有一篇我在网上看到的文章,大家可以看看,服务永驻将会占用系统资源,我们首先需要考虑的问题是,是否需要真的长期永驻?

Android service 不被杀死“永不退出的服务”(双进程,服务,多进程,微信)_安卓 进程不退出-CSDN博客

当然,在我们本例中,还是可以做点事情,解决一些手机锁屏或软件退入后台而无法接收通知的问题,可以在电源管理方面进行设置,但需要我们手动设置

在手机系统设置中,完成了以上两项设置,那么前台APP转入后台,或手机锁屏仍然可不停的收到通知信息,除非将程序卸载。

至于如何做到像微信一样,服务永驻系统,即使前台关闭,后台服务也一直运行的实现方式,后面我单独再开一篇说明


三、Intent Local Service程序

3.1 Intent Local Service特点

如上面《表3:Android Service四种类型说明》所述,Intent Local Service大部分功能与Local Service是一致的,只是多了一个Intent而已,那这个Intent到底是什么东西呢,在Service程序中,它能起到什么作用呢,我们下面先简单的介绍一下。

3.1.1 什么是Intent?

在 Android 开发中,Intent 是一种非常重要的机制,它能够在应用程序之间传递数据并启动不同的组件,广泛应用于Android程序中各组件(Activity、BroadcastReceive、Service)的交互,并且可以在组件之间传递数据,分为显式Intent和隐式Intent

  • 显式Intent:明确指出了目标组件名称的Intent,我们称之为显式Intent,更多用于在应用程序内部传递消息。比如在某应用程序内,一个Activity启动一个Service
  • 隐式Intent:没有明确指出目标组件名称的Intent,则称之为隐式Intent,它不会用组件名称定义需要激活的目标组件,它更广泛地用于在不同应用程序之间传递消息,是根据action和category找出合适的目标。可通过Mainfest.xml配置各组件的<intent-filter>,只有当<action>和<category>同时匹配时,才能响应对应的Intent

上面的解释如果对安卓系统不太了解,可能会听起来一头雾水,不知道我在说啥,不过没有关系,下面我们针对一些重点的知识点,做详细的说明。

3.1.2 Intent的关键属性有哪些?

为了更清楚的理解,我们用表格的方式来进一步说明Intent。

表4:Intent的关键属性
属性说明

Component Name

要启动的组件名称,在创建Intent的时候是可选的,但是它是显式Intent的重要标志,有它就意味着只有Component name匹配上的那个组件才能接收你发送出来的显示intent。如果不写那么你创建的Intent就是隐式的,系统会根据这个intent的其他信息(比如:action、data、category)来确定哪些组件来接收这个intent,所以如果你想明确的启动哪个组件,就通过component name来指定

Action

意图,一个字符串变量,用来指定Intent要执行的动作类别(比如:view or pick)。你可以在你的应用程序中自定义action,但是大部分的时候你只使用在Intent中定义的action,标准Action有以下几项:

  • ACTION_VIEW: (android.intent.action.VIEW) 显示指定数据。
  • ACTION_EDIT: (android.intent.action.EDIT) 编辑指定数据。
  • ACTION_DIAL: (android.intent.action.DIAL) 显示拨号面板。
  • ACTION_CALL: (android.intent.action.CALL) 直接呼叫Data中所带的号码。
  • ACTION_ANSWER: (android.intent.action.ANSWER) 接听来电。
  • ACTION_SEND: (android.intent.action.SEND) 向其他人发送数据(例如:彩信/email)。
  • ACTION_SENDTO: (android.intent.action.SENDTO) 向其他人发送短信。
  • ACTION_SEARCH: (android.intent.action.SEARCH) 执行搜索。
  • ACTION_GET_CONTENT: (android.intent.action.GET_CONTENT) 让用户选择数据,并返回所选数据。

Data

一个Uri对象,对应着一个数据,这个数据可能是MIME类型的。当创建一个intent时,除了要指定数据的URI之外,指定数据的类型(MIME type)也很重要,比如,一个activity能够显示照片但是无法播放视频,虽然启动Activity时URI格式很相似。指定MIME type是很重要的,它能够帮助系统找到最合适的那个系统组件来处理你的intent请求。然而,MIME type有时能够通过URI来推测出来,特别是当data是content:的URI,这样的data表明在设备中由ContentProvider提供.

只设置数据的URI可以调用setData()方法,只设置MIME类型(MIME的类型定义,请参考本文最后的说明)可以调用setType()方法,如果要同时设置这两个可以调用setDataAndType()。

内置的常量属性如下:

  • tel://:号码数据格式,后跟电话号码。
  • mailto://:邮件数据格式,后跟邮件收件人地址。
  • smsto://:短息数据格式,后跟短信接收号码。
  • content://:内容数据格式,后跟需要读取的内容。
  • file://:文件数据格式,后跟文件路径。
  • market://search?q=pname:pkgname:市场数据格式,在Google Market里搜索包名为pkgname的应用。
  • geo://latitude, longitude:经纬数据格式,在地图上显示经纬度所指定的位置。

Category

一个包含Intent额外信息的字符串,表示哪种类型的组件来处理这个Intent。任何数量的Category 描述都可以添加到Intent中,但是很多intent不需要category,你可以通过调用addCagegory()方法来设置category,标准的常量如下:

  • CATEGORY_DEFAULT: (android.intent.category.DEFAULT) Android系统中默认的执行方式,按照普通 Activity的执行方式执行。
  • CATEGORY_HOME: (android.intent.category.HOME) 设置该组件为Home Activity。
  • CATEGORY_PREFERENCE: (android.intent.category.PREFERENCE) 设置该组件为Preference。
  • CATEGORY_LAUNCHER: (android.intent.category.LAUNCHER) 设置该组件为在当前应用程序启动器中优先级最高的Activity,通常与入口ACTION_MAIN配合使用。
  • CATEGORY_BROWSABLE: (android.intent.category.BROWSABLE) 设置该组件可以使用浏览器启动。

Extras

Intent可以携带的额外key-value数据,你可以通过调用putExtra()方法设置数据,每一个key对应一个value数据。你也可以通过创建Bundle对象来存储所有数据,然后通过调用putExtras()方法来设置数据。对于数据key的名字要尽量用包名做前缀,然后再加上其他,这样来保证key的唯一性,常用的常量属性如下:

  • EXTRA_BCC:存放邮件密送人地址的字符串数组。
  • EXTRA_CC:存放邮件抄送人地址的字符串数组。
  • EXTRA_EMAIL:存放邮件地址的字符串数组。
  • EXTRA_SUBJECT:存放邮件主题字符串。
  • EXTRA_TEXT:存放邮件内容。
  • EXTRA_KEY_EVENT:以KeyEvent对象方式存放触发Intent的按键。
  • EXTRA_PHONE_NUMBER:存放调用ACTION_CALL时的电话号码。

Flags

用来指示系统如何启动一个Activity(比如:这个Activity属于哪个Activity栈)和Activity启动后如何处理它(比如:是否把这个Activity归为最近的活动列表中)

MIME的说明,请参考以下文章:

android intent MIME type_android intent mimetype-CSDN博客

3.1.3 如何创建Intent并发送数据?

Android 中,我们可以使用 Intent 类来创建一个新的 Intent。其构造方法包含两个参数:Context 参数和目标组件的 Class 对象。 Context 参数通常指当前的 Activity 或 Application 对象,而目标组件则是要启动的 Activity、Service 或 BroadcastReceiver 等组件的类名,下面我们用一个小例子来说明Intent的创建:

我们新建一个工程,在Form上放一个Button,用来发送Intent,大概如下:

然后我们需要引用的单元与发送Intent的功能代码如下:

Uses

  {$IFDEF Android}
  Androidapi.JNI.GraphicsContentViewText, // JIntent
  Androidapi.Helpers, // StringToJString
  FMX.Platform.Android; // MainActivity
  {$ENDIF}
procedure TForm2.Button1Click(Sender: TObject);
var
  AText: string;
  Intent: JIntent;
begin
  AText := '这是来自人马座星系发来的贺电';
  Intent := TJIntent.Create;
  Intent.setType(StringToJString('text/plain')); //设置MIME类型为纯文本格式
  Intent.setAction(TJIntent.JavaClass.ACTION_SEND); //设置意图为发送数据

  //调用系统程序发送文本信息
  Intent.putExtra(TJIntent.JavaClass.EXTRA_TEXT, StringToJString(AText));

{使用Android API PackageManager类的queryIntentActivities方法,
确认是否存在可以处理该意图的应用程序。
如果有可以处理的应用程序,请发送意图。没有则提示“未找到接收者”}

  if MainActivity.getPackageManager.queryIntentActivities(Intent,
    TJPackageManager.JavaClass.MATCH_DEFAULT_ONLY).size > 0 then

    MainActivity.startActivity(Intent)  // 启动Intent

  else
    ShowMessage('未找到接收者');
end;

上面这段代码实现的功能是:发送一段文本信息给另一个应用程序,如果我们按照《表4:Intent的关键属性》的说明去理解,那么上面这一小段代码就不难理解了,大概结论如下:

  • 上面这一段代码是一个隐式Intent
  • 功能是:系统根据Intent的条件(类型:文本,意图:发送数据)去寻找符合条件的程序,当程序的数量大于1时,会提示软件清单,由用户选择哪一个来实现。比如能发送文本数据的程序有:邮箱,微信等等

3.1.4 如何接收Intent数据?

我们继续结合3.1.3上面的例子,做一个接收Intent的实例来说明,如果想要做到此功能,我们最少需要做两个步骤:

  • 具备符合接收此类Intent的条件
  • 接收Intent的功能代码

那如何让我们的程序具备接收Intent的条件,又如何让程序接收Intent呢,下面我们用一个小例子来具体说明:

我们新建一个工程,在上面放一个Memo,一个Button,大概界面如下,然后进行保存:

首件解决:具备符合接收此类Intent的条件

我们根据3.1.3上面代码针对Intent的设定,我们要在接收的程序设置符合发送方的条件,手段就是通过修改AndroidManifest.template来实现,我们在下面的代码中可以看到设置都是根据发送方的条件设置的:

        <intent-filter>

            <action android:name="android.intent.action.SEND" />

            <category android:name="android.intent.category.DEFAULT" />

            <data android:mimeType="text/plain" />

        </intent-filter>

        <activity
            android:name="com.embarcadero.firemonkey.FMXNativeActivity"
            android:label="%activityLabel%"
            android:configChanges="orientation|keyboard|keyboardHidden|screenSize"
            android:launchMode="singleTask">
            <!-- Tell NativeActivity the name of our .so -->
            <meta-data android:name="android.app.lib_name" android:value="%libNameValue%" />

            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>


        <!-- 以下是追加的部分,与发送的程序Intent设置相同 -->
        <intent-filter>

            <action android:name="android.intent.action.SEND" />

            <category android:name="android.intent.category.DEFAULT" />

            <data android:mimeType="text/plain" />

        </intent-filter>

        </activity>

如果不做上面这个动作,那么我们在发送端就无法调用到此接收程序,因为此接收程序不具备接收发送方的条件,发下图所示,当我在发送方点击Button时,系统会弹出一个符合接收条件的所有程序出来,红框标示的程序是我自己写得接收程序,如果不做上面的步骤,那就无法显示并无法调用:

接下来,我们继续写接收Intent的功能代码,按照程序的设计,我把接收Intent的功能代码赋给Button,详细如下:

先引用单元:

  fmx.Platform,

  {$IFDEF Android}
  fmx.Platform.Android,
  androidapi.Jni.GraphicsContentViewText,
  androidapi.Helpers,
  Androidapi.JNI.Os,
  {$ENDIF}

 然后编写接收Intent代码:

procedure TForm2.Button1Click(Sender: TObject);
var
   Intent:JIntent;
begin
   Intent:=SharedActivity.getIntent;
   if Intent.hasExtra(TJIntent.JavaClass.EXTRA_TEXT) then
   begin      
    Memo1.Lines.Add(JStringToString(Intent.getStringExtra(TJIntent.JavaClass.EXTRA_TEXT)));
   end else begin
       Memo1.Text:='距离太远,半路丢了';
   end;
end;

 编译运行,结束

我们来试试看,为了避免其它一些不必要的问题,我们先关闭发送端与接收端程序,然后重新打开发送程序,点击Button,并选择接收程序,图片同上就不重复传了,然后在接收程序中点击“接收Intent”按钮,我们就会发现,已经收到发送端的消息了

提示:之所以用这么长的篇幅来介绍Intent,是因为它不仅在Android Service中能用,其它地方也可以,所以就写得相对啰嗦了点。而Intent Local Service的实例演示就不再重复做了,大家可以根据前面的Local Service再结合Intent的知识点,自己尝试着做一个案例试试


四、Remote Service程序

4.1 Remote Service特点

如上面《表3:Android Service四种类型说明》所述,Remote Service与Local Service大部分功能是相同的,其区别主要有以下几点:

  • Remote Service是独立进程,Local Service则是与Activity属于同一进程
  • Remote Service可以被多个Activity调用,Local Service与Activity属于一对一绑定
  • Remote Service绑定需要使用AIDL进行IPC,Local Service不需要
  • Remote Service在Activity进程被杀死时还能独立运行,Local Service不可以

基于以上的特点,因此Remote Service一般常用于公共服务,即为系统常驻的Service(如:天气服务等)。

4.2 Remote Service实例

我们还是利用一个实例来说明Remote Service的应用吧,在实例创建的过程中,我们再做详细的说明与解析。

本实例我们实现的功能描述大概如下:

在后台的Service自定义两条消息,在前台建立两个程序,分别接收后台的两条消息,这个例子虽然简单,但主要体现的是,多个程序绑定同一个Service,这也是Remote Service的特性。

我们按照上面1.3所写的内容,创建一个Remote Service的服务程序,得到的初始界面如下,另外和Local Service一样,记得工程和单元要保存在独立的目录,还有记住工程名字:

第二步:把Service需要用到的单元先引用

第三步:因为可能要给两个程序调用,而且每个程序调用的消息不同,所以我们先自定义两组常量来区分消息

第四步:我们编写发送消息的代码,点击DM窗体,双击OnHandleMessage事件,并输入如下代码

function TDM.AndroidServiceHandleMessage(const Sender: TObject;
  const AMessage: JMessage): Boolean;
var
  MyMessage: JMessage;
  MyBundle: JBundle;
begin

  case AMessage.what of
  CusText1:  //自定义消息1
    begin
      MyBundle := TJBundle.Create;
      MyBundle.putString(StringToJString('Message1'), StringToJString('这是Remote Service自定义的第一个信息'));

      MyMessage := TJMessage.Create;
      MyMessage.what := ServiceText1;
      MyMessage.obj := MyBundle;
      AMessage.replyTo.send(MyMessage);

      Result := True;
    end;

  CusText2:  //自定义消息2
    begin
      MyBundle := TJBundle.Create;
      MyBundle.putString(StringToJString('Message2'), StringToJString('这是Remote Service自定义的第二个信息'));

      MyMessage := TJMessage.Create;
      MyMessage.what := ServiceText2;
      MyMessage.obj := MyBundle;
      AMessage.replyTo.send(MyMessage);

      Result := True;
    end
  else
    Result := False;
  end;
end;

第五步:Build程序,就完成了Service端的创建

接下来我们开始做前台的程序,先做第一个,我们命名为:Prog1

创建一个FMX工程,在上面放上Label,Memo,Button,界面大致如下:

第二步:引用单元

第三步:建立与Service对应的常量来接收消息

第四步:申明变量与函数

第五步:编写第一个程序的代码,为了大家看起来方便,我一次全部导入

unit Unit2;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.Memo.Types,
  FMX.ScrollBox, FMX.Memo, FMX.Controls.Presentation, FMX.StdCtrls,

  {$IFDEF Android}
  system.Android.Service,
  androidapi.Jni.Os,
  androidapi.Jni.GraphicsContentViewText,
  androidapi.Helpers,
  androidapi.Jni.JavaTypes,
  {$ENDIF}

  FMX.Layouts;

type
  TForm2 = class(TForm)
    Memo1: TMemo;
    Layout1: TLayout;
    Button1: TButton;
    Button2: TButton;
    Label1: TLabel;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }

    MyService:TRemoteServiceConnection;

    procedure OnHandleMessage(const AMessage: JMessage);
    procedure OnServiceConnected(const ServiceMessenger: JMessenger);
  public
    { Public declarations }
  end;

var
  Form2: TForm2;

const
  CusText1 = 1001;
  CusText2 = 1002;
  ServiceText1 = 2001;
  ServiceText2 = 2002;

implementation

{$R *.fmx}

procedure TForm2.Button1Click(Sender: TObject);
var
   MyMSG:JMessage;
begin
  //接收后台自定义的第一个消息,这里关键是CusText1,与后台对应的消息
  MyMSG := TJMessage.JavaClass.obtain(nil, CusText1);
  MyMSG.replyTo := MyService.LocalMessenger;
  MyService.ServiceMessenger.send(MyMSG);
end;

procedure TForm2.OnHandleMessage(const AMessage: JMessage);
var
  AText: JString;
  MyBundle: JBundle;
begin
  case AMessage.what of
  ServiceText1:
    begin
      MyBundle := TJBundle.Wrap(AMessage.obj);
      AText := MyBundle.getString(TAndroidHelper.StringToJString('Message1'));
      Memo1.Lines.Add(JStringToString(AText));
    end;

  ServiceText2:
    begin
      MyBundle := TJBundle.Wrap(AMessage.obj);
      AText := MyBundle.getString(TAndroidHelper.StringToJString('Message2'));
      Memo1.Lines.Add(JStringToString(AText));
    end;

  else
    MyService.Handler.Super.handleMessage(AMessage);
  end;
end;

procedure TForm2.OnServiceConnected(const ServiceMessenger: JMessenger);
begin
   Button1.Enabled:=True;
end;

procedure TForm2.Button2Click(Sender: TObject);
begin
   if MyService <> nil then
   begin
     MyService.UnbindService;
   end;
end;

procedure TForm2.FormCreate(Sender: TObject);
begin
   Button1.Enabled:=False;

   MyService:=TRemoteServiceConnection.Create;
   MyService.OnConnected:=OnServiceConnected;
   MyService.OnHandleMessage:=OnHandleMessage;
   MyService.BindService('com.embarcadero.Prog1','com.embarcadero.services.MyService');
   //BindService有两个字符串参数,都是包名,第一个是前台程序的包名,第二参数是后台Service的包名,前台程序包名的默认值都是com.embarcadero开头,后台Service的默认包名是com.embarcadero.services开头
end;

procedure TForm2.FormDestroy(Sender: TObject);
begin
   MyService.Free;
end;

end.

然后我们把Service目录导入到前台程序中,导入的步骤与Local Service一样,这里不重复,请大家参考Local Service前台编程的第二步操作即可,完成后我们运行程序看看

我们点击消息1按钮,就收到了来自后台Service自定义的第一条消息,那么此前台程序完成。

但我们做的是Remote Service,其特性是可以给多个程序绑定,那么我们再来建立一个新的前台程序,我们为其命名为:Prog2,大概界面跟上面这个差不多,完成后如下:

接下来,我们按照第一个前台程序一样的步骤完成第二前台程序的工作,这里需要注意的几个地方大家要看看,虽然功能是一样的,但第二个程序的工程名称不同,而且我们在第二程序里调用的是后台Service的第二条消息,所以下面几个地方不能跟第一台程序一样

第一个不同:我们调用第二条消息,红框这里的参数就需要更改

第二个不同:第二个程序的工程名不一样,所以绑定Service的时候也要变

第二个前台程序的所有代码如下:

unit Main;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.Memo.Types,
  FMX.StdCtrls,  FMX.Controls.Presentation, FMX.ScrollBox,fmx.Memo,

  {$IFDEF Android}
  system.Android.Service,
  androidapi.Jni.Os,
  androidapi.Jni.GraphicsContentViewText,
  androidapi.Helpers,
  androidapi.Jni.JavaTypes,
  {$ENDIF}

  FMX.Layouts;

type
  TForm2 = class(TForm)
    Memo1: TMemo;
    Layout1: TLayout;
    Button1: TButton;
    Button2: TButton;
    Label1: TLabel;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    { Private declarations }
    MyService:TRemoteServiceConnection;

    procedure OnHandleMessage(const AMessage: JMessage);
    procedure OnServiceConnected(const ServiceMessenger: JMessenger);

  public
    { Public declarations }
  end;

var
  Form2: TForm2;

const
  CusText1 = 1001;
  CusText2 = 1002;
  ServiceText1 = 2001;
  ServiceText2 = 2002;


implementation

{$R *.fmx}

procedure TForm2.Button1Click(Sender: TObject);
var
   MyMSG:JMessage;
begin
  MyMSG := TJMessage.JavaClass.obtain(nil, CusText2);
  MyMSG.replyTo := MyService.LocalMessenger;
  MyService.ServiceMessenger.send(MyMSG);

end;

procedure TForm2.Button2Click(Sender: TObject);
begin
   if MyService <> nil then
   begin
     MyService.UnbindService;
   end;
end;

procedure TForm2.FormCreate(Sender: TObject);
begin
   Button1.Enabled:=False;

   MyService:=TRemoteServiceConnection.Create;
   MyService.OnConnected:=OnServiceConnected;
   MyService.OnHandleMessage:=OnHandleMessage;
   MyService.BindService('com.embarcadero.Prog2','com.embarcadero.services.MyService');
end;

procedure TForm2.FormDestroy(Sender: TObject);
begin
   MyService.Free;
end;

procedure TForm2.OnHandleMessage(const AMessage: JMessage);
var
  AText: JString;
  MyBundle: JBundle;
begin
  case AMessage.what of
  ServiceText1:
    begin
      MyBundle := TJBundle.Wrap(AMessage.obj);
      AText := MyBundle.getString(TAndroidHelper.StringToJString('Message1'));
      Memo1.Lines.Add(JStringToString(AText));
    end;

  ServiceText2:
    begin
      MyBundle := TJBundle.Wrap(AMessage.obj);
      AText := MyBundle.getString(TAndroidHelper.StringToJString('Message2'));
      Memo1.Lines.Add(JStringToString(AText));
    end;

  else
    MyService.Handler.Super.handleMessage(AMessage);
  end;

end;

procedure TForm2.OnServiceConnected(const ServiceMessenger: JMessenger);
begin
   Button1.Enabled:=True;
end;

end.

其它操作与第一个前台程序是一致的,完成后我们运行第二个程序看看

到这里为止,我们前台的两个程序绑定到同一个Service全部成功了,其它的功能操作大概与Local Service是一样的,这里不再重复的写了。


五、Intent Remote Service程序

关于Intent Remote Service的介绍与实例,这里不再重复的啰嗦了,大家结合Remote Service的案例说明,再加上Intent的相关知识点,应该能自己写出案例来了


六、结束语

这篇文章写得真叫累,感觉有点啰嗦,但为了让新人朋友们能看懂,我宁愿啰嗦点并尽可能写得让大家都看得懂,如果本文存在一些问题,请大家在下面留言,或者大家希望写哪些方面的内容,也请在下面留言。

我们下次再见

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/650983.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

AI芯片软件定义硬件架构

软件定义硬件架构 AI 应用正促使芯片制造商和 OEM 重新审视重新配置硬件的可能性。 摩尔定律放缓&#xff0c;软件应用复杂性和规模激增&#xff0c;x86架构CPU运行通用软件的传统方法已无法满足嵌入式和AI应用的高效需求。 在当前x86架构主导的环境中&#xff0c;软硬件间差…

三丰云免费虚拟主机和免费云服务器评测

三丰云是一家专业的云服务提供商&#xff0c;为用户提供免费虚拟主机和免费云服务器服务。通过对三丰云的使用体验&#xff0c;我对他们的服务进行了评测。首先&#xff0c;三丰云的免费虚拟主机性能稳定&#xff0c;网站加载速度快&#xff0c;给用户提供了良好的访问体验。其…

R可视化:另类的箱线图

介绍 方格状态的箱线图 加载R包 knitr::opts_chunk$set(echo TRUE, message FALSE, warning FALSE) library(patternplot) library(png) library(ggplot2) library(gridExtra)rm(list ls()) options(stringsAsFactors F)导入数据 data <- read.csv(system.file(&qu…

数据集005:螺丝螺母目标检测数据集(含数据集下载链接)

数据集简介 背景干净的目标检测数据集。 里面仅仅包含螺丝和螺母两种类别的目标&#xff0c;背景为干净的培养皿。图片数量约420张&#xff0c;train.txt 文件描述每个图片中的目标&#xff0c;label_list 文件描述类别 另附一个验证集合&#xff0c;有10张图片&#xff0c;e…

力扣503. 下一个更大元素 II

Problem: 503. 下一个更大元素 II 文章目录 题目描述思路复杂度Code 题目描述 思路 由于此题是环形数组&#xff0c;我们在利用单调栈模板的基础上还需要将给定数组扩大一倍&#xff0c;但实际上我们只需要利用取余的操作模拟扩大数组即可&#xff08;具体操作看代码。在解决有…

800HZ电源-高频电源行业的明星

一、800Hz电源的简介&#xff1a; 800Hz电源&#xff0c;顾名思义&#xff0c;是一种专为满足通信系统中特定频率要求而设计的电源。通常&#xff0c;800Hz电源具有极高的稳定性和精确度&#xff0c;能提供稳定的电压输出&#xff0c;确保通信设备如交换机、基站、无线路由器等…

【设计模式】JAVA Design Patterns——Commander(指挥官模式)

&#x1f50d;目的 用于处理执行分布式事务时可能遇到的所有问题。 &#x1f50d;解释 处理分布式事务很棘手&#xff0c;但如果我们不仔细处理&#xff0c;可能会带来不想要的后果。假设我们有一个电子商务网站&#xff0c;它有一个支付微服务和一个运输微服务。如果当前运输…

香橙派Kunpeng Pro测评:他给的实在太多了

文章目录 一、开箱环节1、包装配置2、开发板包装3、开发板3.1、开发版正面3.2、开发板背面 二、硬件配置1、硬件配置清单 2、配置图解 三、开机~启动&#xff01;1、运行系统1.1、外设配置1.2、系统启动1.3、官方教程 2、openEuler系统概览 四、系统测试1、性能测试1.1、安装sy…

如何基于springboot构建cas最新版源码?

环境准备 下载JDK21 https://download.oracle.com/java/21/archive/jdk-21.0.2_windows-x64_bin.zip下载gradle 8.5并配置环境变量 https://gradle.org/next-steps/?version8.5&formatbin下载项目git clone http://gitlab.ruishan.cc/meta/anka-authentication.git 开始…

UI问题 --- CardView和其它的控件在同一布局中时,始终覆盖其它控件

原本代码&#xff1a; <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width"40dp"android:layout_height"wrap_content"andr…

for循环绑定id,更新html页面的文字内容

需求&#xff1a;将方法中内容对齐 实现方式 给for循环中每个方法添加一个动态的id在DOM结果渲染完后&#xff0c;更新页面数据&#xff0c;否则会报错&#xff0c;找不到对应节点或对应节点为空 <view v-for"(item, index) in itemList" :key"index"…

Java面试八股之Synchronized锁升级的原理

Synchronized锁升级的原理 Synchronized锁升级是Java为了提高并发性能而引入的一项优化措施&#xff0c;这一机制主要发生在JDK 1.6及之后的版本中。Synchronized锁升级旨在减少锁带来的性能开销&#xff0c;通过从低开销的锁逐步升级到高开销的锁&#xff0c;以适应不同的竞争…

Swagger测试接口,请求头添加token

概述Swagger 1、概述 在日常开发中&#xff0c;我们的业务需要用户登录&#xff0c;权限控制。但是在某些情况下我们使用Swagger测试接口&#xff0c;部分接口需要携带token&#xff0c;才能访问&#xff0c;就需要在swagger添加token窗口。 效果图&#xff1a; 由 右上角 A…

统计计算四|蒙特卡罗方法(Monte Carlo Method)

系列文章目录 统计计算一|非线性方程的求解 统计计算二|EM算法&#xff08;Expectation-Maximization Algorithm&#xff0c;期望最大化算法&#xff09; 统计计算三|Cases for EM 文章目录 系列文章目录一、基本概念&#xff08;一&#xff09;估算 π \pi π&#xff08;二&…

现代 c++ 三:移动语义与右值引用

移动语义很简单&#xff0c;但它相关联的术语很复杂。本文尝试从历史的角度解释清楚这些乱七八糟的术语及其关联&#xff1a; 表达式 (expression)、类型&#xff08;type&#xff09;、值类别 (value categories)&#xff1b; 左值 (lvalue)、右值 (rvalue)、广义左值 (glval…

【WEB前端2024】开源智体世界:乔布斯3D纪念馆-第30课-门的移动动画

【WEB前端2024】开源智体世界&#xff1a;乔布斯3D纪念馆-第30课-门的移动动画 使用dtns.network德塔世界&#xff08;开源的智体世界引擎&#xff09;&#xff0c;策划和设计《乔布斯超大型的开源3D纪念馆》的系列教程。dtns.network是一款主要由JavaScript编写的智体世界引擎…

UML-系统架构师(二)

1、UML&#xff08;Unified Modeling Language&#xff09;是面向对象设计的建设工具&#xff0c;独立于任何具体程序设计语言&#xff0c;以下&#xff08;&#xff09;不属于UML中的模型。 A用例图 B协作图 C活动图 DPAD图 解析&#xff1a; UML一共14种图 结构图&…

【传知代码】私人订制词云图-论文复现

文章目录 概述原理介绍核心逻辑1、选取需要解析的txt文档2、选取背景图明确形状3、配置停用词4、创建分词词典&#xff0c;主要解决新的网络热词、专有名词等不识别问题 技巧1、中文乱码问题&#xff0c;使用的时候指定使用的文字字体2、更换背景图3、词库下载以及格式转换方式…

vscode在Ubantu键位错乱问题

摘要&#xff1a;抄的vscode键位错乱_有没有在使用vscode时偶尔遇到退格键无法正常删除内容的情况?如果有的话,你是如何-CSDN博客 只是作为记录&#xff0c;查找方便

ThreadLocal一步梭哈

大家好&#xff0c;这里是教授.F 引入&#xff1a; 1. ThreadLocal 的作用&#xff0c;可以实现在同一个线程数据共享, 从而解决多线程数据安全问题. 2. ThreadLocal 可以给当前线程关联一个数据(普通变量、对象、数组)set 方法[源码!] 3. ThreadLocal 可以像 Map 一样存取数据…