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

Android后台心跳线程执行异常:Thread.sleep()计时混乱致MQTT连接断开,求稳定执行方案

Android后台心跳线程执行异常:Thread.sleep()计时混乱致MQTT连接断开,求稳定执行方案

嗨,看了你的问题和代码,我马上就发现了几个关键问题,咱们一步步拆解原因,再给你靠谱的解决方案:

先搞懂你遇到的奇怪现象为啥会发生

  1. 屏幕熄灭无充电时心跳间隔超长:这是Android的Doze模式/应用待机限制在搞鬼——系统为了省电,会大幅延迟甚至暂停后台普通线程的调度,Thread.sleep()的实际休眠时间会被系统强制拉长,根本不会按你设置的20秒准时唤醒线程。
  2. 出现远小于20000ms的间隔:这是你代码里的逻辑漏洞!当Thread.sleep()被系统中断(比如系统调整后台线程优先级),会直接抛出InterruptedException,你的代码catch异常后没有处理剩余睡眠时间,直接跳到发送消息的逻辑,导致距离上一次发送的时间远短于预期。
  3. 电池设置无限制仍无效:就算你给App开了电池无限制,Android的后台线程调度限制依然存在,普通Thread.sleep()在低功耗场景下本来就不适合做精准定时。

解决方案:从紧急治标到彻底治本

一、先修复当前线程的逻辑漏洞(快速解决短间隔问题)

你的run()方法里的异常处理和线程启动方式都有问题,先改这两处:

  1. 修复InterruptedException的处理逻辑
    当sleep被中断后,要重新计算剩余睡眠时间,而不是直接发送消息:

    @Override
    public void run() {
        long lastSendTime = System.currentTimeMillis();
        while (bloop) {
            try {
                // 计算距离下一次发送需要补睡的时间,确保每次间隔都是20秒
                long now = System.currentTimeMillis();
                long remainingSleep = sleepTime - (now - lastSendTime);
                if (remainingSleep > 0) {
                    Thread.sleep(remainingSleep);
                }
            } catch (InterruptedException e) {
                Log.d(TAG, sdf.format(System.currentTimeMillis()) + " 线程被中断: " + e.getMessage());
                // 中断后继续循环,重新计算睡眠时间,不直接发消息
                continue;
            }
            if (!bloop) break;
            
            // 记录本次发送时间,计算下一次间隔
            lastSendTime = System.currentTimeMillis();
            String logMsg = sdf.format(lastSendTime) + " / " + sleepTime + " / HB message";
            mqtts.sendMessage(topic_hb, logMsg);
            Log.d(TAG, logMsg);
        }
        // 停止时的日志和消息
        long stopTime = System.currentTimeMillis();
        String stopMsg = sdf.format(stopTime) + " / bloop = false";
        mqtts.sendMessage(topic_hb, stopMsg);
        Log.d(TAG, stopMsg);
    }
    
  2. 修复冗余的线程启动逻辑
    你的HeartBeat本身已经继承了Thread,没必要再新建t_wait线程,直接启动当前实例即可:

    public void startLoop() {
        if (!bloop) {
            bloop = true;
            this.setPriority(Thread.MAX_PRIORITY);
            this.start(); // 直接启动当前HeartBeat线程
            long now = System.currentTimeMillis();
            String startMsg = sdf.format(now) + " HeartBeat.start()";
            mqtts.sendMessage(topic_hb, startMsg);
            Log.d(TAG, startMsg);
        }
    }
    

二、用系统级定时替代Thread.sleep()(彻底解决低功耗下的定时不准)

普通线程的sleep在Android后台场景下天生不靠谱,必须用系统提供的定时组件:

方案1:AlarmManager(适合精准定时,Doze模式下也能触发)

AlarmManager是系统级定时服务,不受后台线程调度限制,用setExactAndAllowWhileIdle(API23+)可以在Doze模式下唤醒设备执行任务:

  • 步骤:
    1. 写一个广播接收器,接收Alarm触发的广播,在里面发送MQTT心跳;
    2. 每次触发后重新设置下一次Alarm,避免系统忽略重复定时。

示例代码片段:

// 心跳广播接收器
public class HeartbeatReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        // 调用MQTT发送心跳
        if (MQTTService.getInstance() != null) {
            MQTTService.getInstance().sendHeartbeat();
        }
        // 重新设置下一次心跳
        scheduleNextHeartbeat(context);
    }
}

// 定时方法
private static void scheduleNextHeartbeat(Context context) {
    AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
    Intent intent = new Intent(context, HeartbeatReceiver.class);
    PendingIntent pendingIntent = PendingIntent.getBroadcast(
        context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
    
    long nextTriggerTime = System.currentTimeMillis() + 20000;
    // Doze模式下也能触发的精准定时
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, nextTriggerTime, pendingIntent);
    } else {
        alarmManager.setExact(AlarmManager.RTC_WAKEUP, nextTriggerTime, pendingIntent);
    }
}
方案2:WorkManager(适合非精准的周期性任务,现代Android架构推荐)

WorkManager是Jetpack组件,自动适配系统低功耗策略,就算App被杀死,任务也会在合适时机执行:

  • 步骤:
    1. 写一个Worker类,在doWork()里发送MQTT心跳;
    2. 配置周期性任务,添加网络连接等约束。

示例代码片段:

// 心跳Worker
public class HeartbeatWorker extends Worker {
    private static final String TAG = "HeartbeatWorker";

    public HeartbeatWorker(@NonNull Context context, @NonNull WorkerParameters params) {
        super(context, params);
    }

    @NonNull
    @Override
    public Result doWork() {
        try {
            if (MQTTService.getInstance() != null) {
                MQTTService.getInstance().sendHeartbeat();
            }
            return Result.success();
        } catch (Exception e) {
            Log.e(TAG, "心跳发送失败", e);
            return Result.retry(); // 发送失败时重试
        }
    }
}

// 启动周期性任务
public void startHeartbeatWork() {
    PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(
        HeartbeatWorker.class, 20, TimeUnit.SECONDS)
        .setConstraints(new Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED) // 仅网络连接时执行
            .build())
        .build();
    // 唯一任务,重复启动时替换旧任务
    WorkManager.getInstance(this).enqueueUniquePeriodicWork(
        "HeartbeatWork", ExistingPeriodicWorkPolicy.REPLACE, workRequest);
}

三、优化MQTT客户端本身的心跳设置(从根源避免造轮子)

你其实没必要自己实现心跳!Paho MQTT客户端自带KeepAlive机制,解决屏幕熄灭后不发PINGREQ的问题:

// 配置MQTT连接选项
MqttConnectOptions options = new MqttConnectOptions();
options.setKeepAliveInterval(60); // 60秒无通信则自动发PINGREQ(客户端会每隔30秒发一次)
options.setAutomaticReconnect(true); // 断开后自动重连
options.setCleanSession(false); // 保持会话,重连后恢复订阅
options.setConnectionTimeout(10); // 连接超时时间

// 客户端实例配置
MqttAsyncClient client = new MqttAsyncClient(serverUri, clientId, new MemoryPersistence());
client.setCallback(new MqttCallbackExtended() {
    @Override
    public void connectComplete(boolean reconnect, String serverURI) {
        // 连接成功/重连成功后的逻辑
    }
    // 其他回调方法...
});
client.connect(options);

这样客户端会自动处理心跳、重连,比你自己写的线程可靠得多。


总结建议

  1. 先修复当前线程的异常处理逻辑,解决短间隔问题;
  2. 长期来看,替换成AlarmManager或WorkManager做系统级定时;
  3. 优先用MQTT客户端自带的KeepAlive机制,避免重复造轮子,这是最靠谱的方案。

火山引擎 最新活动