资讯动态

Android MQTT消息不显示通知?心跳、QoS与前台服务三者对齐方案

发布时间:2026/10/15 6:10:35 来源:尧图企业网站定制
简介本资源是一份面向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日志显示已接收通知却像被黑洞吸走。最后发现是QoS0下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:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE / !-- Android 12 需显式声明前台服务类型 -- uses-permission android:nameandroid.permission.POST_NOTIFICATIONS / !-- Android 10 后台位置权限仅当需要时添加本例无需 --提示若MQTT服务器使用HTTP而非HTTPS如本地测试用mqtt://192.168.1.100:1883Android 9默认禁止明文流量。需在res/xml/network_security_config.xml中配置?xml version1.0 encodingutf-8? network-security-config domain-config domain includeSubdomainstrue192.168.1.100/domain trust-anchors certificates srcsystem / /trust-anchors cleartextTrafficPermittedtrue / /domain-config /network-security-config并在AndroidManifest.xml的application标签中引用android:networkSecurityConfigxml/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); } }参数说明keepAliveInterval60这是心跳包间隔。若设为10秒在家庭路由器NAT表老化时间通常30~60秒内无法维持长连接导致静默断连。实测中30≤keepAliveInterval≤120为安全区间。cleanSessiontrue通知类应用不需要离线消息堆积。设为false时Broker会保存未确认的QoS1消息但Android App进程被杀后无法恢复会话反而造成消息积压与重复投递。automaticReconnecttruePaho内部实现的指数退避重连初始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 { // QoS1确保消息至少送达一次通知类场景推荐 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的Contextprivate 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.0API 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 12API 31起startService()在后台调用被彻底禁止。MQTT客户端需运行在前台服务中否则系统会在App进入后台后10秒内终止服务。解决方案是将MQTT逻辑封装进Service并在onStartCommand()中调用startForeground()。首先在AndroidManifest.xml中声明服务service android:name.MQTTService android:enabledtrue android:exportedfalse android:foregroundServiceTypespecialUse / !-- 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默认QoS0但若Broker强制要求QoS1如某些企业级EMQX配置订阅虽成功消息却因QoS不匹配被静默丢弃。解决在subscribe()时显式指定QoS1并确认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:foregroundServiceTypespecialUse。系统将服务识别为普通后台服务并强杀。解决检查AndroidManifest.xml中权限与服务声明是否完整尤其android:foregroundServiceType属性不可省略。4.5 现象同一主题多条消息快速到达通知栏只显示最后一条且点击无响应原因setContentIntent()中PendingIntent未设置FLAG_IMMUTABLEAndroid 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.1private 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、前台服务这三根线拧紧消息就再也不会“已接收但你看不见”。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑