简介:本资源是一份面向Android应用开发者与物联网初学者的MQTT消息推送实战示例,聚焦在移动终端实现轻量级远程通信的核心能力——连接MQTT服务器、订阅主题、接收消息并实时展示于系统通知栏。项目基于Eclipse Paho Android客户端库构建,已集成完整服务生命周期管理、断线自动重连、消息回调处理及NotificationCompat通知封装逻辑,可直接编译运行或作为模块嵌入现有工程。压缩包共607个文件,含134个flat资源、121个json配置、188个xml布局与清单文件、8个核心Java业务类及APK安装包等,结构清晰,便于理解Android后台服务与MQTT协议协同机制;整体大小为15.58MB。已有1142人学习下载,提供开箱即用的可运行代码框架、关键注释说明及典型错误处理范式,助开发者快速掌握物联网消息推送落地的关键链路。
1. Android端MQTT消息推送落地:从连接服务器、订阅主题到通知栏实时显示,为什么90%的初学者卡在权限配置和后台保活?
在某高校物联网课程设计中,A同学花三天时间跑通了MQTT服务器通信,却始终收不到通知栏提醒——不是代码没写,而是NotificationChannel未创建、ForegroundService未启动、AndroidManifest.xml里漏了<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />(Android 13+强制要求)。这并非个例:实际项目中,MQTT客户端能连上、能收包、但消息“静默消失”,根本原因往往不在协议栈,而在Android系统级行为约束。本文聚焦一个最小可运行闭环:用Eclipse Paho Android Client连接公开MQTT测试服务器(如broker.hivemq.com:1883),订阅指定主题(如test/android),收到消息后立即在通知栏弹出带图标、渠道、点击跳转Activity的完整通知。不依赖任何第三方推送SDK,纯原生实现,覆盖Android 8.0(Oreo)至14(UpsideDownCake)全版本适配要点。适合正在做毕业设计、IoT设备配套App或需要轻量级消息通道的开发者——你不需要懂MQTT协议细节,但必须清楚Android如何“允许”一条消息从网络层抵达用户眼前。
2. 环境准备与依赖接入:选Paho而非MQTTX SDK,因为它的轻量性与可控性更适配通知场景
2.1 为什么是Eclipse Paho Android Client?不是Mosquitto Java版,也不是自研Netty封装
MQTT客户端库在Android生态有三类主流选择:
- Mosquitto官方Java绑定:需JNI编译,APK体积膨胀明显,且对Android生命周期无感知;
- 自研基于OkHttp/Netty的MQTT over WebSocket:灵活性高,但重试逻辑、离线缓存、QoS 1/2状态管理极易出错;
- Eclipse Paho Android Client:专为Android优化,内置
MqttAndroidClient,自动处理主线程调用阻塞、后台服务绑定、广播接收器注册等胶水逻辑,且支持setCallback()回调直接在主线程更新UI——这对通知栏触发至关重要。
提示:Paho Android Client已停止维护(最后更新2021年),但其稳定性和API简洁性仍远超多数新库。我们使用
org.eclipse.paho:org.eclipse.paho.android.service:1.1.1,该版本兼容AndroidX且无反射报错风险。
2.2 Gradle依赖与混淆规则配置(含AndroidX迁移关键项)
在模块级build.gradle中添加:
dependencies { implementation 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.1' implementation 'androidx.core:core:1.12.0' // 必须≥1.10.0,否则NotificationCompat.Builder构造失败 implementation 'androidx.work:work-runtime-ktx:2.9.0' // 后台任务保活基础 }若启用ProGuard/R8混淆,必须保留Paho核心类(否则MqttAndroidClient初始化时抛ClassNotFoundException):
# Paho MQTT for Android -keep class org.eclipse.paho.client.mqttv3.** { *; } -keep class org.eclipse.paho.android.service.** { *; } -dontwarn org.eclipse.paho.client.mqttv3.**2.3 权限声明:从Android 6.0到14的渐进式适配清单
AndroidManifest.xml中需声明以下权限(按最低目标版本动态增减):
<!-- 基础网络 --> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- 通知栏必备(Android 8.0+强制渠道) --> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <!-- 后台保活(Android 9+限制前台服务,需显式声明) --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- (可选)WiFi状态监听,用于网络切换时重连 --> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />注意:
POST_NOTIFICATIONS权限在Android 13(API 33)起为运行时权限,必须在首次订阅前动态申请;而FOREGROUND_SERVICE是安装时权限,无需运行时请求,但必须在<service>标签中显式声明android:foregroundServiceType属性(见3.2节)。
3. 核心代码实现:从MQTT连接、主题订阅到通知构建的四步链路
3.1 初始化MqttAndroidClient并建立连接:用connectOptions控制超时与心跳
private MqttAndroidClient mqttClient; private String brokerUrl = "tcp://broker.hivemq.com:1883"; // 公共测试Broker,无认证 private String clientId = "android_" + System.currentTimeMillis(); // 唯一客户端ID private void initMqttClient() { mqttClient = new MqttAndroidClient(this.getApplicationContext(), brokerUrl, clientId); MqttConnectOptions options = new MqttConnectOptions(); options.setCleanSession(true); // 设为true:断开后丢弃服务器未投递消息;false则保留QoS1/2消息 options.setConnectionTimeout(30); // 单位秒,避免Connect阻塞过久 options.setKeepAliveInterval(60); // 心跳间隔,单位秒,低于30秒可能被某些Broker拒绝 try { IMqttToken token = mqttClient.connect(options); token.setActionCallback(new IMqttActionListener() { @Override public void onSuccess(IMqttToken asyncActionToken) { Log.d("MQTT", "Connected to broker"); subscribeToTopic("test/android"); // 连接成功后立即订阅 } @Override public void onFailure(IMqttToken asyncActionToken, Throwable exception) { Log.e("MQTT", "Connection failed", exception); // 此处应加入指数退避重连逻辑(见4.1节) } }); } catch (MqttException e) { Log.e("MQTT", "MqttException on connect", e); } }参数说明:
cleanSession=true是通知场景推荐值:用户关闭App后,不期望收到历史消息,避免通知堆积;connectionTimeout=30防止弱网下connect()卡死主线程;keepAliveInterval=60是HiveMQ公共Broker接受的最大值,设为120会返回CONNECTION_REFUSED。
3.2 创建前台服务承载MQTT长连接:解决Android 9+后台执行限制
Android 9(Pie)起禁止应用在后台启动Service,MQTT必须运行在前台服务中。新建MqttForegroundService.java:
public class MqttForegroundService extends Service { private static final int NOTIFICATION_ID = 1001; private MqttAndroidClient mqttClient; @Override public void onCreate() { super.onCreate(); createNotificationChannel(); // 必须先创建渠道,否则startForeground失败 startForeground(NOTIFICATION_ID, buildForegroundNotification()); } @Override public int onStartCommand(Intent intent, int flags, int startId) { // 初始化MQTT客户端(复用Application单例或此处new) initMqttClient(); return START_STICKY; // 系统内存不足杀掉后,会尝试重启 } private void initMqttClient() { mqttClient = new MqttAndroidClient(this, "tcp://broker.hivemq.com:1883", "svc_" + System.currentTimeMillis()); mqttClient.setCallback(new MqttCallbackExtended() { @Override public void connectComplete(boolean reconnect, String serverURI) { try { mqttClient.subscribe("test/android", 0); // QoS 0:最多一次,通知场景足够 } catch (MqttException e) { Log.e("MQTT-SVC", "Subscribe failed", e); } } @Override public void connectionLost(Throwable cause) { Log.w("MQTT-SVC", "Connection lost", cause); // 触发重连(见4.1节) } @Override public void messageArrived(String topic, MqttMessage message) throws Exception { String payload = new String(message.getPayload()); Log.d("MQTT-SVC", "Received: " + payload); showNotification(topic, payload); // 关键:收到即通知 } @Override public void deliveryComplete(IMqttDeliveryToken token) {} }); } private void showNotification(String topic, String content) { Intent intent = new Intent(this, MainActivity.class); intent.putExtra("topic", topic); intent.putExtra("content", content); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT); NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "mqtt_channel") .setSmallIcon(R.drawable.ic_notification) // 必须是alpha通道清晰的白底图标 .setContentTitle("MQTT消息") .setContentText(content) .setPriority(NotificationCompat.PRIORITY_HIGH) .setAutoCancel(true) .setContentIntent(pendingIntent) .setStyle(new NotificationCompat.DecoratedCustomViewStyle()); NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); notificationManager.notify(System.currentTimeMillis(), builder.build()); } 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("mqtt_channel", name, importance); channel.setDescription(description); channel.enableLights(true); channel.setLightColor(Color.RED); channel.enableVibration(true); NotificationManager notificationManager = getSystemService(NotificationManager.class); notificationManager.createNotificationChannel(channel); } } 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("正在监听主题:test/android") .setSmallIcon(R.drawable.ic_service) .setContentIntent(pendingIntent) .build(); } @Override public IBinder onBind(Intent intent) { return null; } }关键点说明:
startForeground()必须在onCreate()中调用,且NotificationChannel必须提前创建;PendingIntent.FLAG_IMMUTABLE是Android 12+强制要求,否则getActivity()抛异常;R.drawable.ic_notification图标尺寸必须为24×24 dp,且背景透明(系统会自动染色为白色);START_STICKY并非100%保证服务存活,需配合JobIntentService或WorkManager做兜底(见4.2节)。
3.3 在Activity中启动服务并处理通知点击:解耦UI与后台逻辑
在MainActivity.java的onCreate()中启动服务:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 检查并申请通知权限(Android 13+) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.POST_NOTIFICATIONS}, 100); } } // 启动MQTT前台服务 Intent serviceIntent = new Intent(this, MqttForegroundService.class); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { startForegroundService(serviceIntent); } else { startService(serviceIntent); } } // 处理通知栏点击传入的数据 @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); if (intent.hasExtra("content")) { String content = intent.getStringExtra("content"); Toast.makeText(this, "收到消息:" + content, Toast.LENGTH_LONG).show(); } }注意:
onNewIntent()仅在launchMode="singleTop"或"singleTask"时生效,务必在AndroidManifest.xml中为MainActivity设置:
<activity android:name=".MainActivity" android:launchMode="singleTop" <!-- 关键!否则点击通知无法触发onNewIntent --> ... />4. 避坑指南:Android MQTT通知场景最常踩的5个深坑及血泪修复方案
4.1 现象:MQTT连接成功,但messageArrived()从不触发
原因:
- 订阅主题时未指定QoS等级(
subscribe(topic, qos)第二个参数为0/1/2),Paho默认QoS=0,但部分Broker(如EMQX)要求显式传参; - 主题名含空格或特殊字符(如
test/ android),Broker拒绝订阅; - 客户端ID重复:同一
clientId在多个设备登录,后登录者踢掉前者,导致旧连接失效。
解决:
- 强制指定QoS:
mqttClient.subscribe("test/android", 0); - 主题名只用ASCII字母、数字、
/、_、-; clientId拼接设备唯一标识:"android_" + Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID)。
4.2 现象:通知栏无反应,Log显示NotificationManager: Ignoring notification
原因:
NotificationChannel创建时importance设为IMPORTANCE_NONE或IMPORTANCE_MIN;setSmallIcon()传入的资源ID不存在或不是drawable类型(误用mipmap);- Android 12+未设置
setPriority(),系统降级为低优先级。
解决:
- 渠道重要性必须≥
IMPORTANCE_DEFAULT(推荐IMPORTANCE_HIGH); - 图标资源放
res/drawable/下,命名如ic_notification.xml(VectorDrawable); - 显式调用
.setPriority(NotificationCompat.PRIORITY_HIGH)。
4.3 现象:App退到后台后10分钟内MQTT断连,且不再自动重连
原因:
- Android 9+限制后台应用的网络访问,前台服务被系统回收;
MqttCallbackExtended.connectionLost()中未实现重连逻辑,或重连未在主线程外执行(导致ANR)。
解决:
- 在
connectionLost()中启动HandlerThread执行重连:
new HandlerThread("MqttReconnect").start(); Handler reconnectHandler = new Handler(reconnectThread.getLooper()); reconnectHandler.post(() -> { try { mqttClient.connect(options); } catch (MqttException e) { /* 重试计数+1,指数退避 */ } });4.4 现象:点击通知后Activity黑屏或闪退
原因:
PendingIntent未设置FLAG_IMMUTABLE(Android 12+);MainActivity未设置android:exported="true"(Android 12+强制要求);Intent携带的content字符串含不可见控制字符(如\u202E),解析时报BadParcelableException。
解决:
PendingIntent.getActivity()必须加FLAG_IMMUTABLE;AndroidManifest.xml中<activity>标签添加android:exported="true";- 发送前对payload做
TextUtils.htmlEncode()过滤。
4.5 现象:Android 14设备上通知完全不显示,Log无错误
原因:
- Android 14(API 34)新增
NotificationManager.canNotify()检查,需在showNotification()前调用; NotificationChannel未设置setSound(),而用户系统设置中关闭了“无声通知”。
解决:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { if (!notificationManager.canNotify("mqtt_channel")) { // 引导用户去设置页开启通知 Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) .putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName()); startActivity(intent); return; } }5. 进阶技巧:让MQTT通知真正可靠——离线消息缓存、多主题分级与点击归因追踪
5.1 用Room数据库缓存离线消息,避免网络抖动丢失关键通知
当设备断网时,MQTT Broker会根据QoS等级缓存消息,但Android端若未及时处理messageArrived(),消息将丢失。解决方案:在messageArrived()中先存入本地数据库,再构建通知。
定义Entity:
@Entity(tableName = "mqtt_message_cache") public class MqttMessageCache { @PrimaryKey(autoGenerate = true) public long id; public String topic; public String payload; public long timestamp; public boolean isRead; }在MqttForegroundService.messageArrived()中:
@Override public void messageArrived(String topic, MqttMessage message) throws Exception { String payload = new String(message.getPayload()); MqttMessageCache cache = new MqttMessageCache(); cache.topic = topic; cache.payload = payload; cache.timestamp = System.currentTimeMillis(); cache.isRead = false; // 插入Room数据库(异步) new InsertMessageAsyncTask(mqttDatabase.messageDao()).execute(cache); // 同时发通知(不阻塞) showNotification(topic, payload); }优势:用户打开App时可查询
isRead=false的消息列表,实现“未读消息中心”;即使通知被清除,数据仍在。
5.2 多主题分级通知:用不同渠道区分告警、状态、调试消息
单一通知渠道易被用户关闭。按主题前缀动态创建渠道:
| 主题模式 | 渠道ID | 重要性 | 声音 |
|---|---|---|---|
alert/# | alert_channel | IMPORTANCE_HIGH | 自定义铃声 |
status/# | status_channel | IMPORTANCE_DEFAULT | 无声音 |
debug/# | debug_channel | IMPORTANCE_MIN | 仅震动 |
在showNotification()中:
String channelId; if (topic.startsWith("alert/")) { channelId = "alert_channel"; createChannelIfNotExists(channelId, "告警通知", NotificationManager.IMPORTANCE_HIGH, R.raw.alert_sound); } else if (topic.startsWith("status/")) { channelId = "status_channel"; createChannelIfNotExists(channelId, "状态通知", NotificationManager.IMPORTANCE_DEFAULT, 0); } else { channelId = "debug_channel"; createChannelIfNotExists(channelId, "调试通知", NotificationManager.IMPORTANCE_MIN, 0); }5.3 通知点击归因:记录每次点击的原始主题与消息ID,用于效果分析
在showNotification()构建PendingIntent时,注入唯一追踪ID:
long traceId = System.currentTimeMillis(); // 或UUID.randomUUID().getMostSignificantBits() Intent intent = new Intent(this, MainActivity.class); intent.putExtra("trace_id", traceId); intent.putExtra("topic", topic); intent.putExtra("content", content); // 记录到数据库(供后续分析点击率) MqttClickLog log = new MqttClickLog(); log.traceId = traceId; log.topic = topic; log.timestamp = System.currentTimeMillis(); new InsertClickLogAsyncTask(mqttDatabase.clickLogDao()).execute(log);这样,当用户点击通知进入App,即可在onNewIntent()中捕获trace_id,上报埋点:“用户对alert/fire主题的点击率为73%”。
我带过的三个模拟项目X都验证过:把MQTT通知拆成“连接-订阅-缓存-渠道-归因”五层,比堆砌SDK更可控。尤其在教育类、工控类轻量App中,Paho+前台服务+Room的组合,既满足功能,又规避了厂商推送SDK的审核风险和唤醒限制。现在我的习惯是:每次写MQTT逻辑,第一行先敲createNotificationChannel(),第二行写startForegroundService(),第三行才碰connect()——顺序错了,后面全是玄学。希望帮到你。
本文还有配套的精品资源,点击获取