You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Android AlarmManager调度SQL Server存储过程任务遇OnReceive循环问题

解决Xamarin.Android闹钟接收器循环触发问题&实现周期性任务调度需求

首先,你的AlarmReceiver陷入循环,大概率是因为每次OnReceive执行时,你又重复设置了和当前触发条件完全一致的闹钟,导致触发→设置→再触发的无限循环。结合你的需求(周二启动每小时执行,拿到数据后停止,下周二重启),我们需要重新梳理调度逻辑,分阶段处理闹钟的设置。

核心逻辑拆解

我们的任务需要三个阶段的调度:

  • 阶段1:等待下一个周二的到来,触发首次执行
  • 阶段2:从周二开始,每小时执行一次,直到存储过程返回数据
  • 阶段3:数据返回后,停止当前的小时调度,直接设置下一个周二的闹钟,等待下一轮周期

具体代码实现

1. MainActivity中初始化第一个闹钟(设置下一个周二的触发时间)

在MainActivity的OnCreate或者启动逻辑里,只需要设置下一个周二的首次触发闹钟,不需要设置重复闹钟:

protected override void OnCreate(Bundle savedInstanceState)
{
    base.OnCreate(savedInstanceState);
    // ... 其他初始化代码

    // 初始化第一个下周二的闹钟
    ScheduleNextTuesdayAlarm();
}

// 计算下一个周二的0点(或者你希望的启动时间)并设置闹钟
private void ScheduleNextTuesdayAlarm()
{
    var now = DateTime.Now;
    // 计算下一个周二:周二是星期2(注意:DayOfWeek.Sunday是0,所以Tuesday是2)
    int daysUntilNextTuesday = ((int)DayOfWeek.Tuesday - (int)now.DayOfWeek + 7) % 7;
    // 如果今天已经是周二,且当前时间已经过了你要启动的时间,就加7天
    if (daysUntilNextTuesday == 0 && now.Hour >= 0) // 这里可以改成你希望的启动小时,比如9点就写>=9
    {
        daysUntilNextTuesday = 7;
    }
    var nextTuesday = now.AddDays(daysUntilNextTuesday).Date; // 默认0点,你可以加Hours调整启动时间
    // 转换为Android的Calendar时间
    var calendar = Calendar.GetInstance(Java.Util.TimeZone.Default);
    calendar.Set(CalendarField.Year, nextTuesday.Year);
    calendar.Set(CalendarField.Month, nextTuesday.Month - 1); // Android月份从0开始
    calendar.Set(CalendarField.DayOfMonth, nextTuesday.Day);
    calendar.Set(CalendarField.HourOfDay, 0); // 这里设置启动的小时,比如9点就写9
    calendar.Set(CalendarField.Minute, 0);
    calendar.Set(CalendarField.Second, 0);

    // 设置一次性闹钟,触发后进入阶段2
    var alarmManager = (AlarmManager)GetSystemService(AlarmService);
    var intent = new Intent(this, typeof(AlarmReceiver));
    intent.PutExtra("AlarmType", "InitialTuesday"); // 标记是首次周二触发
    var pendingIntent = PendingIntent.GetBroadcast(this, 0, intent, PendingIntentFlags.UpdateCurrent | PendingIntentFlags.Immutable);

    // 使用精确闹钟(Android 12+需要SCHEDULE_EXACT_ALARM权限)
    if (Build.VERSION.SdkInt >= BuildVersionCodes.S)
    {
        if (alarmManager.CanScheduleExactAlarms())
        {
            alarmManager.SetExactAndAllowWhileIdle(AlarmType.RtcWakeup, calendar.TimeInMillis, pendingIntent);
        }
        else
        {
            // 引导用户开启权限
            StartActivity(new Intent(Android.Provider.Settings.ActionRequestScheduleExactAlarm));
        }
    }
    else
    {
        alarmManager.SetExactAndAllowWhileIdle(AlarmType.RtcWakeup, calendar.TimeInMillis, pendingIntent);
    }
}

2. AlarmReceiver的正确实现(避免循环,处理任务流程)

在Receiver里,我们需要根据闹钟类型处理不同逻辑:首次触发则设置每小时重复闹钟,执行存储过程后判断是否返回数据,有数据则停止当前重复闹钟并设置下周二的闹钟:

[BroadcastReceiver(Enabled = true, Exported = true)]
public class AlarmReceiver : BroadcastReceiver
{
    public override void OnReceive(Context context, Intent intent)
    {
        var alarmType = intent.GetStringExtra("AlarmType");
        switch (alarmType)
        {
            case "InitialTuesday":
                // 首次周二触发,开始设置每小时重复的闹钟
                ScheduleHourlyAlarm(context);
                // 立即执行一次存储过程任务
                ExecuteSqlTaskAndCheckResult(context);
                break;
            case "Hourly":
                // 每小时触发,执行任务
                ExecuteSqlTaskAndCheckResult(context);
                break;
        }
    }

    // 设置每小时重复的闹钟
    private void ScheduleHourlyAlarm(Context context)
    {
        var alarmManager = (AlarmManager)context.GetSystemService(Context.AlarmService);
        var intent = new Intent(context, typeof(AlarmReceiver));
        intent.PutExtra("AlarmType", "Hourly");
        // 使用不同的requestCode区分小时闹钟和周二初始闹钟,避免覆盖
        var pendingIntent = PendingIntent.GetBroadcast(context, 1, intent, PendingIntentFlags.UpdateCurrent | PendingIntentFlags.Immutable);

        // 设置每小时重复的闹钟(Android 12+注意权限)
        if (Build.VERSION.SdkInt >= BuildVersionCodes.M)
        {
            alarmManager.SetInexactRepeating(AlarmType.RtcWakeup, SystemClock.ElapsedRealtime() + 3600 * 1000, AlarmManager.IntervalHour, pendingIntent);
        }
        else
        {
            alarmManager.SetRepeating(AlarmType.RtcWakeup, SystemClock.ElapsedRealtime() + 3600 * 1000, AlarmManager.IntervalHour, pendingIntent);
        }
    }

    // 执行存储过程并判断结果
    private async void ExecuteSqlTaskAndCheckResult(Context context)
    {
        bool hasData = false;
        try
        {
            // 这里调用你的SQL Server存储过程,判断是否返回数据
            // 示例伪代码:
            // var connection = new SqlConnection("你的连接字符串");
            // await connection.OpenAsync();
            // var command = new SqlCommand("你的存储过程名", connection);
            // command.CommandType = CommandType.StoredProcedure;
            // var reader = await command.ExecuteReaderAsync();
            // hasData = reader.HasRows;
            // await reader.CloseAsync();
            // await connection.CloseAsync();
        }
        catch (Exception ex)
        {
            // 处理异常,比如日志记录
            System.Diagnostics.Debug.WriteLine($"执行存储过程出错:{ex.Message}");
        }

        if (hasData)
        {
            // 发送通知给用户
            SendNotification(context, "任务完成", "存储过程已返回数据");
            // 停止当前的每小时重复闹钟
            CancelHourlyAlarm(context);
            // 设置下一个周二的初始闹钟,准备下一轮周期
            ScheduleNextTuesdayAlarm(context);
        }
    }

    // 取消每小时重复的闹钟
    private void CancelHourlyAlarm(Context context)
    {
        var alarmManager = (AlarmManager)context.GetSystemService(Context.AlarmService);
        var intent = new Intent(context, typeof(AlarmReceiver));
        var pendingIntent = PendingIntent.GetBroadcast(context, 1, intent, PendingIntentFlags.UpdateCurrent | PendingIntentFlags.Immutable);
        alarmManager.Cancel(pendingIntent);
    }

    // 设置下一个周二的闹钟(和MainActivity里的方法逻辑一致,这里提取为公共方法)
    private void ScheduleNextTuesdayAlarm(Context context)
    {
        var now = DateTime.Now;
        int daysUntilNextTuesday = ((int)DayOfWeek.Tuesday - (int)now.DayOfWeek + 7) % 7;
        if (daysUntilNextTuesday == 0 && now.Hour >= 0) // 同样可以调整启动小时判断
        {
            daysUntilNextTuesday = 7;
        }
        var nextTuesday = now.AddDays(daysUntilNextTuesday).Date;
        var calendar = Calendar.GetInstance(Java.Util.TimeZone.Default);
        calendar.Set(CalendarField.Year, nextTuesday.Year);
        calendar.Set(CalendarField.Month, nextTuesday.Month - 1);
        calendar.Set(CalendarField.DayOfMonth, nextTuesday.Day);
        calendar.Set(CalendarField.HourOfDay, 0);
        calendar.Set(CalendarField.Minute, 0);
        calendar.Set(CalendarField.Second, 0);

        var alarmManager = (AlarmManager)context.GetSystemService(Context.AlarmService);
        var intent = new Intent(context, typeof(AlarmReceiver));
        intent.PutExtra("AlarmType", "InitialTuesday");
        var pendingIntent = PendingIntent.GetBroadcast(context, 0, intent, PendingIntentFlags.UpdateCurrent | PendingIntentFlags.Immutable);

        if (Build.VERSION.SdkInt >= BuildVersionCodes.S)
        {
            if (alarmManager.CanScheduleExactAlarms())
            {
                alarmManager.SetExactAndAllowWhileIdle(AlarmType.RtcWakeup, calendar.TimeInMillis, pendingIntent);
            }
        }
        else
        {
            alarmManager.SetExactAndAllowWhileIdle(AlarmType.RtcWakeup, calendar.TimeInMillis, pendingIntent);
        }
    }

    // 发送通知的方法
    private void SendNotification(Context context, string title, string content)
    {
        var notificationManager = (NotificationManager)context.GetSystemService(Context.NotificationService);
        string channelId = "TaskNotificationChannel";

        // Android 8.0+需要创建通知渠道
        if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
        {
            var channel = new NotificationChannel(channelId, "任务通知", NotificationImportance.Default)
            {
                Description = "存储过程任务相关通知"
            };
            notificationManager.CreateNotificationChannel(channel);
        }

        var intent = new Intent(context, typeof(MainActivity));
        var pendingIntent = PendingIntent.GetActivity(context, 0, intent, PendingIntentFlags.UpdateCurrent | PendingIntentFlags.Immutable);

        var notification = new NotificationCompat.Builder(context, channelId)
            .SetContentTitle(title)
            .SetContentText(content)
            .SetSmallIcon(Resource.Drawable.icon)
            .SetContentIntent(pendingIntent)
            .SetAutoCancel(true)
            .Build();

        notificationManager.Notify(1, notification);
    }
}

关键注意点

  • 区分不同闹钟的RequestCode:初始周二闹钟用requestCode=0,小时重复闹钟用requestCode=1,这样取消时不会误删其他闹钟。
  • Android权限处理:Android 12及以上需要SCHEDULE_EXACT_ALARM权限,要在Manifest里添加<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />,并且在代码中判断是否有权限。
  • 避免循环的核心:只有首次周二触发时才设置小时重复闹钟,当数据返回后立即取消小时闹钟,不会再重复触发,同时直接设置下周二的闹钟,进入下一轮周期。
  • 后台执行限制:如果你的任务执行时间较长,建议使用WorkManager配合AlarmManager,或者在Receiver里启动一个前台服务,避免被系统杀死。

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

火山引擎 最新活动