资讯详情

资讯详情

Delphi Android Service 实战:后台常驻与前台服务避坑指南

简介这份资源是面向Delphi开发者的Android服务Service实战示例基于Delphi XE5与XE7两个版本分别提供完整工程帮助开发者理解Android四大组件之一的服务如何在后台执行长时间任务、与Activity通信以及管理生命周期。压缩包共32个文件约69KB以pas单元、java源码、dproj与dpr工程文件、deployproj部署配置、fmx界面文件、xml清单及bat构建脚本为主另含res资源与jar包覆盖服务类、主Activity、AndroidManifest声明及示例资源等模块。已有286人学习下载。通过对照XE5与XE7两套代码读者可掌握TAndroidService子类的OnCreate、OnStartCommand、OnBind等方法实现学会在清单中注册服务、用startService或bindService启动绑定并借助FireMonkey框架处理跨平台兼容问题为开发后台常驻型Delphi Android应用提供可直接参考的工程范例与排错思路。1. Delphi Android Service Demo把后台常驻这件事讲透很多做 Delphi 的朋友在 Windows 上写服务写得挺顺一到 Android 就懵了明明代码没报错App 一退到后台定时任务就停了Socket 也断了日志也不打了。这不是代码写错了是你没搞懂 Android 的后台限制。Android 从 8.0 开始对后台进程管得非常严普通后台线程在 App 进入后台后会被系统挂起甚至杀掉想让逻辑继续跑必须用 Service。Delphi 从 10.3 开始对 Android Service 的支持逐渐完善但官方文档写得比较散很多人第一次做 Delphi Android Service Demo 都会踩一堆坑。这篇笔记就把这套东西从头到尾拆一遍Service 怎么建、怎么和主界面通信、前台服务怎么配、权限怎么加、被杀怎么救。适合已经会用 Delphi 写 Android App、但后台逻辑总是翻车的开发者。2. Delphi 里 Android Service 到底怎么建从零到跑通2.1 先搞清楚 Android 的 Service 和 Delphi 的对应关系Android 原生有三种 ServiceStarted Service、Bound Service、Foreground Service。Delphi 把前两种封装成了TAndroidService前台服务则需要你在TAndroidService基础上自己改 Manifest 和通知栏。很多人以为 Delphi 里建个 Service 就是拖个控件其实不是。Delphi 的 Android Service 是一个独立的工程单元编译出来是一个单独的.so或者.jar里的 Service 类跟主 App 的 Activity 不在同一个进程里。这意味着你不能直接在 Service 里访问主界面的变量也不能直接弹 Toast 给用户看。通信必须走 Intent、BroadcastReceiver 或者 AIDL。我一般会先用 Started Service 做定时任务再用 LocalBroadcast 把结果传回主界面。这样结构最清晰调试也方便。2.2 新建一个 Delphi Android Service 工程的完整步骤在 Delphi IDE 里File → New → Other → Delphi Projects → Android Service。注意这个模板建出来的不是一个完整的 App而是一个 Service 模块。你需要把它编译成一个.jar或者.so然后放到主 App 的libs目录里。具体操作# 假设你的 Delphi 安装目录在 C:\Program Files (x86)\Embarcadero\Studio\22.0 # 新建 Service 工程后先编译成 Android Release # 在 IDE 里选择 Release 配置Target 选 Android 64-bit # 编译产物在工程目录的 Android\Release\ 下 # 你会看到类似 libProject1Service.so 的文件编译完之后把这个.so文件复制到主 App 工程的Android\libs\arm64-v8a\目录下。然后在主 App 的AndroidManifest.template.xml里注册 Serviceservice android:namecom.embarcadero.services.Project1Service android:exportedfalse android:process:remote /这里android:process:remote表示 Service 跑在独立进程里避免主 App 被杀时 Service 跟着挂。android:exportedfalse是安全要求除非你要让其他 App 调用否则别开。很多新手忘了加android:process结果主 App 一退Service 也没了还以为是代码问题。2.3 在 Service 里写定时逻辑TThread 和 Handler 怎么选Delphi 的 Android Service 里不能用TTimer因为TTimer依赖消息循环而 Service 没有 UI 消息循环。常见做法是用TThread加Sleep或者用 Android 原生的Handler。我一般用TThread因为 Delphi 的跨平台封装更顺手。下面是一个每 10 秒打一次日志的 Service 实现unit Project1Service; interface uses System.SysUtils, System.Classes, Androidapi.JNI.App, Androidapi.JNI.Os, Androidapi.JNI.JavaTypes, FMX.Helpers.Android; type TProject1Service class(TAndroidService) procedure AndroidServiceStartCommand(const Sender: TObject; const Intent: JIntent; Flags, StartId: Integer; var Stopped: Boolean); procedure AndroidServiceCreate(const Sender: TObject; const AContext: JContext); private FWorker: TThread; procedure StartWorker; end; var Project1Service: TProject1Service; implementation procedure TProject1Service.AndroidServiceCreate(const Sender: TObject; const AContext: JContext); begin // Service 创建时调用适合做初始化 StartWorker; end; procedure TProject1Service.StartWorker; begin FWorker : TThread.CreateAnonymousThread( procedure var I: Integer; begin I : 0; while not TThread.CurrentThread.CheckTerminated do begin Inc(I); // 这里写你的业务逻辑比如上报数据、检查更新 // 注意不能直接操作 UI只能通过广播或 Intent 通知主界面 Sleep(10000); // 10 秒 end; end); FWorker.FreeOnTerminate : True; FWorker.Start; end; procedure TProject1Service.AndroidServiceStartCommand(const Sender: TObject; const Intent: JIntent; Flags, StartId: Integer; var Stopped: Boolean); begin Stopped : False; // 表示 Service 不被停止继续运行 end; end.这段代码的关键点AndroidServiceCreate是 Service 第一次创建时触发适合启动工作线程。AndroidServiceStartCommand每次通过startService调用时触发返回Stopped : False表示不主动停止。TThread.CreateAnonymousThread里用CheckTerminated判断线程是否该退出避免死循环。Sleep(10000)是毫秒10 秒。注意这个线程在 Service 被杀时会一起死所以如果你需要更顽强的存活得用前台服务。2.4 主 App 怎么启动和停止 Service在主 App 的按钮事件里用JIntent启动 Serviceuses Androidapi.JNI.GraphicsContentViewText, Androidapi.JNI.JavaTypes, Androidapi.Helpers; procedure TForm1.ButtonStartClick(Sender: TObject); var Intent: JIntent; begin Intent : TJIntent.JavaClass.init( TAndroidHelper.Context, TJProject1Service.JavaClass); TAndroidHelper.Context.startService(Intent); end; procedure TForm1.ButtonStopClick(Sender: TObject); var Intent: JIntent; begin Intent : TJIntent.JavaClass.init( TAndroidHelper.Context, TJProject1Service.JavaClass); TAndroidHelper.Context.stopService(Intent); end;这里TJProject1Service是 Delphi 自动生成的 Java 类桥接你不需要手动写 Java。startService和stopService是 Android 原生方法Delphi 通过TAndroidHelper.Context拿到当前 Context。注意从 Android 8.0 开始如果 App 在后台startService会抛IllegalStateException必须改用startForegroundService。这个坑后面会细说。3. 前台服务与通知栏让 Service 不被系统杀掉3.1 为什么必须用前台服务Android 8.0 之后后台 Service 最多活几分钟系统就会把它干掉。想让 Service 长期存活唯一合法途径是前台服务在通知栏显示一个常驻通知告诉用户“这个 App 正在后台运行”。这不是可选项是强制要求。Delphi 里创建前台服务需要两步一是在 Service 的AndroidServiceCreate里调用startForeground二是在 Manifest 里加FOREGROUND_SERVICE权限。很多人只加了权限没调startForeground结果 Service 还是被杀。3.2 在 Delphi 里实现前台服务的代码uses Androidapi.JNI.App, Androidapi.JNI.Os, Androidapi.JNI.GraphicsContentViewText, Androidapi.JNI.JavaTypes, Androidapi.Helpers, FMX.Helpers.Android; procedure TProject1Service.AndroidServiceCreate(const Sender: TObject; const AContext: JContext); var Notification: JNotification; Channel: JNotificationChannel; Builder: JNotification_Builder; ChannelId: JString; begin // 创建通知渠道Android 8.0 必须 ChannelId : StringToJString(my_service_channel); Channel : TJNotificationChannel.JavaClass.init( ChannelId, StringToJString(后台服务), TJNotificationManager.JavaClass.IMPORTANCE_LOW); AContext.getSystemService(TJContext.JavaClass.NOTIFICATION_SERVICE) .JavaClass.getSystemService(TJContext.JavaClass.NOTIFICATION_SERVICE); // 注意上面这行是示意实际要用 JNotificationManager 转换 // 更稳妥的写法 TJNotificationManager.JavaClass.from(AContext).createNotificationChannel(Channel); // 构建通知 Builder : TJNotification_Builder.JavaClass.init(AContext, ChannelId); Builder.setContentTitle(StringToJString(服务运行中)); Builder.setContentText(StringToJString(正在后台执行任务)); Builder.setSmallIcon(AContext.getApplicationInfo.icon); Notification : Builder.build; // 启动前台服务 startForeground(1, Notification); end;这段代码里startForeground(1, Notification)的1是通知 ID必须唯一。TJNotificationManager.JavaClass.from(AContext)是 Delphi 里获取系统服务的标准写法。IMPORTANCE_LOW表示通知不响铃不震动适合后台服务。注意setSmallIcon必须用 App 自己的图标资源否则通知不显示。很多人在这一步翻车就是因为图标 ID 传错了。3.3 Manifest 里必须加的三项配置uses-permission android:nameandroid.permission.FOREGROUND_SERVICE / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE_DATA_SYNC / service android:namecom.embarcadero.services.Project1Service android:foregroundServiceTypedataSync android:exportedfalse android:process:remote /FOREGROUND_SERVICE是基础权限Android 14 开始还要加FOREGROUND_SERVICE_DATA_SYNC这种具体类型权限。foregroundServiceType要跟你的业务匹配定时同步数据就用dataSync定位就用location。类型写错会导致startForeground抛异常。我一般会在AndroidServiceStartCommand里再检查一次权限没权限就提示用户去设置里开。4. Service 与主界面通信广播、Intent 和文件三种方式4.1 用 LocalBroadcast 做实时通信Service 和主界面不在同一个进程不能直接调方法。最轻量的方式是LocalBroadcastManager。Service 发广播主界面注册接收器。Delphi 里用TAndroidHelper和JLocalBroadcastManager实现// Service 端发送广播 uses Androidapi.JNI.Support, Androidapi.JNI.GraphicsContentViewText; procedure TProject1Service.SendUpdate(const Msg: string); var Intent: JIntent; begin Intent : TJIntent.JavaClass.init(StringToJString(SERVICE_UPDATE)); Intent.putExtra(StringToJString(msg), StringToJString(Msg)); TJLocalBroadcastManager.JavaClass.getInstance( TAndroidHelper.Context).sendBroadcast(Intent); end; // 主界面端注册接收器 procedure TForm1.FormCreate(Sender: TObject); begin FReceiver : TJavaLocalBroadcastReceiver.Create( procedure(const AContext: JContext; const AIntent: JIntent) begin // 收到广播更新 UI Label1.Text : JStringToString( AIntent.getStringExtra(StringToJString(msg))); end); TJLocalBroadcastManager.JavaClass.getInstance( TAndroidHelper.Context).registerReceiver( FReceiver, TJIntentFilter.JavaClass.init(StringToJString(SERVICE_UPDATE))); end;TJavaLocalBroadcastReceiver是 Delphi 提供的 Java 桥接类用来接收广播。registerReceiver的第二个参数是IntentFilter指定只接收SERVICE_UPDATE这个 action。注意主界面销毁时要unregisterReceiver否则内存泄漏。这个方式延迟低适合频繁更新。4.2 用文件或 SQLite 做异步数据交换如果通信频率不高比如每分钟同步一次状态用文件更简单。Service 写文件主界面定时读。或者用 SQLite两边都操作同一个数据库。Delphi 的TFDConnection在 Android 上可以直接用 SQLite。注意并发问题Service 和主界面同时写会锁库。我一般让 Service 只写主界面只读用WAL模式提高并发。// Service 端写状态文件 procedure TProject1Service.WriteStatus(const Status: string); var Path: string; Lines: TStringList; begin Path : TPath.Combine(TPath.GetDocumentsPath, service_status.txt); Lines : TStringList.Create; try Lines.Add(Status); Lines.SaveToFile(Path); finally Lines.Free; end; end;TPath.GetDocumentsPath在 Android 上对应 App 的私有目录不需要额外权限。主界面读同一个路径即可。这个方式最简单但实时性差适合状态上报。4.3 三种通信方式怎么选方式延迟复杂度适用场景LocalBroadcast低中实时 UI 更新、频繁消息文件/SQLite高低状态同步、日志上报AIDL最低高跨进程方法调用、复杂交互新手建议先用文件跑通后再换 LocalBroadcast。AIDL 在 Delphi 里支持有限除非必要别碰。5. 避坑与排查Delphi Android Service 最常见的五个翻车点5.1 现象App 退到后台Service 几分钟后就不干活了原因Android 8.0 后台 Service 被系统限制没有前台通知就会被杀。解决改成前台服务调startForeground加FOREGROUND_SERVICE权限。如果还不行检查android:process:remote是否加了独立进程能多活一会儿。5.2 现象startService抛IllegalStateException: Not allowed to start service原因App 在后台时不能直接startService必须用startForegroundService。解决在主 App 里判断版本Android 8.0 以上用startForegroundService并且 Service 必须在 5 秒内调startForeground否则系统会杀 Service 并报 ANR。if TJBuild_VERSION.JavaClass.SDK_INT 26 then TAndroidHelper.Context.startForegroundService(Intent) else TAndroidHelper.Context.startService(Intent);5.3 现象Service 里写的日志看不到原因Service 跑在独立进程Log.d输出到 logcat 但过滤条件不对。解决用adb logcat | grep Project1Service过滤或者在 Service 里把日志写到文件。Delphi 的OutputDebugString在 Android 上不生效别用。5.4 现象通知栏不显示前台服务通知原因setSmallIcon传了无效资源 ID或者通知渠道没创建。解决用AContext.getApplicationInfo.icon拿 App 图标确保渠道 ID 和Notification.Builder里的一致。Android 8.0 以上不创建渠道通知直接不显示。5.5 现象Service 和主界面同时写 SQLite 导致锁库原因两个进程并发写同一个数据库文件。解决开启 WAL 模式或者让 Service 只写、主界面只读。Delphi 里执行PRAGMA journal_modeWAL;即可。如果还锁加busy_timeout。6. 进阶让 Service 在开机后自动启动并保活开机自启需要RECEIVE_BOOT_COMPLETED权限和一个BroadcastReceiver。Delphi 里可以写一个简单的 Receiver在onReceive里启动 Service。注意 Android 8.0 后不能直接startService要用startForegroundService。另外很多国产 ROM 会拦截自启需要用户手动在白名单里加 App。保活方面前台服务加START_STICKY返回值是基础但别指望能永久存活。我一般会在AndroidServiceStartCommand里返回START_STICKY并在onTaskRemoved里重启 Service。但说实话Android 的保活是玄学不同厂商策略不一样能做的就是尽量合规、省电、别被用户手动杀。最后说一个我自己的习惯每次改完 Service 代码先adb shell dumpsys activity services | grep Project1看 Service 状态再adb logcat看有没有异常。这个流程能省很多瞎猜的时间。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →