Delphi 11.1中如何让Android Service在APP关闭后持续运行?
Delphi 11.1 Android后台服务保活+SQLite访问问题
问题说明
使用Delphi 11.1开发Android应用,需要实现一个能在APP被用户关闭或系统杀死后持续运行的Service,长期访问应用文档目录下的SQLite数据库并插入数据。
已尝试方案:
- 创建本地Service,使用
START_STICKY标记,在StartCommand事件中调用StartForeground()方法 - 尝试过Remote类型服务,但无法访问应用的SQLite数据库
当前状态:
- APP运行时代码正常,能插入并加载数据
- 关闭APP后服务会被系统杀死
服务数据模块(UDMService.pas)
unit UDMService; interface uses System.SysUtils, System.Classes, System.IOUtils, System.Android.Service, AndroidApi.JNI.GraphicsContentViewText, Androidapi.JNI.Os, FireDAC.Stan.Intf, FireDAC.Stan.Option, FireDAC.Stan.Error, FireDAC.UI.Intf, FireDAC.Phys.Intf, FireDAC.Stan.Def, FireDAC.Stan.Pool, FireDAC.Stan.Async, FireDAC.Phys, FireDAC.Phys.SQLite, FireDAC.Phys.SQLiteDef, FireDAC.Stan.ExprFuncs, FireDAC.ConsoleUI.Wait, FireDAC.DApt, FireDAC.Phys.SQLiteWrapper.Stat, Data.DB, FireDAC.Comp.Client; type TThreadTeste = class(TThread) protected procedure Execute; override; end; TDM = class(TAndroidService) fdConDados: TFDConnection; FDPhysSQLiteDriverLink1: TFDPhysSQLiteDriverLink; function AndroidServiceStartCommand(const Sender: TObject; const Intent: JIntent; Flags, StartId: Integer): Integer; private { Private declarations } FThreadGravarTabela: TThread; procedure ConectarBanco; procedure GerarRegistroTabela; procedure GerarRegistrosTabela; procedure StartForeground; public { Public declarations } end; var DM: TDM; implementation {%CLASSGROUP 'FMX.Controls.TControl'} {$R *.dfm} uses Androidapi.JNI.App, Androidapi.JNI.Support, Androidapi.Helpers; procedure TDM.StartForeground; var {$if COMPILERVERSION > 34} LBuilder: Japp_NotificationCompat_Builder; {$else} LBuilder: JNotificationCompat_Builder; {$endif} begin try //if FIsForeground or not TAndroidHelperEx.CheckBuildAndTarget(26) then //Exit; // <====== LBuilder := TJapp_NotificationCompat_Builder.JavaClass.init(TAndroidHelper.Context); LBuilder.setAutoCancel(True); LBuilder.setContentTitle(StrToJCharSequence('Sample')); LBuilder.setContentText(StrToJCharSequence('Monitoring location changes')); LBuilder.setSmallIcon(TAndroidHelper.Context.getApplicationInfo.icon); LBuilder.setTicker(StrToJCharSequence('Sample 2')); TJService.Wrap(System.JavaContext).startForeground(3987, LBuilder.build); except end; end; function TDM.AndroidServiceStartCommand(const Sender: TObject; const Intent: JIntent; Flags, StartId: Integer): Integer; begin StartForeground; ConectarBanco; //GerarRegistroTabela; GerarRegistrosTabela; Result := TJService.JavaClass.START_STICKY; end; procedure TDM.ConectarBanco; begin fdconDados.Params.Values['Database'] := TPath.Combine(TPath.GetDocumentsPath, 'MyServicePOC.db3'); fdconDados.Params.Add('SharedCache=False'); fdconDados.UpdateOptions.LockWait := True; fdconDados.Connected := True; end; procedure TDM.GerarRegistrosTabela; begin FThreadGravarTabela := TThreadTeste.Create(True); FThreadGravarTabela.FreeOnTerminate := False; FThreadGravarTabela.Start; end; procedure TDM.GerarRegistroTabela; begin var AQuery: TFDQuery := TFDQuery.Create(nil); AQuery.Connection := fdConDados; try with AQuery, sql do begin Clear; add('INSERT INTO tabela (data) VALUES (:data)'); ParamByName('data').AsDateTime := Now; ExecSQL; end; finally AQuery.Free; end; end; { TThreadTeste } procedure TThreadTeste.Execute; begin var i: integer := 0; var AQuery: TFDQuery := TFDQuery.Create(nil); AQuery.Connection := DM.fdConDados; try while not Terminated do begin Inc(i); try with AQuery, sql do begin Clear; add('INSERT INTO tabela (data) VALUES (:data)'); ParamByName('data').AsDateTime := Now; ExecSQL; end; except end; Sleep(MSecsPerSec*10); //30 secs if i > 10 then Terminate; end; finally AQuery.Free; end; end; end.
应用主窗体(UMainApp.pas)
unit UMainApp; interface uses System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants, FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.Controls.Presentation, FMX.StdCtrls, System.Android.Service, FireDAC.Stan.ExprFuncs, FireDAC.Phys.SQLiteWrapper.Stat, FireDAC.Phys.SQLiteDef, FireDAC.Stan.Intf, FireDAC.Stan.Option, FireDAC.Stan.Error, FireDAC.UI.Intf, FireDAC.Phys.Intf, FireDAC.Stan.Def, FireDAC.Stan.Pool, FireDAC.Stan.Async, FireDAC.Phys, FireDAC.Phys.SQLite, FireDAC.FMXUI.Wait, Data.DB, FireDAC.Comp.Client, System.IOUtils, FMX.Memo.Types, FireDAC.DApt, FMX.ScrollBox, FMX.Memo, FMX.ListView.Types, FMX.ListView.Appearances, FMX.ListView.Adapters.Base, FMX.ListView; type TForm1 = class(TForm) Button1: TButton; FDPhysSQLiteDriverLink1: TFDPhysSQLiteDriverLink; fdConDados: TFDConnection; mmLog: TMemo; ButAtualizarLista: TButton; ListView1: TListView; procedure butStartServiceClick(Sender: TObject); procedure FormCreate(Sender: TObject); procedure ButAtualizarListaClick(Sender: TObject); private { Private declarations } ConexaoServico : TLocalServiceConnection; procedure AtualizarEstruturaBase; function GetDadosTabelaTestes: TDataSet; public { Public declarations } end; var Form1: TForm1; implementation {$R *.fmx} procedure TForm1.AtualizarEstruturaBase; function TabelaExiste(const pTable: String): Boolean; begin var AQuery: TFDQuery := TFDQuery.Create(nil); AQuery.Connection := fdConDados; try AQuery.Open('PRAGMA table_info(' + QuotedStr(pTable) + ')'); result := AQuery.RecordCount > 0; finally AQuery.Free; end; end; const SQL_CRIAR_TABELA_TESTES = 'CREATE TABLE tabela' + ' (id INTEGER PRIMARY KEY AUTOINCREMENT,' + ' data DATETIME)'; begin var AQuery: TFDQuery := TFDQuery.Create(nil); AQuery.Connection := fdConDados; try if not (TabelaExiste('tabela')) then AQuery.ExecSQL(SQL_CRIAR_TABELA_TESTES); finally AQuery.Free; end; end; procedure TForm1.ButAtualizarListaClick(Sender: TObject); begin ListView1.Items.Clear; var ADataSet: TDataSet := GetDadosTabelaTestes; with ADataSet do begin First; while not (Eof) do begin ListView1.Items.Add.Text := Format('%d - %s', [FieldByName('id').AsInteger, FormatDateTime('dd/mm/yyyy hh:nn:ss', FieldByName('data').AsDateTime)]); Next; end; end; end; procedure TForm1.butStartServiceClick(Sender: TObject); begin ConexaoServico := TLocalServiceConnection.Create; TThread.CreateAnonymousThread( procedure begin ConexaoServico.StartService('MyService2'); end).Start; end; procedure TForm1.FormCreate(Sender: TObject); begin try fdconDados.Params.Values['Database'] := TPath.Combine(TPath.GetDocumentsPath, 'MyServicePOC.db3'); fdconDados.Params.Add('SharedCache=False'); fdconDados.UpdateOptions.LockWait := True; fdconDados.Connected := True; AtualizarEstruturaBase; except on e: exception do mmLog.Lines.Add('[TForm1.FormCreate]'+e.Message) end; end; function TForm1.GetDadosTabelaTestes: TDataSet; begin fdConDados.ExecSQL('SELECT * FROM tabela ORDER BY id, data', result); end; end.
解决方案
一、本地Service保活优化(解决APP关闭后被杀死的问题)
Android 8.0+对后台服务限制严格,仅靠START_STICKY和普通前台服务不够,需做以下调整:
完善前台服务通知
恢复Android版本判断,适配8.0+的通知渠道要求,防止通知被清除导致服务降级:procedure TDM.StartForeground; var {$if COMPILERVERSION > 34} LBuilder: Japp_NotificationCompat_Builder; {$else} LBuilder: JNotificationCompat_Builder; {$endif} LNotificationChannel: JNotificationChannel; LNotificationManager: JNotificationManager; LChannelID: JString; begin try LChannelID := StrToJCharSequence('MyServiceChannel'); if TAndroidHelper.SdkVersion >= 26 then begin LNotificationManager := TJNotificationManager.Wrap( TAndroidHelper.Context.getSystemService(TJContext.JavaClass.NOTIFICATION_SERVICE)); LNotificationChannel := TJNotificationChannel.JavaClass.init( LChannelID, StrToJCharSequence('后台服务'), TJNotificationManager.JavaClass.IMPORTANCE_LOW); LNotificationChannel.setDescription(StrToJCharSequence('用于保持服务后台运行')); LNotificationManager.createNotificationChannel(LNotificationChannel); end; LBuilder := TJapp_NotificationCompat_Builder.JavaClass.init(TAndroidHelper.Context, LChannelID); LBuilder.setAutoCancel(False); // 禁止用户手动关闭通知 LBuilder.setContentTitle(StrToJCharSequence('服务运行中')); LBuilder.setContentText(StrToJCharSequence('正在同步数据')); LBuilder.setSmallIcon(TAndroidHelper.Context.getApplicationInfo.icon); LBuilder.setOngoing(True); // 标记为持续通知,无法滑动清除 TJService.Wrap(System.JavaContext).startForeground(3987, LBuilder.build); except // 捕获异常避免服务启动失败 end; end;添加服务重启逻辑
重写AndroidServiceDestroy方法,发送重启广播;同时创建广播接收器接收广播并重启服务:// 在服务单元中添加广播接收器类 TServiceRestartReceiver = class(TBroadcastReceiver) protected procedure OnReceive(Context: JContext; Intent: JIntent); override; end; // 实现OnReceive方法 procedure TServiceRestartReceiver.OnReceive(Context: JContext; Intent: JIntent); var LServiceIntent: JIntent; begin LServiceIntent := TJIntent.JavaClass.init(Context, TDM.JavaClass); if TAndroidHelper.SdkVersion >= 26 then Context.startForegroundService(LServiceIntent) else Context.startService(LServiceIntent); end; // 重写服务销毁方法 procedure TDM.AndroidServiceDestroy(Sender: TObject); var LIntent: JIntent; begin inherited; LIntent := TJIntent.JavaClass.init(TAndroidHelper.Context.getPackageName() + '.RESTART_SERVICE'); LIntent.setPackage(TAndroidHelper.Context.getPackageName()); TJContext.Wrap(System.JavaContext).sendBroadcast(LIntent); end; // 服务创建时注册广播接收器 procedure TDM.AndroidServiceCreate(Sender: TObject); var LFilter: JIntentFilter; LReceiver: TServiceRestartReceiver; begin inherited; LFilter := TJIntentFilter.JavaClass.init; LFilter.addAction(StrToJCharSequence(TAndroidHelper.Context.getPackageName() + '.RESTART_SERVICE')); LReceiver := TServiceRestartReceiver.Create; TAndroidHelper.Context.registerReceiver(LReceiver, LFilter); end;申请电池优化豁免
Android 6.0+的Doze模式会限制后台服务,引导用户将应用加入电池优化白名单:procedure RequestIgnoreBatteryOptimizations; var LIntent: JIntent; LPackageName: JString; begin if TAndroidHelper.SdkVersion >= 23 then begin LPackageName := TAndroidHelper.Context.getPackageName; if not TJPowerManager.Wrap( TAndroidHelper.Context.getSystemService(TJContext.JavaClass.POWER_SERVICE)) .isIgnoringBatteryOptimizations(LPackageName) then begin LIntent := TJIntent.JavaClass.init(TJSettings.JavaClass.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); LIntent.setData(TJUri.JavaClass.parse('package:' + JStringToString(LPackageName))); TAndroidHelper.Activity.startActivity(LIntent); end; end; end;在主窗体
FormCreate中调用此方法,引导用户授权。
二、Remote Service访问SQLite数据库的解决方案
Remote Service运行在独立进程,无法直接访问主进程数据库,可通过以下方式解决:
使用Content Provider共享数据库
创建Content Provider封装数据库操作,Remote Service通过Content Provider执行增删改查。在AndroidManifest.xml中注册Provider,设置android:exported="true"(可通过权限限制访问)。进程间通信(IPC)传递数据
扩展TLocalServiceConnection的服务接口,Remote Service通过IPC请求主进程执行数据库插入操作,主进程处理后返回结果。
三、其他注意事项
- 避免长期持有数据库连接,建议每次操作后关闭连接或使用连接池
- 测试使用真机,模拟器后台行为与真机存在差异
- 国内厂商定制系统(小米、华为等)限制更严格,需引导用户开启"自启动权限"和"后台运行权限"
内容的提问来源于stack exchange,提问作者Guilherme Camargo
相关产品推荐
相关产品推荐

