简介:面向物联网毕设与竞赛项目,围绕Android端通过OkHttp获取OneNET云平台数据的核心需求,提供一套可直接运行的Android Studio工程源码。典型场景是STM32等单片机采集传感器数据上传OneNET后,Android APP实时拉取并展示对应数据流,填补了这类闭环项目在移动端实现上教程较少的空白。资源包共1307个文件,其中含39个Java源码、82个XML布局与配置、120个JSON数据及配置、205个class与207个dex编译产物,另有可直接安装的APK文件,整体约12.35MB。工程从OneNET新建项目、创建数据流模板到Android Studio编码完整走通,并已在虚拟机上测试运行,可作为毕设答辩演示或参赛原型基础。目前已有1859人学习下载,适合具备基础Android编程能力、正为毕业设计或竞赛寻找物联网数据展示参考实现的同学,导入后可按模块拆解OkHttp网络请求与JSON数据解析全流程。
1. Android 获取 OneNET 数据:先想清楚数据在谁手里,再决定怎么取
第一次做 Android 获取 OneNET 数据这个需求时,很多人以为调一个接口就能拿到全部数据,实际上扎进去才发现:OneNET 只是数据的寄存处,设备才是源头,App 只是末端消费者。真正的问题不是「怎么调接口」,而是「设备到 OneNET、OneNET 到 App 这条链路怎么打通、选哪条通道」。做过设备配套 App、远程监控面板或运维工具的开发者都清楚,这里最费时间的往往不是请求本身,而是鉴权、数据流命名、轮询频率和断线重连这些边角料。本文把这些边角料一次讲透,给出能直接抄的 Android 侧接入代码和参数说明。
2. OneNET 的数据链路:产品、设备、数据流与两种取数方式
2.1 OneNET 的三层对象模型:产品、设备、数据流与数据点
OneNET 是一个面向物联网设备接入的云端平台,数据不是简单堆在一个大表里,而是按「产品 → 设备 → 数据流 → 数据点」四层组织。先理解这四层,后面写代码才不会被参数绕晕。
产品是最顶层的容器,相当于一个项目的集合。设备必须挂在一个产品下。产品在创建时会得到一个产品ID,这个 ID 在很多鉴权参数里都要用到。设备属于某个产品,每个设备有唯一的设备ID,设备接入平台后可以上报数据。数据流(datastream)挂在设备下面,一个设备可以有多个数据流,每个数据流有自己的名称,比如 temperature、humidity、switch_state。你拉数据时基本是按「设备ID + 数据流名」来定位的。数据点则是某个数据流在某个时刻产生的一条具体记录,包含时间 at 和值 value,这个值可以是整数、浮点数、字符串甚至 JSON 对象。
理解了这个模型,就明白 Android 侧要做的核心动作:在已知设备ID的前提下,查询这个设备某个数据流下的数据点列表。数据流名在 OneNET 控制台的设备详情页能看到,和代码里写的字符串必须完全一致,大小写、符号都对不上,否则返回空。
还有一些细节会影响你的实现。数据流名允许普通字符和中文,但拼进 URL 时中文必须编码。数据点的 value 类型在控制台能看到,取值和单位其实是设备端上报时定的,平台只负责存和转发,不去校验语义。所以你拿到 value 后当成什么类型解析,取决于你设备端上报时用了什么格式。常见的是浮点字符串,但也有设备直接把整个状态 JSON 压成一个字符串上报,这种情况下 value 字段里是一段 JSON 文本,需要二次解析。
2.2 两种取数方式:API 轮询拉取与 MQTT 订阅推送
Android 获取 OneNET 数据,方案上绕不开两条路:一条是 HTTP API 周期性拉取,一条是 MQTT 长连接订阅。两条路都能拿到数据,但特性差别很大,选错方向后面全是坑。
API 拉取的思路是 App 主动发起请求,问 OneNET「你那边有新的数据点吗」。常见做法是通过 REST 接口按设备ID和数据流名查询最新数据点,得到 JSON 后解析展示。这条路的优点是实现简单、调试直白,浏览器里把请求地址拼出来就能验证,出了错也容易定位;缺点是实时性完全取决于轮询频率,轮询太密会被平台限流,太疏数据刷新有延迟。
MQTT 订阅的思路正好反过来:App 和 OneNET 建立一条长连接,设备上报数据后,平台把消息推送给订阅了对应主题的客户端。App 被动接收,实时性能做到秒级甚至毫秒级,而且不需要频繁发请求,流量和电量消耗低很多。代价是链路变长、调试难度大,连接状态、心跳、断线重连、主题命名这些都要处理,代码量比 API 拉取多出一截。
下面用一张表把两条路的关键差异列出来,方便做选型对比。
| 对比项 | HTTP API 轮询 | MQTT 订阅 |
|---|---|---|
| 实时性 | 取决于轮询间隔,秒级到分钟级 | 消息到达即推送,秒级 |
| 流量消耗 | 每次请求都要带上鉴权和查询参数 | 长连接下只传增量消息,省流量 |
| 服务端压力 | 轮询越频繁压力越大,容易被限流 | 连接建立后推送开销较小 |
| 离线消息 | 拉取时始终取最新状态,天然可查历史 | 是否补收离线消息取决于 QoS 和 cleanSession |
| 开发复杂度 | 低,一个请求 + JSON 解析 | 高,涉及连接、心跳、重连、主题管理 |
| 适合场景 | 监控面板秒级刷新、查询历史数据 | 告警推送、实时曲线、状态变更 |
2.3 选型判断:先画清楚你的场景再选通道
我一般会先问自己一个问题:这个 App 的数据刷新频率到底是多少?如果只是打开页面时看一下当前温湿度、或者每 30 秒刷一次大屏,API 轮询足够,没必要上 MQTT。反过来,如果是设备异常告警、断电上报这类需要第一时间让用户看到的消息,走 API 轮询会非常被动——你根本不知道设备什么时候上报,只能靠短轮询硬扛,很容易撞上限流。
常见的折中方案是双通道:实时性要求高的数据走 MQTT,普通展示数据和历史趋势走 API。这个思路后面第 6 章会展开讲,它几乎是生产环境里最稳的取数姿势。
从协议本身看,OneNET 的设备侧大量使用 MQTT 接入,平台的消息推送能力本身就围绕 MQTT 设计得很完整。你如果只做 App 展示端,用 API 拉取虽然在功能上能满足,但要接受轮询存在数据延迟;如果想要的是「设备一上报,App 马上就变」,那 MQTT 是唯一合理选择,没有替代方案。
选型的另一个考量是团队的维护成本。API 拉取的代码任何 Android 开发者都能看懂,MQTT 则要求团队理解消息质量和会话概念。如果你的项目是短期 Demo 或者内部工具,我建议先 API 拉取跑通全链路再考虑上 MQTT;如果是面向最终用户的产品,一开始就要把 MQTT 的坑排掉,后续才能睡得着觉。
3. 用 HTTP API 拉取 OneNET 数据:token 鉴权与最小实现
3.1 鉴权思路:为什么 OneNET 不用明文 APIKey
OneNET 的老版 API 里,鉴权方式是每个请求在 Header 里带一个 token。token 不是固定的字符串,而是一段带签名的动态凭证。这么设计的原因很直接:APIKey 是账号级别的密钥,如果直接放进请求里,一旦被抓包泄露,别人就能用你的身份随意读设备数据。token 方案把密钥留在本地参与签名计算,网络上传的只是签名结果,并且可以设置过期时间,泄露出去了影响也有限。
token 的生成规则并不复杂,核心是把几个参数按固定顺序拼成一个字符串,用 HmacSHA1 算法和 APIKey 一起算出签名,再把签名拼回去。需要参与拼接的参数有四个:version 是平台固定的版本标记;res 是资源标识,代表这个 token 能访问哪一级资源;et 是 token 过期时间,用 Unix 秒级时间戳表示;method 是签名算法名称,这里固定是 sha1。
一个容易踩的地方是 res 的写法。很多开发者误以为 res 应该写设备ID,实际常见做法是写成 products/产品ID 这种产品维度,具体以你控制台里接入文档给的资源标识前缀为准。如果 res 写错,token 算出来也能生成,但平台去校验资源归属时会直接拒绝,返回 401。
et 参数同样值得注意。它代表的是过期时刻,不是有效时长。所以设置时要去掉当前时间,再往后推一段有效期,比如当前时间戳加 600 秒。如果直接把当前时间戳塞进去,等请求到达服务器时就已经过期了,这在新手阶段是非常常见的翻车点。
3.2 生成 token 的 Java 代码:照着写就能跑
下面这段代码是 token 生成的核心逻辑,可以直接放到工具类里。它做的事情是:把固定参数拼成一个字符串,用 APIKey 做 HMAC-SHA1 签名,Base64 编码后拼回 token 字符串。
import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Base64; public class OneNetToken { public static String buildToken(String accessKey, String res, long etSeconds) throws Exception { String version = "20180630"; String method = "sha1"; // 参与签名的字符串,顺序不能乱 String plain = "version=" + version + "&res=" + res + "&et=" + etSeconds + "&method=" + method; // HMAC-SHA1 计算签名,密钥就是产品下的 APIKey Mac mac = Mac.getInstance("HmacSHA1"); SecretKeySpec spec = new SecretKeySpec( accessKey.getBytes(StandardCharsets.UTF_8), "HmacSHA1"); mac.init(spec); byte[] rawSign = mac.doFinal(plain.getBytes(StandardCharsets.UTF_8)); // Base64 编码后做 URL 编码,防止 + 号被解析成空格 String sign = Base64.getEncoder().encodeToString(rawSign); sign = URLEncoder.encode(sign, "UTF-8"); return plain + "&sign=" + sign; } }逻辑说明:plain 字符串是参与签名的原文,参数顺序变化会导致签名结果完全不同,所以必须严格按照 version、res、et、method 的顺序拼。accessKey 是 OneNET 控制台里产品详情页的 APIKey,每个产品一个,长度通常在 32 位左右。Base64 编码后的 sign 里可能包含 +、/、= 这些字符,放进 HTTP Header 时 + 会被解析为空格导致鉴权失败,所以这里做了一层 URL 编码。
参数说明:res 一般写 products/产品ID,具体前缀以控制台给出的资源标识为准;et 是过期时间,建议用 System.currentTimeMillis() / 1000 + 600,有效期十分钟足够一次请求使用;accessKey 属于敏感信息,不要硬编码在代码里,我一般是放到本地配置文件或者通过构建参数注入,至少不要写进能被反编译看到的明文常量。
3.3 拉取数据点的请求与参数说明
拿到 token 之后,就可以发请求查数据了。常见做法是用 HttpURLConnection 或者 OkHttp,把 token 放进 Header 的 Authorization 字段,然后在 URL 里指定设备ID、数据流名、时间范围和条数。
String deviceId = "你的设备ID"; String datastreamId = "temperature"; long end = System.currentTimeMillis(); long start = end - 60 * 60 * 1000L; String token = OneNetToken.buildToken(API_KEY, "products/" + PRODUCT_ID, end / 1000 + 600); String url = "https://api.heclouds.com/devices/" + deviceId + "/datapoints" + "?datastream_id=" + datastreamId + "&start=" + start + "&end=" + end + "&limit=20"; URL httpUrl = new URL(url); HttpURLConnection conn = (HttpURLConnection) httpUrl.openConnection(); conn.setRequestMethod("GET"); conn.setRequestProperty("Authorization", token); conn.setConnectTimeout(8000); conn.setReadTimeout(8000); int code = conn.getResponseCode(); if (code == 200) { // 读取响应体,拿到 JSON }这里有几个参数要解释清楚。datastream_id 如果没有特殊字符,直接拼 URL 没问题;如果数据流名是中文或者带点号,必须先用 URLEncoder.encode 编码再拼进去,否则服务端解析不到。start 和 end 是查询时间范围,单位是毫秒,这是我常用的平台版本;如果你的平台版本要求秒级,就除以 1000,具体以你控制台的 API 文档为准。limit 控制返回的数据点条数,一般查最新数据给 1 到 20 就够。
这段代码里用的是 HttpURLConnection,不引第三方依赖,跑通方便。正式项目建议换成 OkHttp,连接池、超时和日志拦截都更好用。另外注意网络请求不能放在主线程,Android 要求网络操作必须在子线程执行,后面解析完还要切回主线程更新 UI。
实际请求地址里的 api.heclouds.com 是老版本平台的公共域名,新建产品的控制台可能会给出不同的 REST API 地址,以上面讨论的「控制台设备详情里查看」为准。接口路径和参数名也可能随平台版本调整,先用控制台自带的 API 调试工具发出一次成功请求,再把它生成的参数搬到 Android 端,是效率最高的做法。
3.4 解析返回值与 UI 刷新
OneNET 的 API 返回格式通常是外层包一层 errno 和 error,数据本体放在 data 字段里。data 里再按数据流分组,每个数据点包含时间戳 at 和值 value。解析时先判断 errno 是不是 0,不是 0 说明这次请求出了问题,不要继续往下解析。
JSONObject root = new JSONObject(json); int errno = root.getInt("errno"); if (errno != 0) { Log.e("OneNet", root.getString("error")); return; } JSONObject data = root.getJSONObject("data"); JSONArray points = data.getJSONArray("datapoints"); List<Float> values = new ArrayList<>(); for (int i = 0; i < points.length(); i++) { JSONObject p = points.getJSONObject(i); float value = (float) p.optDouble("value", 0); values.add(value); } // 切回主线程刷新 UI runOnUiThread(() -> { textView.setText("温度:" + values.get(0) + " ℃"); });解析逻辑不复杂,但有两个边界要处理。一是 data 可能是 null,当指定数据流在查询时间范围内没有数据时,接口会返回 data 为 null 而不是空数组,直接 getJSONObject 会抛异常。正确做法是先 isNull("data") 判断。二是 value 可能是字符串而不是数字,如果设备上报的是状态值,比如 "01" 代表开 "00" 代表关,解析时要用 optString 而不是 optDouble。
UI 刷新必须回到主线程,这一点不用多说。runOnUiThread 是 Activity 自带的方法,如果取数逻辑放在非 Activity 的类里,就用 Handler 或者 LiveData 的 setValue 来传值。解析 JSON 的代码建议 try catch 包一层,因为第三方 SDK 或者设备上报的脏数据随时可能让解析崩掉,宁可跳过一条坏数据也不让整个 App 闪退。
4. 用 MQTT 订阅 OneNET 数据:长连接与实时推送实现
4.1 MQTT 接入参数:地址、端口、clientId 与接入凭证
如果对实时性有要求,MQTT 是绕不开的方案。Android 端做 MQTT 客户端,最成熟的做法是用 Eclipse Paho 库,稳定性和社区资料都够用。接入前先理清四个参数:broker 地址、端口、clientId 和接入凭证。
broker 地址是 OneNET 提供的 MQTT 接入点,通常以 tcp:// 开头,后面跟域名和端口。不同地域、不同产品类型的接入地址可能不一样,我习惯在控制台的接入文档页里直接复制官方调试工具显示的地址,而不是抄网上的旧文章。网上能搜到一些固定 IP 加端口的写法,那些往往来自个人项目记录,平台升级后可能失效。
clientId 是设备接入 MQTT 时的客户端标识,同一个 clientId 同时建立连接会把旧的连接踢掉,这是 MQTT 协议的行为。接入凭证一般由 OneNET 的产品ID、设备ID和 token 共同组成。常见配置是把产品ID作为用户名,token 作为密码,但不同平台版本的设置方式有差异,同样以控制台给出的接入参数对照表为准。
还有两个和稳定性直接相关的参数要提前理解。cleanSession 决定会话是否保留,设为 false 时 broker 会记住订阅关系,离线期间的消息在 QoS 允许下会补发;设为 true 则每次连接都是全新会话,离线消息直接丢。keepAliveInterval 是心跳间隔,客户端在空闲时按这个周期发送心跳包,超出预期没收到心跳,平台会判定连接失效。AP 的移动网络环境复杂,这两个参数务必按生产环境去设。
4.2 连接与订阅的最小代码
用 Paho 写一个最小可用的连接和订阅流程,依赖加入工程后,核心代码大概是这样:
import org.eclipse.paho.client.mqttv3.*; import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; String broker = "tcp://你的MQTT接入地址"; String clientId = "你的设备ID"; String topic = "dp/post/json/你的设备ID"; // 以接入文档实际主题为准 MqttClient client = new MqttClient(broker, clientId, new MemoryPersistence()); MqttConnectOptions opts = new MqttConnectOptions(); opts.setCleanSession(false); opts.setConnectionTimeout(15); opts.setKeepAliveInterval(60); opts.setUserName("你的产品ID"); opts.setPassword("你的接入token".toCharArray()); opts.setAutomaticReconnect(true); client.setCallback(new MqttCallbackExtended() { @Override public void connectComplete(boolean reconnect, String serverURI) { // 重连成功后恢复订阅 if (reconnect) { client.subscribe(topic, 1); } } @Override public void connectionLost(Throwable cause) { // Paho 自动重连开启后,这里只需要记录日志 } @Override public void messageArrived(String topic, MqttMessage message) throws Exception { String payload = new String(message.getPayload(), "UTF-8"); } }); client.connect(opts); client.subscribe(topic, 1);参数说明:setAutomaticReconnect(true) 是 Paho 内置的自动重连开关,但重连成功后订阅关系需要手动恢复,所以要在 connectComplete 里根据 reconnect 参数重新 subscribe。setCleanSession(false) 配合 QoS 1 可以保证大部分离线消息不丢,但极端情况下 broker 端队列会积累,需要注意订阅主题的消息量。
topic 是按 OneNET 接入文档填的,不同版本的主题前缀写法有差异。我见过的最常见格式是 dp/post/json 开头,后面接设备ID,但这不是所有产品都适用。最简单的验证方式是先在控制台用调试工具订阅一个通配层级的主题,看设备上报时消息出现在哪个 topic,再把那个真实 topic 填进代码。
4.3 消息回调里的数据解析与线程切换
MQTT 回调里的 messageArrived 是在 Paho 的网络线程里执行的,不是主线程。如果在这个方法里直接操作 TextView 或者弹 Toast,大概率会直接闪退。正确做法是先把消息解析成内存对象,再通过主线程去更新 UI。
一个比较干净的写法是把解析和 UI 更新分开。messageArrived 里只负责把 payload 转成 JSON 并抽取出关心的字段,存入一个全局变量或者内存缓存;然后通过 runOnUiThread 或者 LiveData 把数据发到主线程。
@Override public void messageArrived(String topic, MqttMessage message) throws Exception { String payload = new String(message.getPayload(), "UTF-8"); try { JSONObject root = new JSONObject(payload); float temperature = (float) root.optDouble("temperature", 0); long at = root.optLong("at", System.currentTimeMillis()); // 保存最近一次数据,供 API 兜底时使用 latestValue = temperature; latestTime = at; runOnUiThread(() -> { temperatureView.setText("实时温度:" + temperature + " ℃"); }); } catch (Exception e) { Log.e("OneNetMQTT", "解析失败:" + payload, e); } }这里的关键点是把 JSON 解析放在 try catch 里。设备侧上报的数据偶尔会出现字段缺失、类型不符、编码错乱,如果不包 try catch,一条脏消息就能让网络线程异常终止,消息回调断掉后整个订阅都不工作了,而且日志看起来像是「连接正常但就是收不到数据」,排查起来非常绕。
线程切换还有一个细节:如果连续多条消息在短时间内到达,runOnUiThread 加的 Runnable 会在主线程排队执行,UI 压力大时会有明显延迟。如果 App 里有图表在动态刷新,建议把收到的原始数据放入队列,由 ViewModel 或自定义控件统一消费,而不是每条消息都直接触发一次 UI 刷新,否则掉帧很难看。
5. 接入路上的五个坑:现象、原因与解决
5.1 Header 带了 token 还是 401/403
现象:请求发出后,服务端返回 401 或 403,日志里看到 Authorization 字段确实带上了 token。
原因:最常见的是 res 参数写错。有人把设备ID当成 res,实际应该用 products/产品ID 这类产品维度资源标识;其次是 et 过期时间用的是当前时间戳,token 刚到服务端就已经过期;还有一种是 sign 的 Base64 结果里有加号,放进 HTTP Header 时被解析掉了,导致签名不匹配。
解决:先从控制台复制一个平台生成的示例 token,和自己生成的 token 逐段比对,确认格式一致;把 et 改成当前时间加 600 秒;在代码里对 Base64 结果做 URL 编码,或者反过来,如果你已经做了编码还是 401,那就去掉编码再试,少数平台两次编码反而过不了。
5.2 平台明明有数据,接口返回空数组
现象:在 OneNET 控制台能看到设备上报的数据点,但 Android 端请求 datapoints 返回 data 为 null,找遍参数都没发现问题。
原因:数据流名不匹配是最常见的。控制台里显示的数据流名带空格、中文或者大小写差异,代码里原样拼进去后 URL 编码没做;也可能查的时间范围和实际数据点时间对不上,比如设备一直在报,但代码里 start 到 end 只查了最近 10 分钟,数据其实是 1 小时前的。
解决:先去掉 datastream_id 参数,拉一次设备全部数据流,确认真实的数据流名;把时间范围放宽到最近 24 小时再试,如果数据能出来,说明是时间范围的问题;再逐步收紧时间。数据流名包含非英文字符时,用 URLEncoder.encode 编码后拼接,不要直接字符串拼接。
5.3 轮询太频繁,接口被限流
现象:App 每 2 秒轮询一次温度,跑了半小时后请求开始超时,偶尔返回平台错误码,还伴随一些连接被重置的异常。
原因:平台对单个产品的 API 调用频率做了限制,短轮询很快就触顶。这是很多初做监控面板的人会踩的坑,因为本地测试时接口响应很快,完全感受不到限流的存在,真机长期运行才暴露。
解决:把轮询间隔拉到 10 秒以上,并且只在页面处于前台时轮询,onPause 里停掉定时器,onResume 里拉一次最新数据然后恢复定时。也可以加本地缓存,请求失败时用上一次成功的数据撑住 UI,避免页面一闪一闪。如果业务确实需要秒级刷新,直接切换到 MQTT,轮询这条路在频率上走不通。
5.4 MQTT 断线后不重连,或者重连就风暴
现象:App 切后台放了一会儿,再切回来发现 MQTT 收不到消息;或者网络在 WiFi 和移动数据之间切换后,日志里出现反复 connect 和 disconnect,甚至把服务端都踢下线了。
原因:前者是连接断了但 App 没有触发重连,Paho 的自动重连在某些异常断线场景下不会自动恢复;后者是 connectionLost 回调里写了 connect 逻辑,断线瞬间触发多次回调,多个线程同时尝试建立连接,互相把对方的会话踢掉。
解决:不要依赖 Paho 唯一的自动重连开关,在 connectComplete 里判断 reconnect 参数,为 true 时统一重新订阅所有主题。重连间隔用指数退避,第一次 2 秒、第二次 4 秒,上限 60 秒,避免断线恢复瞬间产生重连风暴。另外监听 Android 系统的网络变化广播,网络恢复时主动检查连接状态,比被动等心跳超时更及时。
5.5 MQTT 回调里直接操作 UI 崩了
现象:messageArrived 回调里弹了一个 Toast,App 闪退,错误日志指向某个 View 相关的方法。
原因:MQTT 回调运行在 Paho 的独立线程,不是 Android 主线程。Android 不允许非主线程直接修改 UI,一旦碰了 TextView、Toast、Dialog,立刻抛异常。
解决:所有 UI 更新统一回到主线程。用 runOnUiThread 包一层,或者在回调里把数据存进 ViewModel,通过 LiveData 的 setValue 推到主线程观察者。解析数据的逻辑保持在回调线程里做,只有真正的 UI 赋值动作切主线程。记住一个原则:网络线程里只解析数据、不碰界面,UI 线程里只赋值、不解析。
6. 进阶:轮询与订阅混合的取数架构,别只信一个通道
选定了 MQTT 之后,我建议别急着删掉 API 拉取的代码。生产环境里单纯依赖 MQTT 推送会遇到一个隐蔽问题:客户端在后台被系统杀死再复活,期间设备上报的消息很可能收不到,即使 cleanSession 设了 false,broker 的离线队列也不是无限保留的,消息一样会丢。这时候 API 拉取就不是一个备用方案,而是必要的数据补齐通道。
常见的混合架构是这样的:MQTT 负责实时推送,App 在收到消息时更新 UI 并记录最新数据的时间戳;同时一个低频定时器每隔 30 到 60 秒调一次 API,拉取最近几条数据点,拿它和本地缓存做比对,发现 MQTT 漏掉的数据就补上。这样平时流量消耗很小,数据也不会因为断线出现黑洞。
// 每次 MQTT 收到实时数据时更新这个时间 private long lastMqttDataTime = 0; private float cachedTemperature = 0; // 低频 API 兜底任务,每 30 秒执行一次 Runnable validateTask = new Runnable() { @Override public void run() { long now = System.currentTimeMillis(); if (now - lastMqttDataTime > 30_000) { // 说明最近 30 秒没有实时推送,主动拉一次补齐 fetchLatestFromApi(); } handler.postDelayed(this, 30_000); } };这个设计的价值在于两条通道互相兜底。MQTT 挂了,API 补位;API 被限流,MQTT 依然能撑住实时界面。两个通道的优先级也很清晰:实时数据以 MQTT 为准,历史回补和首次进入页面时的冷启动数据以 API 为准。
回到开头的问题,Android 获取 OneNET 数据这件事,本质上不是写一个请求,而是设计一条可靠的数据通道。通道选错,后面所有努力都在补救;通道选对,后续只需要把两端参数维护好。我做这个方向时有一个血泪习惯,至今还在沿用:所有取数逻辑收敛到一个 Repository 类里,Activity 只拿回调,不直接碰 HTTP 或 MQTT 的细节。第一次把请求代码散落在页面里,后来切数据源、加缓存、加兜底,改得焦头烂额。如果你也在做这个方向,认清 API 是冷通道、MQTT 是热通道,两者搭着用,基本能稳住大部分设备场景。希望帮到你。
本文还有配套的精品资源,点击获取