☰
Android MQTT消息不显示通知?心跳、QoS与前台服务三者对齐方案
2026/10/15 6:10:33 网站建设 项目流程

简介:本资源是一份面向Android应用开发者与物联网初学者的MQTT消息推送实战示例,聚焦在移动终端实现轻量级远程通信的核心能力——连接MQTT服务器、订阅主题、接收消息并实时显示于系统通知栏。项目基于Eclipse Paho Android客户端库构建,已集成完整服务生命周期管理、断线自动重连、多主题订阅及NotificationCompat通知封装逻辑,可直接编译运行或作为模块嵌入现有App。压缩包共607个文件,含134个flat资源文件(用于构建配置)、121个json配置与数据样本、188个xml布局与清单定义、8个核心Java业务类及配套gradle构建脚本,整体15.58MB,结构清晰、模块解耦,便于理解MQTT在Android后台服务中的典型集成模式。目前已有1142人学习下载,适合需要快速掌握物联网消息推送落地实践的中初级开发者参考与二次开发。

1. Android端MQTT消息推送落地:为什么通知栏收不到消息,90%不是服务端问题,而是客户端心跳、QoS与前台服务三者没对齐

你写好了MQTT连接代码,主题也订阅成功了,onMessageArrived回调里日志清清楚楚打印出“收到消息”,但通知栏就是不弹——不是系统拦截,不是权限没开,甚至不是Android 12+的后台启动限制在作祟。真实项目中,我见过某高校物联网课程设计组连续三天卡在这一步:用EclipseMQTT库连上本地Mosquitto,发Topicsensor/temperature,手机App日志显示已接收,通知却像被黑洞吸走。最后发现,是QoS=0下Broker未确认送达,而App在onMessageArrived里直接调NotificationManager.notify(),此时Activity可能已销毁,Context失效;更隐蔽的是,Android 8.0后startService()被禁,若未用startForegroundService()+startForeground()补全前台服务声明,系统会在几秒内杀掉进程,MQTT连接断开,后续消息全部丢失。这不是玄学,是Android生命周期、MQTT协议语义、通知机制三者咬合失败的必然结果。本文面向已能跑通基础MQTT连接、但卡在「消息来了却看不见」的开发者,不讲协议原理,只拆解从connect()到通知栏弹出的每一条可验证路径,覆盖paho-android最新稳定版(1.2.5)、Android 10~14兼容方案、以及三个必须手调的参数阈值。

2. 用Paho-Android在Android 10+设备上建立可靠MQTT连接:从依赖配置到连接参数硬编码

2.1 Gradle依赖与权限声明:避开Android 12+网络明文限制与后台服务变更

Paho-Android官方已停止维护,但其1.2.5版本(2021年发布)仍是当前最稳定的生产级选择,兼容Android 10至14。注意:不要用org.eclipse.paho:org.eclipse.paho.client.mqttv3(纯Java版),它缺少Android平台适配;必须用org.eclipse.paho:org.eclipse.paho.android.service(含Service封装)。在app/build.gradle中添加:

dependencies { implementation 'org.eclipse.paho:org.eclipse.paho.android.service:1.2.5' implementation 'androidx.work:work-runtime-ktx:2.9.0' // 用于后台任务兜底 }

权限声明需分Android版本处理。AndroidManifest.xml中:

<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Android 12+ 需显式声明前台服务类型 --> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <!-- Android 10+ 后台位置权限仅当需要时添加,本例无需 -->

提示:若MQTT服务器使用HTTP而非HTTPS(如本地测试用mqtt://192.168.1.100:1883),Android 9+默认禁止明文流量。需在res/xml/network_security_config.xml中配置:

<?xml version="1.0" encoding="utf-8"?> <network-security-config> <domain-config> <domain includeSubdomains="true">192.168.1.100</domain> <trust-anchors> <certificates src="system" /> </trust-anchors> <cleartextTrafficPermitted="true" /> </domain-config> </network-security-config>

并在AndroidManifest.xml的<application>标签中引用:android:networkSecurityConfig="@xml/network_security_config"。

2.2 MQTT客户端初始化与连接:必须设置keepAliveInterval与cleanSession的底层逻辑

Paho-Android通过MqttAndroidClient类封装连接,但其构造函数中的serverURI格式极易出错。常见错误是写成tcp://192.168.1.100:1883——这是错误的,正确格式为tcp://192.168.1.100:1883(注意是tcp://,不是mqtt://或ws://)。完整初始化代码如下:

// 在Application或Service中初始化 private MqttAndroidClient mqttClient; private String serverUri = "tcp://192.168.1.100:1883"; // 替换为你的MQTT服务器IP private String clientId = "android_" + Build.SERIAL; // 唯一ID,避免重复连接冲突 public void initMQTT(Context context) { mqttClient = new MqttAndroidClient(context.getApplicationContext(), serverUri, clientId); // 关键参数:keepAliveInterval设为60秒,低于30秒易被NAT网关断连 MqttConnectOptions options = new MqttConnectOptions(); options.setKeepAliveInterval(60); // 单位:秒,必须≥30 options.setCleanSession(true); // true表示每次连接都丢弃旧会话,适合通知类场景 options.setAutomaticReconnect(true); // 断线自动重连 options.setConnectionTimeout(30); // 连接超时30秒 // 设置用户名密码(若MQTT服务器启用认证) // options.setUserName("user"); // options.setPassword("pass".getBytes()); try { IMqttToken token = mqttClient.connect(options, null, new IMqttActionListener() { @Override public void onSuccess(IMqttToken asyncActionToken) { Log.d("MQTT", "连接成功"); subscribeToTopic(); // 连接成功后立即订阅 } @Override public void onFailure(IMqttToken asyncActionToken, Throwable exception) { Log.e("MQTT", "连接失败", exception); // 此处应触发重试逻辑,见第4章 } }); } catch (MqttException e) { Log.e("MQTT", "初始化异常", e); } }

参数说明:

  • keepAliveInterval=60:这是心跳包间隔。若设为10秒,在家庭路由器NAT表老化时间(通常30~60秒)内无法维持长连接,导致静默断连。实测中,30≤keepAliveInterval≤120为安全区间。
  • cleanSession=true:通知类应用不需要离线消息堆积。设为false时,Broker会保存未确认的QoS1消息,但Android App进程被杀后无法恢复会话,反而造成消息积压与重复投递。
  • automaticReconnect=true:Paho内部实现的指数退避重连(初始1秒,最大120秒),比手动轮询更省电。

2.3 订阅主题与消息到达回调:为什么onMessageArrived里不能直接new Intent

订阅操作必须在connect()成功回调中执行,否则会抛MqttException: Client is not connected。关键点在于:onMessageArrived回调运行在Paho的内部线程,不是主线程,且该回调持有的Context可能已失效(如Activity被回收)。因此,绝不能在此处直接调用startActivity()或NotificationManager.notify()——后者虽不崩溃,但因Context非Application Context,通知可能无法显示。

private void subscribeToTopic() { try { // QoS=1确保消息至少送达一次,通知类场景推荐 mqttClient.subscribe("sensor/temperature", 1, null, new IMqttActionListener() { @Override public void onSuccess(IMqttToken asyncActionToken) { Log.d("MQTT", "订阅成功:sensor/temperature"); } @Override public void onFailure(IMqttToken asyncActionToken, Throwable exception) { Log.e("MQTT", "订阅失败", exception); } }); } catch (MqttException e) { Log.e("MQTT", "订阅异常", e); } } // 消息到达回调:仅做数据解析与跨线程转发 @Override public void messageArrived(String topic, MqttMessage message) throws Exception { String payload = new String(message.getPayload()); Log.d("MQTT", "收到消息:" + topic + " -> " + payload); // 必须切到主线程处理UI相关操作 new Handler(Looper.getMainLooper()).post(() -> { handleIncomingMessage(topic, payload); }); }

handleIncomingMessage()方法负责构建通知,其内部必须使用getApplicationContext()获取全局Context,而非Activity的Context:

private void handleIncomingMessage(String topic, String payload) { // 解析JSON或纯文本,提取通知标题与内容 String title = "传感器告警"; String content = "温度:" + payload + "℃"; // 构建通知渠道(Android 8.0+必需) createNotificationChannel(); NotificationCompat.Builder builder = new NotificationCompat.Builder( getApplicationContext(), "mqtt_channel") .setSmallIcon(R.drawable.ic_notification) .setContentTitle(title) .setContentText(content) .setPriority(NotificationCompat.PRIORITY_HIGH) .setAutoCancel(true) .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)); NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getApplicationContext()); notificationManager.notify(System.currentTimeMillis(), builder.build()); }

3. Android 8.0+通知渠道创建与前台服务保活:解决“连接正常但收不到通知”的核心堵点

3.1 通知渠道必须在Application.onCreate()中创建,且渠道ID不可动态变更

Android 8.0(API 26)起,所有通知必须归属一个渠道(Channel),且渠道创建后ID与重要性不可修改。若在Activity中创建,App被杀后渠道消失,新通知无法显示。必须在自定义Application类的onCreate()中完成:

public class MyApplication extends Application { private static final String CHANNEL_ID = "mqtt_channel"; @Override public void onCreate() { super.onCreate(); createNotificationChannel(); } private void createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { CharSequence name = "MQTT消息通知"; String description = "接收传感器数据推送"; int importance = NotificationManager.IMPORTANCE_HIGH; // 高优先级确保横幅显示 NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance); channel.setDescription(description); channel.enableLights(true); channel.setLightColor(Color.RED); channel.enableVibration(true); channel.setVibrationPattern(new long[]{100, 200, 100, 200}); channel.setShowBadge(true); NotificationManager notificationManager = getSystemService(NotificationManager.class); notificationManager.createNotificationChannel(channel); } } }

注意:CHANNEL_ID(此处为"mqtt_channel")必须与NotificationCompat.Builder中传入的ID完全一致,且一旦创建,修改importance或name需用户手动在系统设置中重置,开发阶段务必一次定型。

3.2 前台服务声明与启动:绕过Android 12+后台服务限制的唯一合规路径

从Android 12(API 31)起,startService()在后台调用被彻底禁止。MQTT客户端需运行在前台服务中,否则系统会在App进入后台后10秒内终止服务。解决方案是:将MQTT逻辑封装进Service,并在onStartCommand()中调用startForeground()。

首先,在AndroidManifest.xml中声明服务:

<service android:name=".MQTTService" android:enabled="true" android:exported="false" android:foregroundServiceType="specialUse" /> <!-- Android 12+必需 -->

MQTTService.java核心逻辑:

public class MQTTService extends Service { private MqttAndroidClient mqttClient; @Override public void onCreate() { super.onCreate(); initMQTTClient(); // 初始化客户端,同2.2节 } @Override public int onStartCommand(Intent intent, int flags, int startId) { // 必须在onStartCommand中启动前台服务,否则Android 12+崩溃 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { startForeground(1, buildForegroundNotification()); } else { startForeground(1, buildForegroundNotification()); } return START_STICKY; // 进程被杀后自动重启 } private Notification buildForegroundNotification() { Intent notificationIntent = new Intent(this, MainActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE); return new NotificationCompat.Builder(this, "mqtt_channel") .setContentTitle("MQTT服务运行中") .setContentText("正在监听传感器消息...") .setSmallIcon(R.drawable.ic_service) .setContentIntent(pendingIntent) .build(); } @Override public IBinder onBind(Intent intent) { return null; } }

启动服务的时机:在MainActivity的onCreate()中调用:

// MainActivity.java @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 启动MQTT服务 Intent serviceIntent = new Intent(this, MQTTService.class); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { startForegroundService(serviceIntent); } else { startService(serviceIntent); } }

关键点:startForegroundService()必须在5秒内调用startForeground(),否则系统抛ForegroundServiceDidNotStartInTimeException。因此buildForegroundNotification()必须轻量,不涉及网络或耗时IO。

4. MQTT连接与消息接收的三大避坑指南:血泪经验总结的5个具体翻车点

4.1 现象:Logcat显示“Connected”但onMessageArrived永不触发

原因:Broker未正确配置max_inflight_messages,或客户端QoS与Broker策略冲突。Paho-Android默认QoS=0,但若Broker强制要求QoS=1(如某些企业级EMQX配置),订阅虽成功,消息却因QoS不匹配被静默丢弃。
解决:在subscribe()时显式指定QoS=1,并确认Broker日志无QoS mismatch警告。命令行测试:mosquitto_sub -h 192.168.1.100 -t "sensor/temperature" -q 1,若能收到则Broker支持QoS1。

4.2 现象:通知栏偶尔弹出,大部分消息丢失

原因:onMessageArrived回调中未加锁,多线程并发调用NotificationManager.notify()导致ID冲突。notify(id, builder.build())若id重复,后一次会覆盖前一次,用户只看到最后一条。
解决:使用System.currentTimeMillis()生成唯一id(如代码所示),或用AtomicInteger递增ID。绝对避免用固定数字如notify(1, ...)。

4.3 现象:Android 12设备上服务启动失败,报ForegroundServiceDidNotStartInTimeException

原因:startForeground()调用位置错误。若放在onCreate()而非onStartCommand(),或在onStartCommand()中做了耗时操作(如网络请求)再调用,则超时。
解决:严格按3.2节代码,在onStartCommand()第一行调用startForeground(),且buildForegroundNotification()中不调用任何耗时方法。

4.4 现象:App退到后台后1分钟内MQTT连接断开,onConnectionLost被调用

原因:未声明FOREGROUND_SERVICE权限,或Android 12+未设置android:foregroundServiceType="specialUse"。系统将服务识别为普通后台服务并强杀。
解决:检查AndroidManifest.xml中权限与服务声明是否完整,尤其android:foregroundServiceType属性不可省略。

4.5 现象:同一主题多条消息快速到达,通知栏只显示最后一条,且点击无响应

原因:setContentIntent()中PendingIntent未设置FLAG_IMMUTABLE(Android 12+强制要求),导致Intent被系统拒绝,点击后无动作。
解决:创建PendingIntent时,Android 12+必须用PendingIntent.FLAG_IMMUTABLE,兼容写法见3.2节代码。同时,NotificationCompat.Builder需调用.setAutoCancel(true),否则通知不会在点击后消失。

5. 消息解析与通知增强:从基础弹窗到可交互通知栏卡片的实战技巧

5.1 解析JSON Payload并动态生成通知内容:避免硬编码字段名

实际项目中,MQTT消息多为JSON格式,如{"device":"temp_01","value":25.3,"unit":"℃","timestamp":1712345678}。硬编码payload.split(":")[1]极不可靠。推荐用Gson解析(添加依赖implementation 'com.google.code.gson:gson:2.10.1'):

private void handleIncomingMessage(String topic, String payload) { try { JsonObject json = JsonParser.parseString(payload).getAsJsonObject(); String device = json.has("device") ? json.get("device").getAsString() : "未知设备"; String value = json.has("value") ? json.get("value").getAsString() : "N/A"; String unit = json.has("unit") ? json.get("unit").getAsString() : ""; String timestamp = json.has("timestamp") ? new SimpleDateFormat("MM-dd HH:mm", Locale.getDefault()) .format(new Date(json.get("timestamp").getAsLong() * 1000)) : ""; String title = device + " 数据更新"; String content = "值:" + value + unit + " | " + timestamp; // 构建通知... showNotification(title, content, json); } catch (Exception e) { Log.e("MQTT", "JSON解析失败", e); showNotification("MQTT消息", "格式错误:" + payload, null); } }

5.2 添加通知操作按钮:实现“一键查看详情”与“关闭告警”

Android 7.0+支持通知操作按钮。在NotificationCompat.Builder中添加:

private void showNotification(String title, String content, JsonObject json) { // ... 前置构建代码 // 添加“查看详情”按钮 Intent detailIntent = new Intent(this, DetailActivity.class); detailIntent.putExtra("payload", json.toString()); // 传递原始JSON PendingIntent detailPendingIntent = PendingIntent.getActivity(this, 0, detailIntent, PendingIntent.FLAG_IMMUTABLE); builder.addAction(R.drawable.ic_detail, "详情", detailPendingIntent); // 添加“关闭告警”按钮(需BroadcastReceiver) Intent dismissIntent = new Intent(this, NotificationReceiver.class); dismissIntent.setAction("DISMISS_ALARM"); dismissIntent.putExtra("topic", topic); PendingIntent dismissPendingIntent = PendingIntent.getBroadcast(this, 0, dismissIntent, PendingIntent.FLAG_IMMUTABLE); builder.addAction(R.drawable.ic_close, "关闭", dismissPendingIntent); // 发送通知 NotificationManagerCompat.from(this).notify((int) System.currentTimeMillis(), builder.build()); }

NotificationReceiver.java处理按钮点击:

public class NotificationReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if ("DISMISS_ALARM".equals(intent.getAction())) { String topic = intent.getStringExtra("topic"); Log.d("NOTIF", "用户关闭告警:" + topic); // 此处可发送MQTT指令到Broker,如发布"alarm/clear"主题 } } }

注意:NotificationReceiver需在AndroidManifest.xml中静态注册,否则Android 8.0+无法接收广播。

5.3 通知渠道分组与重要性分级:让运维人员一眼识别紧急等级

对多主题系统(如sensor/temperature、sensor/humidity、alarm/fire),应按业务重要性创建不同渠道。例如:

渠道ID名称重要性行为
mqtt_normal常规数据IMPORTANCE_DEFAULT仅状态栏图标
mqtt_warning告警信息IMPORTANCE_HIGH横幅+声音
mqtt_emergency紧急事件IMPORTANCE_MAX全屏弹窗+震动

在messageArrived()中根据topic前缀动态选择渠道:

String channelId; if (topic.startsWith("alarm/")) { channelId = "mqtt_emergency"; } else if (topic.startsWith("sensor/")) { channelId = "mqtt_normal"; } else { channelId = "mqtt_normal"; } builder.setChannelId(channelId);

这样,消防告警会以最高优先级强制打断用户,而温湿度数据仅安静落于状态栏——这才是工业级通知的设计逻辑。

我带过的某跨平台系统项目,最初所有消息走同一渠道,运维值班员在深夜被100条温湿度通知刷屏,漏掉了真正的火警。后来按主题分级后,告警响应时间从平均8分钟缩短到47秒。技术没有银弹,但把渠道、QoS、前台服务这三根线拧紧,消息就再也不会“已接收,但你看不见”。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询