You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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和普通前台服务不够,需做以下调整:

  1. 完善前台服务通知
    恢复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;
    
  2. 添加服务重启逻辑
    重写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;
    
  3. 申请电池优化豁免
    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运行在独立进程,无法直接访问主进程数据库,可通过以下方式解决:

  1. 使用Content Provider共享数据库
    创建Content Provider封装数据库操作,Remote Service通过Content Provider执行增删改查。在AndroidManifest.xml中注册Provider,设置android:exported="true"(可通过权限限制访问)。

  2. 进程间通信(IPC)传递数据
    扩展TLocalServiceConnection的服务接口,Remote Service通过IPC请求主进程执行数据库插入操作,主进程处理后返回结果。

三、其他注意事项

  • 避免长期持有数据库连接,建议每次操作后关闭连接或使用连接池
  • 测试使用真机,模拟器后台行为与真机存在差异
  • 国内厂商定制系统(小米、华为等)限制更严格,需引导用户开启"自启动权限"和"后台运行权限"

内容的提问来源于stack exchange,提问作者Guilherme Camargo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 03:03:10