ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Delphi开发Android后台Service完整指南:从JNI到前台服务

Delphi开发Android后台Service完整指南:从JNI到前台服务 简介一份针对 Delphi 开发者的安卓服务示例包同时提供 XE5 与 XE7 两个版本的工程代码适合需要在 Delphi 中实现后台运行任务的移动应用开发者。包内共含 32 个文件大小约 69KB核心部分以 Pascal 源码文件与 Java 辅助文件为主并配套工程配置文件、安卓清单文件、界面定义文件以及构建脚本构成一套完整的服务创建、注册与启动示例。压缩包按 XE5、XE7 分目录组织便于快速定位对应版本。目前已有 279 人学习下载。通过阅读示例可以了解如何继承服务类并重写生命周期方法如何在安卓清单中声明服务以及如何在界面代码中通过启动或绑定方式调用服务同时还能掌握后台任务与界面组件之间的通信思路对于需要实现音乐播放、数据同步等长期后台功能的 Delphi 安卓应用开发者具有直接参考价值也可作为初学者理解安卓服务机制的入门案例。 上个月我在一个小项目里需要做一个Android端后台采集任务每到一个位置就记录一次数据然后定时把日志传回服务端。项目组技术栈一直用的是Delphi大家也习惯了VCL那套拖控件、写事件的节奏到了移动端才发现资料少得可怜尤其是Android Service这一块官方文档里只给了个极简示例真正往工程里一放就各种跑不起来。折腾了好几天把Service从建工程到真机验证这条链彻底捋了一遍。如果你也需要在Delphi开发的Android应用里加一个后台Service这篇应该能帮你省下这几天。这篇文章会从Delphi对Android Service的封装边界讲起再走一遍完整的Demo实现把Service注册、生命周期映射、线程模型、前后台通信这些关键环节逐个拆开最后把我实测遇到的坑按排查链路整理出来。适合已经有Delphi基础、但第一次碰Android Service的开发者也适合想判断Delphi到底能不能做这类后台任务的技术负责人参考。1. Delphi写Android Service的封装边界能直接用和必须自己补的部分1.1 为什么这个Demo值得做先说个很多人容易误会的点Delphi不是不能写Android后台服务而是FireMonkey框架在Service这层基本没给多少封装你没法像拖一个TButton那样拖一个后台服务组件出来必须自己去碰Android的JNI接口。这和你熟悉VCL里成千上万的可视组件相比体验落差会很大。但这个落差是正常的搞清楚它能做什么、不能做什么心里有底了后面写代码反倒顺。我这次做的Demo选型很简单一个独立的Service类通过startService方式启动在onStartCommand里写日志再通过广播把消息抛给主界面。整个过程中Delphi负责的部分是工程结构、生命周期方法的声明、JNI类型映射和编译打包Android原生机制负责的部分是Service调度、权限控制和系统回收策略。你把这两层分清楚遇到问题才知道去哪找答案。1.2 打破幻想FireMonkey没有给你一个服务组件在VCL里你习惯了组件面板上拖一个TTimer、写一行代码它就跑但在Delphi做Android Service你要面对的是直接用Java类映射。以最常见的写法为例你需要自己声明一个继承自TJavaLocal、实现JService接口的类然后重写onCreate、onStartCommand、onDestroy这些方法。类名、包名、方法签名都要符合Android的约定Delphi只是把你的Object Pascal代码翻译成Java字节码并不会给你套一层Delphi风格的Service组件。这带来一个实际影响很多你在Delphi桌面端顺手的东西在Service里不能用。比如FMX的TTimer依赖消息循环你在纯JNI层的Service里直接放一个TTimer它可能根本不触发再比如VCL的TForm、TDataModule的生命周期和Android的Service生命周期完全是两回事不能拿桌面开发那套窗体关闭任务就结束的思路套到移动端。1.3 Delphi版本的差异对写法的影响Delphi XE7开始正式支持Android开发但Service相关的JNI映射在不同版本里差别不小。早期版本里TAndroidHelper还没出来获取Context的方式很别扭到了Delphi 10.4、11、12这几个版本Androidapi.Helpers里的TAndroidHelper.Context已经非常稳定代码写法统一了很多。我做Demo用的环境是Delphi 11.3配Android SDK 33下面的代码在这个组合下可以编译通过。如果你是Delphi 10.4或者12个别方法名可能有细微差异这个只能以本机IDE里按CtrlSpace弹出来的接口声明为准。别拿着网上老掉牙的代码直接粘贴那是很多踩坑的开始。2. Service要想跑起来先过这三关注册、生命周期与线程模型2.1 AndroidManifest.xmlService不是写了类就会被系统发现很多第一次写的兄弟都会在这里卡很久类写了方法也写了运行起来却报找不到Service。原因是Android不像VCL那样只要有类就能用Service必须在AndroidManifest.xml里通过service节点注册系统才能找到它。Delphi工程里这个文件对应的是AndroidManifest.template.xml在Project Manager的Android节点下可以找到。你需要往application节点下加一段service android:namecom.embarcadero.myapp.DemoService android:exportedfalse /这里android:name的值必须和Service单元的命名空间、类名完全一致包括包名。我建议把Service类固定放在某个命名空间下不要图省事用默认的空命名空间后面打包和排查会清晰很多。android:exportedfalse的意思是其他应用不能越权调用你的Service这是个安全习惯除非你确实需要跨应用调用否则一律设false。2.2 生命周期映射onCreate、onStartCommand、onDestroy该在哪里放业务Android Service的生命周期和Activity是两条线但和Delphi的事件模型很相似理解成创建、启动、销毁三个时机就行。我习惯把这三件事的职责分得很清onCreate只做一次性初始化比如读取配置、打开数据库连接、初始化日志句柄。这个方法的返回类型是JIBinder如果不需要绑定返回nil。onStartCommand每次startService都会进入这里所以真正要执行的业务逻辑放这个函数。它的返回值是一个整数告诉系统进程被回收之后要不要尝试重建我。onDestroy释放你在onCreate里占用的资源关掉线程和数据库连接。这个方法的调用时机不受你控制系统在回收Service前会先走这里所以不要把重要数据的持久化放在onDestroy里赌运气。还有一个onBind方法。Delphi里用纯JNI方式写Service时onBind通常直接返回nil表示不支持绑定。但如果你做的功能需要绑定返回值给调用方事情就复杂了后面我会说为什么Demo阶段别碰它。2.3 谁在跑你的代码主线程与后台线程的界限这可能是新手最容易出安全问题的一关。记住了onStartCommand执行在进程的主线程但Service本身不会因为你在里面写了耗时操作就变成后台线程。你如果在onStartCommand里写个while循环、下载一个大文件这个操作会卡住整个应用的UIAndroid系统检测到主线程阻塞超过5秒就会弹ANRApplication Not Responding。正确的做法是onStartCommand里只负责派发任务把耗时的活交到单独线程。Delphi里最直接的选择就是TThread.CreateAnonymousThread或者用Delphi的匿名线程池。现场演示时我用了一个简单例子TThread.CreateAnonymousThread( procedure begin // 在这里做网络请求、文件读写、定位采集 Log(child thread running); end ).Start;顺手提一下有相关热搜里问到ITask和匿名线程区别。ITask从语义上看更轻量但生命周期不好控制对Service这种随时可能被系统杀掉再重建的场景匿名线程更直观。我这边的经验是Service里坚持用TThread别引复杂线程库给自己找麻烦。3. 从空工程到跑通Demo完整实现步骤3.1 环境准备与工程配置先确认你的Delphi版本带Android SDK环境。安装时如果没装SDKTools Options Deployment SDK Manager里可以补。我个人建议SDK版本不用追最新带Android API 33就够跑绝大多数设备。创建工程时Target Platforms里勾上Android平台如果之后发现Android选项是灰的多半是SDK路径没配对。建好空工程之后菜单File New Other在Delphi Projects分类下能找到Android Service向导。个别版本名称可能写成Android ServiceService别和普通的Data Module搞混。向导生成的骨架会包含Service类的声明、AndroidManifest里注册的占位信息你在这个基础上改比从空白JNI写起要稳得多。3.2 创建Service单元并重写核心方法我用的Service单元结构是这样注释里说明了每个方法职责unit uDemoService; interface uses Androidapi.JNI.App, Androidapi.JNI.GraphicsContentViewText, Androidapi.JNI.Os, Androidapi.Helpers, Androidapi.JNIBridge; type TDemoService class(TJavaLocal, JService) private FTag: string; public function onCreate: JIBinder; cdecl; function onStartCommand(AIntent: JIntent; AFlags: Integer; AStartId: Integer): Integer; cdecl; procedure onDestroy; cdecl; function onBind(AIntent: JIntent): JIBinder; cdecl; end; implementation uses System.SysUtils; function TDemoService.onCreate: JIBinder; begin FTag : DelphiDemoService; // 初始化日志、连接池等 Result : nil; end; function TDemoService.onStartCommand(AIntent: JIntent; AFlags, AStartId: Integer): Integer; begin // 业务逻辑入口尽量只派发任务 TJLog.JavaClass.i(StringToJString(FTag), StringToJString(onStartCommand fired, startId AStartId.ToString)); Result : TJContext.JavaClass.START_STICKY; end; procedure TDemoService.onDestroy; begin // 释放资源 end; function TDemoService.onBind(AIntent: JIntent): JIBinder; begin Result : nil; end; end.注意TJLog.JavaClass.i是Delphi里调Android的Log.i方法第一个参数是TAG第二个是内容。日志在后续验证阶段非常重要我甚至建议你在每个生命周期方法里都加一条日志这样系统什么时候杀了你、什么时候又拉起你一清二楚。3.3 在Manifest里注册Service并设置导出属性向导生成骨架时Manifest里通常已经有一行service注册。但它的包名和类名是模板默认值不改成你的实际类名运行时一定找不到。我把Manifest里的默认占位全部替换成上面代码的包名和类名再确认了android:exportedfalse。还有一个很多人漏掉的点如果Service里要执行定位、网络请求这类敏感操作Manifest里要声明对应的权限。比如定位采集就需要ACCESS_FINE_LOCATION和ACCESS_COARSE_LOCATION网络请求需要INTERNET。Delphi空工程默认带了一部分权限但细粒度的权限你得自己加。别等真机运行时报SecurityException再回来补。3.4 从主界面启动、停止Service现在回到主窗体的按钮事件写启动和停止的代码。核心是通过Intent显式指定要启动的Service而不是隐式匹配这样最稳妥。uses Androidapi.Helpers, Androidapi.JNI.GraphicsContentViewText; procedure TFormMain.btnStartServiceClick(Sender: TObject); var LIntent: JIntent; begin LIntent : TJIntent.Create; LIntent.setClassName(TAndroidHelper.Context.getPackageName(), StringToJString(com.embarcadero.myapp.DemoService)); TAndroidHelper.Context.startService(LIntent); end;停止的时候用同一个Intentprocedure TFormMain.btnStopServiceClick(Sender: TObject); var LIntent: JIntent; begin LIntent : TJIntent.Create; LIntent.setClassName(TAndroidHelper.Context.getPackageName(), StringToJString(com.embarcadero.myapp.DemoService)); TAndroidHelper.Context.stopService(LIntent); end;这里最容易踩的坑是setClassName的第一个参数。getPackageName返回的是应用包名不是开发者在IDE里看到的Project name两者可能不一样。你可以先TAndroidHelper.Context.getPackageName.toString打日志确认包名再拼Service类路径。3.5 用logcat验证Service是否真正活过代码写完先别急着上界面效果直接用Logcat验证。Delphi IDE里Run菜单下面有个Log View窗口但信息全的时候非常乱过滤条件也不是很好用。我更推荐连接真机后用命令行看adb logcat -s DelphiDemoService:V这样只显示tag为DelphiDemoService的日志。你点击启动按钮应该能看到onStartCommand那行日志证明Service确实启动起来了。如果没有先看Manifest的name字段再看setClassName里的包名路径很多时候问题就出在这两个地方。4. Service如何和主界面通信先选广播别急着碰Binder4.1 广播方案的发送端与接收端Service跑起来之后主界面要拿到它的状态或数据常见做法有两种一种是绑定Service通过Binder拿实例后直接调方法另一种是发广播谁想收谁注册接收器。我强烈建议Demo先用广播理由后面讲。发送端的写法很简单还是在onStartCommand里把业务结果塞进Intent然后sendBroadcastprocedure TDemoService.SendMessage(const ACmd: string; const AData: string); var LIntent: JIntent; begin LIntent : TJIntent.Create; LIntent.setAction(StringToJString(com.delphi.demo.SERVICE_MSG)); LIntent.putExtra(StringToJString(cmd), StringToJString(ACmd)); LIntent.putExtra(StringToJString(data), StringToJString(AData)); TAndroidHelper.Context.sendBroadcast(LIntent); end;接收端要定义一个BroadcastReceiver的子类。Delphi里写这个的套路是用一个TJavaLocal子类实现JBroadcastReceiver接口在onReceive里拿Intent里的数据。这里有个细节onReceive回调是在主线程执行的但方法本身是JNI回调你没法直接访问FMX里的VCL对象需要把数据先放到一个公共的位置再靠主线程去取。4.2 为什么Demo阶段不建议做Binder绑定绑定方式在原生Android开发里很常见主界面通过bindService拿到Service的Binder对象然后直接调用Service实例的公开方法。但Delphi的JNI映射在这层非常痛苦你要实现一个继承自JNI的Binder类要在Object Pascal和Java之间做大量类型转换还要自己维护绑定回调。对Demo来说这一套的复杂度远超它带来的便利。更重要的是如果你只是想让Service在后台跑独立任务然后定期把状态同步给界面广播已经够用了。绑定方式只有在需要像调用本地方法一样调用Service方法的密集交互场景里才值得。业务没到那个复杂度别把自己卷进去。4.3 接收端如何刷新UI全局状态轮询还是事件回调接收端收到广播之后最简单的刷新UI办法是定义一个全局的最新状态结构体onReceive里把数据写进去主窗体的TTimer每200毫秒轮询一次发现数据变了就更新Label。对Demo来说这个方式完全够用而且调试非常直观。更优雅一点的做法是拿到数据后往主线程抛一个匿名线程回调通过TThread.Synchronize去刷新FMX控件。但要注意onReceive的Context生命周期和你窗体的Context不是一回事Synchronize里访问Form对象时最好先判断Form是否已经释放。杀进程重启的场景里Form对象可能已经被系统销毁但全局变量还没清空直接访问会崩。5. 真机实测避坑记录从后台限制到日志不输出的完整排查链路5.1 Android 8.0后台执行限制startService不是你想调就能调Android 8.0开始系统对后台应用启动Service做了严格限制。什么叫后台你的App切到后台超过几分钟或者没有可见Activity系统就认为你处于后台。这时候你调startService不会静默失败而是直接抛IllegalStateException日志里能看到Background service start exception。解决方法有三个方向一是把Service提升为前台服务通过startForeground让系统明白你这个任务有正在进行的通知二是用JobScheduler、WorkManager这类延迟任务调度器但Delphi里没有现成封装要自己写JNI三是尽量缩短任务时间别让Service做无限循环的常驻操作。我测试时是先把App保持在前台跑通逻辑再切到后台验证限制问题。5.2 进程被系统回收START_STICKY不是免死金牌很多人在onStartCommand里返回START_STICKY以为这样进程被杀之后系统一定会重新拉起Service。真相是START_STICKY只是告诉系统可以尝试重建我但系统是否真的重建取决于内存压力、厂商策略、用户是否手动上滑清理。国产ROM对后台服务的管控尤其严格你在模拟器里测得好好的一上某些品牌真机Service说没就没。我的建议是别把START_STICKY当成可靠性保障它在原生Android、配置低的国外机型上表现还行但在国产ROM上你真正需要的是一个看门狗机制比如定时任务检查Service是否还活着死了就重新拉起来。5.3 前台服务与通知Channel让Service在后台脱引而出想稳定一点就必须把Service变成前台服务。前台服务简单理解就是带一条常驻通知的服务用户能在通知栏看到你这个App正在后台干活系统也会降低回收优先级。Delphi里实现前台服务核心是在onStartCommand里马上调startForeground。但Android 8.0以上必须先创建NotificationChannel否则通知不会显示Android 13以上还要动态申请POST_NOTIFICATIONS权限。这个流程在Delphi里不算复杂前提是你把方法名映射对var LChannelId: string; LBuilder: JNotification_Builder; LNotification: JNotification; begin LChannelId : demo_channel; // Android 8 先创建NotificationChannel // 再通过Notification.Builder构造通知最后调用startForeground(1, LNotification) end;代码细节不贴全了因为不同Delphi版本对Notification.Builder的映射差异很大。我说一下正确顺序检查权限、创建Channel、构造Notification、startForeground。你照着这个顺序去查对应版本的API基本不会错。5.4 日志看不到不要先怀疑代码日志是最容易让人崩溃的环节。有一次我确认Service逻辑没问题但logcat里就是什么都没有。排查了一圈才发现是用错了TAGLogcat默认会过滤非tag的日志你在代码里写的TAG是DelphiDemoService过滤条件却写了别的自然看不到。还有一个坑是Release打包。Delphi Release编译时如果勾选了某些代码混淆或裁剪选项日志输出会被优化掉。在你还没把Service逻辑彻底调稳定之前统一用Debug构建别为省那几十KB体积给自己添堵。5.5 线程阻塞与ANRService里别直接写死循环后台任务最容易写出的代码就是在onStartCommand里写个while True循环。这个写法在桌面开发里可能没事但在Android上系统会认为主线程无响应过一会儿就ANR然后弹窗问用户等待还是关闭。正确的后台循环结构是在onStartCommand里启动一个匿名线程线程里写循环循环的条件通过标志位控制在onDestroy里把标志位置false让线程自然退出。注意线程退出之后要确保没有残余引用否则Service被重建时旧线程还活着就会出现每次启动都多一个线程在跑的灵异现象。我在Demo里用一个Boolean字段FIsRunning做循环开关所有耗时逻辑都放到线程里实测下来内存和CPU表现都正常。6. 从Demo到能用的后台任务优化方向与个人体会6.1 定时任务别用FMX的TTimer第一次做Service的定时上报时我理所当然地在Service里放了一个TTimer结果完全没反应。原因是FMX的TTimer依赖应用的消息循环而Service在JNI层跑的时候没有Form的消息泵可以挂。要实现定时要么用Java的Handler类要么用AlarmManager设闹钟式唤醒要么走JobScheduler让系统帮你挑时间执行。对需要周期性上报的业务我的建议是按系统的时间策略来做真的需要准点执行的任务用AlarmManager否则用JobScheduler省电也省心。6.2 面向省电的策略调整Android系统现在很反感应永远活着的后台Service。省电模式下即使你用了前台服务系统也可能在屏幕熄灭一段时间后缩小CPU和网络的使用。硬扛不是办法业务上做三个调整会被系统更友好地对待任务能分批就分批一次处理完就停数据上传能等WiFi就连WiFi不要在移动网络下频繁重试空闲时给Service一个休眠状态让轮询间隔动态拉长。6.3 我实测后的总结和建议踩过一轮之后我的体会是Delphi在Android Service这层确实没有VCL那种舒适感很多封装要自己补但这不代表做不到。关键是先接受这是Android的系统机制这个事实然后顺着Android的规则做而不是拿VCL的习惯硬套。我建议你在Demo基础上先把日志链路跑通再加线程、加广播、加前台服务每一步都用logcat确认状态。Delphi每个版本升级后都要重新跑一遍这个Demo因为JNI映射很容易在版本更新时出现微妙变化。最后再提醒一句Service不是后台任务的唯一答案短任务用线程池延迟任务用系统调度器只有那些真的需要长时间运行的场景才值得让Service常驻。想明白这个边界你的App在系统眼里才是一个合格的公民。本文还有配套的精品资源点击获取
返回列表