【MQTT】基于 Android 设备接入物联网平台最佳实践

一、关于 MQTT

MQTT 是 OASIS 标准中面向物联网 (IoT) 的消息传递协议,它被设计成一种极其轻量级的发布/订阅消息传输方式,非常适合以小的代码占用和极低的网络带宽连接远程设备。如今,MQTT 已广泛应用于汽车、制造、电信、石油天然气等众多行业,是物联网消息传递标准。

网址:https://mqtt.org/

1.1 Eclipse Paho

Paho 项目旨在为机器对机器 (M2M) 和物联网 (IoT) 领域的新兴、现有及未来应用提供可靠的开源实现,这些实现基于开放且标准的即时通讯协议。Paho 充分考虑了设备连接固有的物理和成本限制,其目标包括有效实现设备和应用之间的解耦,旨在保持市场开放,并促进可扩展的 Web 和企业中间件及应用的快速发展。

Paho 包含用于嵌入式平台的MQTT发布/订阅客户端实现,以及社区确定的相应服务器支持。Paho Java Client 是一个用 Java 编写的 MQTT 客户端库,用于开发运行在 JVM 或其他 Java 兼容平台(例如 Android)上的应用程序。

网址:https://eclipse.dev/paho/

1.2 MQTT.fx

MQTT.fx® 5 是我们用于测试开发和生产环境中物联网路由的工具。它允许您连接到开发和/或生产代理,并在编写代码之前测试您的项目。由于无需编程即可运行这些测试用例,这不仅意味着可以显著减少您的手动工作量,而且还能显著提高软件质量。

MQTT.fx 是一款基于Eclipse Paho,使用Java语言编写的MQTT客户端工具。支持通过Topic订阅和发布消息,用来前期和物理云平台调试非常方便。

网址:https://www.softblade.de/

二、基于Android的MQTT网络架构

设计一个基于Android的MQTT网络架构,核心在于解耦健壮性 (断连重连)和易用性

我们将采用 单例模式 (Singleton) 结合 观察者模式 (Observer) 的设计。底层使用成熟的 Eclipse Paho Android Client 库,因为它提供了对Android Service的封装,能够更好地处理后台运行和心跳保活。

2.1 架构设计概览

  • MqttManager (Core) : 单例核心类,负责管理 MqttAndroidClient 实例,处理连接、断开、重连逻辑。
  • IMqttEventListener: 接口,用于业务层接收消息、连接状态变化。
  • TopicDispatcher: 内部逻辑,将收到的MQTT消息根据Topic分发给不同的监听器。
  • Config: 独立的配置参数,便于项目移植。

2.2 依赖准备 (build.gradle)

app/build.gradle 中添加 Paho MQTT 依赖(注意:Paho Android Service 需要添加 localbroadcastmanagersupport-v4 的相关支持,或者使用兼容版本):

groovy 复制代码
dependencies {
    implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5'
    implementation 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.1'
    // 如果是AndroidX项目,可能需要添加此依赖以支持Paho Service
    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
}

AndroidManifest.xml 中注册 Service:

xml 复制代码
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<application ...>
    <!-- MqttService -->
    <service android:name="org.eclipse.paho.android.service.MqttService" />
</application>

2.3 核心代码实现 (Java)

2.3.1 定义回调接口 (IMqttEventListener.java)

这个接口用于统一业务层接收消息和状态。

java 复制代码
package com.example.mqtt.core;

public interface IMqttEventListener {
    // 收到消息
    void onMessageArrived(String topic, String message);
    
    // 连接成功
    void onConnected();
    
    // 连接断开(包含异常断开)
    void onDisconnected(String reason);
}

2.3.2 MQTT 管理核心类 (MqttManager.java)

这是最关键的模块,封装了连接配置、自动重连、消息分发和线程切换(确保回调在主线程)。

java 复制代码
package com.example.mqtt.core;

import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import org.eclipse.paho.android.service.MqttAndroidClient;
import org.eclipse.paho.client.mqttv3.IMqttActionListener;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.IMqttToken;
import org.eclipse.paho.client.mqttv3.MqttCallbackExtended;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * MQTT 管理单例
 * 负责统一连接、断开、发布、订阅以及网络状态处理
 */
public class MqttManager {
    private static final String TAG = "MqttManager";
    private static MqttManager instance;
    private MqttAndroidClient client;
    private Context context;
    
    // 保存每个Topic对应的订阅列表(用于断线重连后自动重新订阅)
    private final List<String> subscribedTopics = new ArrayList<>();
    
    // 消息监听器列表(支持多处订阅)
    private final List<IMqttEventListener> eventListeners = new ArrayList<>();

    // 主线程Handler,用于将消息抛回UI线程
    private final Handler mainHandler = new Handler(Looper.getMainLooper());

    private MqttManager() {}

    public static MqttManager getInstance() {
        if (instance == null) {
            synchronized (MqttManager.class) {
                if (instance == null) {
                    instance = new MqttManager();
                }
            }
        }
        return instance;
    }

    /**
     * 初始化并连接
     * @param context 上下文
     * @param brokerUrl 服务器地址 (tcp://xxx:1883)
     * @param clientId 客户端ID
     * @param username 用户名 (可选)
     * @param password 密码 (可选)
     */
    public void init(Context context, String brokerUrl, String clientId, String username, String password) {
        this.context = context.getApplicationContext();
        
        if (client != null && client.isConnected()) {
            disconnect();
        }

        client = new MqttAndroidClient(this.context, brokerUrl, clientId);
        
        // 设置回调
        client.setCallback(new MqttCallbackExtended() {
            @Override
            public void connectComplete(boolean reconnect, String serverURI) {
                Log.d(TAG, "Connected! Reconnect=" + reconnect);
                if (reconnect) {
                    //如果是自动重连成功,需要重新订阅之前的Topic
                    reSubscribeAllTopics();
                }
                notifyConnected();
            }

            @Override
            public void connectionLost(Throwable cause) {
                Log.w(TAG, "Connection Lost", cause);
                notifyDisconnected(cause != null ? cause.getMessage() : "Unknown");
            }

            @Override
            public void messageArrived(String topic, MqttMessage message) throws Exception {
                String msgContent = new String(message.getPayload());
                Log.d(TAG, "Msg Arrived: [" + topic + "] " + msgContent);
                notifyMessageArrived(topic, msgContent);
            }

            @Override
            public void deliveryComplete(IMqttDeliveryToken token) {
                // 消息发送成功确认,如有需要可处理
            }
        });

        connect(username, password);
    }

    private void connect(String username, String password) {
        MqttConnectOptions options = new MqttConnectOptions();
        // 自动重连是Paho的关键特性,处理网络不稳定
        options.setAutomaticReconnect(true); 
        // 清除会话:false表示服务器会保留之前的订阅信息和未接收的消息(QoS>0)
        // 建议设为true,由客户端在connectComplete中手动控制订阅,逻辑更清晰
        options.setCleanSession(true); 
        options.setConnectionTimeout(30); // 超时时间
        options.setKeepAliveInterval(60); // 心跳间隔

        if (username != null && !username.isEmpty()) {
            options.setUserName(username);
            options.setPassword(password.toCharArray());
        }

        try {
            client.connect(options, null, new IMqttActionListener() {
                @Override
                public void onSuccess(IMqttToken asyncActionToken) {
                    // 首次连接成功由 connectComplete 回调处理
                    Log.d(TAG, "Connect Action Success");
                }

                @Override
                public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
                    Log.e(TAG, "Connect Failed", exception);
                    notifyDisconnected("Initial Connect Failed: " + exception.getMessage());
                }
            });
        } catch (MqttException e) {
            e.printStackTrace();
        }
    }

    /**
     * 订阅主题
     * @param topic 主题
     * @param qos 服务质量 (0, 1, 2)
     */
    public void subscribe(String topic, int qos) {
        if (client != null && client.isConnected()) {
            try {
                client.subscribe(topic, qos);
                if (!subscribedTopics.contains(topic)) {
                    subscribedTopics.add(topic);
                }
                Log.d(TAG, "Subscribed to: " + topic);
            } catch (MqttException e) {
                Log.e(TAG, "Subscribe Error", e);
            }
        } else {
            // 如果当前未连接,记录下来,等连接成功后自动订阅
            if (!subscribedTopics.contains(topic)) {
                subscribedTopics.add(topic);
            }
            Log.w(TAG, "Client not connected, topic added to pending list: " + topic);
        }
    }

    /**
     * 发布消息
     */
    public void publish(String topic, String msg, int qos, boolean retained) {
        if (client != null && client.isConnected()) {
            try {
                MqttMessage message = new MqttMessage();
                message.setPayload(msg.getBytes());
                message.setQos(qos);
                message.setRetained(retained);
                client.publish(topic, message);
                Log.d(TAG, "Published to " + topic + ": " + msg);
            } catch (MqttException e) {
                Log.e(TAG, "Publish Error", e);
            }
        } else {
            Log.e(TAG, "Cannot publish, client not connected.");
        }
    }

    /**
     * 断开连接
     */
    public void disconnect() {
        if (client != null) {
            try {
                client.disconnect();
                client = null;
                subscribedTopics.clear();
                Log.d(TAG, "Disconnected manually");
            } catch (MqttException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 注册全局监听器
     */
    public void registerListener(IMqttEventListener listener) {
        if (!eventListeners.contains(listener)) {
            eventListeners.add(listener);
        }
    }

    /**
     * 移除监听器
     */
    public void unregisterListener(IMqttEventListener listener) {
        eventListeners.remove(listener);
    }

    // --- 内部辅助方法 ---

    private void reSubscribeAllTopics() {
        for (String topic : subscribedTopics) {
            try {
                client.subscribe(topic, 1);
                Log.d(TAG, "Re-subscribed: " + topic);
            } catch (MqttException e) {
                Log.e(TAG, "Re-subscribe failed for " + topic, e);
            }
        }
    }

    private void notifyMessageArrived(String topic, String message) {
        mainHandler.post(() -> {
            for (IMqttEventListener listener : eventListeners) {
                listener.onMessageArrived(topic, message);
            }
        });
    }

    private void notifyConnected() {
        mainHandler.post(() -> {
            for (IMqttEventListener listener : eventListeners) {
                listener.onConnected();
            }
        });
    }

    private void notifyDisconnected(String reason) {
        mainHandler.post(() -> {
            for (IMqttEventListener listener : eventListeners) {
                listener.onDisconnected(reason);
            }
        });
    }
    
    public boolean isConnected() {
        return client != null && client.isConnected();
    }
}

2.4. 项目中的使用方法

你只需要在项目中按照以下步骤调用即可。

2.4.1 初始化 (通常在 Application 或 MainActivity)

java 复制代码
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        
        // 替换你的配置
        String brokerUrl = "tcp://broker.emqx.io:1883";
        String clientId = "Android_App_" + System.currentTimeMillis();
        
        // 初始化并连接
        MqttManager.getInstance().init(this, brokerUrl, clientId, null, null);
    }
}

2.4.2 订阅和接收消息 (在 Activity 或 ViewModel 中)

java 复制代码
public class MainActivity extends AppCompatActivity implements IMqttEventListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 1. 注册监听
        MqttManager.getInstance().registerListener(this);

        // 2. 订阅主题 (可以在连接成功回调里做,也可以直接调用,Manager内部做了缓存处理)
        MqttManager.getInstance().subscribe("device/sensor/data", 1);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 3. 必须移除监听,防止内存泄漏
        MqttManager.getInstance().unregisterListener(this);
    }

    // --- 接口回调实现 ---

    @Override
    public void onMessageArrived(String topic, String message) {
        // 这里已经是主线程,可以直接更新UI
        if ("device/sensor/data".equals(topic)) {
            // 处理具体的业务逻辑,例如解析JSON
            System.out.println("Received: " + message);
        }
    }

    @Override
    public void onConnected() {
        Toast.makeText(this, "MQTT Connected", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onDisconnected(String reason) {
        Toast.makeText(this, "MQTT Disconnected: " + reason, Toast.LENGTH_SHORT).show();
    }
    
    // --- 按钮点击发布消息 ---
    public void onSendClick(View view) {
        MqttManager.getInstance().publish("device/control", "{\"cmd\":\"ON\"}", 1, false);
    }
}

2.5 设计亮点与可维护性分析

  1. 自动重连机制 (setAutomaticReconnect):
  • 代码中配置了Paho自带的自动重连。当网络从断开变为连接(如4G切换WiFi,或进入电梯后出来),Paho Client 会尝试重连。
  • 关键点 : 我们使用了 MqttCallbackExtended。当重连成功时,会触发 connectComplete(boolean reconnect, ...)。代码中通过判断 reconnect 标志,自动执行 reSubscribeAllTopics(),解决了**"重连后丢失之前订阅Topic"**的常见痛点。
  1. 统一的订阅管理 (subscribedTopics List) :

    即使用户在网络未连接时调用了 subscribe,我们也会将其加入列表。一旦连接建立,或者重连成功,管理器会自动遍历列表进行实际订阅。业务层无需关心当前是否在线。

  2. 线程安全与UI交互 :

    MQTT的回调通常在后台线程。MqttManager 内部持有一个 Handler(Looper.getMainLooper()),确保分发给 IMqttEventListener 的事件都在主线程 执行。业务层更新UI时不需要再写 runOnUiThread

  3. 易扩展性 :

    如果未来需要解析复杂的JSON消息,可以在 MqttManagermessageArrived 中引入一个 MessageParser 接口,或者直接在业务层的 onMessageArrived 中通过 Topic 字符串进行 switch-case 分流处理。

  4. 生命周期安全 :

    使用 ApplicationContext 初始化Client,避免持有 Activity 的 Context 导致内存泄漏。提供 registerListenerunregisterListener,符合Android组件生命周期规范。

相关推荐
消失的旧时光-194318 分钟前
Android 面试高频:JSON 文件、大数据存储与断电安全(从原理到工程实践)
android·面试·json
dalancon1 小时前
VSYNC 信号流程分析 (Android 14)
android
dalancon1 小时前
VSYNC 信号完整流程2
android
dalancon1 小时前
SurfaceFlinger 上帧后 releaseBuffer 完整流程分析
android
用户69371750013842 小时前
不卷AI速度,我卷自己的从容——北京程序员手记
android·前端·人工智能
程序员Android3 小时前
Android 刷新一帧流程trace拆解
android
墨狂之逸才3 小时前
解决 Android/Gradle 编译报错:Comparison method violates its general contract!
android
阿明的小蝴蝶4 小时前
记一次Gradle环境的编译问题与解决
android·前端·gradle
汪海游龙4 小时前
开源项目 Trending AI 招募 Google Play 内测人员(12 名)
android·github
qq_283720055 小时前
MySQL技巧(四): EXPLAIN 关键参数详细解释
android·adb