OpenHarmony上Flutter实现SIM卡管理与流量统计实战 做 OpenHarmony 上这套 Flutter 应用的时候我第一个想到的问题不是界面怎么写而是流量数据从哪里来、SIM 卡状态怎么拿到。项目名字叫“移动数据使用监管助手”听起来范围挺大真正落地下来就是两件事把数据流量使用情况管清楚把 SIM 卡状态管明白。我一直觉得这类工具型 App 最怕做成“表面功能齐全、实际数据全错”所以 SIM 卡管理这一层就是我优先啃的硬骨头。做之前我简单列了一下需求边界发现它其实不只是“显示一下还剩多少流量”这么简单。双卡用户需要知道哪张卡正在跑流量、哪张卡处于漫游状态、默认数据卡是哪一张企业或者家庭场景还可能需要按周期统计每张卡的用量、设置提醒阈值。这些能力在 OpenHarmony 上并不是 Flutter 一个框架就能全部搞定的需要走原生接口通过插件桥接拿到底层数据再统一交给 Flutter 层做 UI 展示和业务逻辑。这篇实战记录就以 SIM 卡管理为主线讲讲我从环境搭建、原生桥接、数据读取到界面联动的完整方案也把一些踩过的坑一并分享出来。1. 项目思路与整体方案选型1.1 核心需求拆解OpenHarmony 上的移动数据监管 App 和 Android/iOS 上的同类应用相比有一点本质差异底层接口和权限模型不完全一样。所以第一步不是写界面而是把需求拆成“平台无关层”和“平台相关层”。我从项目中抽出三个最核心的功能SIM 卡信息展示卡槽数量、运营商名称、ICCID、手机号、SIM 卡状态、网络制式。数据流量统计当前周期已用流量、总流量、日均用量、按卡区分的数据消耗。SIM 卡管理操作设置默认数据卡、切换双卡、启停某张卡的数据服务。这三个功能里界面部分全部由 Flutter 负责数据源则需要通过 OpenHarmony 原生能力获取。我在最初设计的时候就定了原则Flutter 层不直接感知任何 OpenHarmony 专属 API所有底层调用都收敛到一个自定义插件里向上暴露一组干净的异步方法只要返回的数据结构稳定后面换设备、换系统版本都只改插件内部逻辑。1.2 为什么选择 Flutter 来做 OpenHarmony 应用可能有人会问OpenHarmony 有自己的应用开发框架直接用 ArkTS 写不就行了为什么还要引入 Flutter这个问题我在项目评审时就被问过一次。实际原因有三个。第一是团队现有技术栈以 Flutter 为主一套代码同时要维护 OpenHarmony 和 Android 两个版本如果 OpenHarmony 单独用 ArkTS 重写那 UI 层就是双倍工作量。第二是 Flutter 在复杂列表、图表展示上已经有成熟的生态流量报表要用到饼图、柱状图直接引第三方库比在原生侧手写图表高效得多。第三是 OpenHarmony 本身已经支持 Flutter 引擎的移植社区里也有持续迭代的适配版本只要接入方式正确跑起来性能和稳定性都能达到可用级别。当然这并不意味着 Flutter 能包打一切。SIM 卡状态、流量统计这类能力必须通过系统接口读取Flutter 应用跑在应用沙箱里拿不到这些私有数据。于是这套方案的核心就变成了“Flutter 负责业务原生负责系统能力中间用方法通道通信”。这也是整个项目架构上最需要花心思的地方。2. 环境准备与原生桥接设计2.1 工程接入的两种姿势Flutter 接入 OpenHarmony目前主流做法有两种一种是直接把 Flutter Module 集成到 OpenHarmony 工程里另一种是把 Flutter 工程作为主工程OpenHarmony 作为插件工程。我这个项目用的是后者因为我们要做的是一款独立 App不是某个已有系统应用里的一个模块主工程保持 Flutter 形态更有利于后续多端复用。实际工程结构大概这样flutter_app/Flutter 业务工程负责所有 UI 和业务逻辑。ohos_plugin/OpenHarmony 插件工程负责 SIM 卡、流量统计等原生能力。App 入口通过 FlutterViewControllerArkTS 侧对应的容器加载 Flutter 页面。在 OpenHarmony 侧插件需要声明为一个 Harmony 模块并通过 DevEco Studio 构建出 .har 包再集成到 Flutter 工程的 runner 里。刚开始接触这套流程的人容易忽略一点Flutter 引擎的 OpenHarmony 版本和 OpenHarmony SDK 版本要匹配不能随便拿一个 Flutter 版本对接某一个 OpenHarmony 版本否则编译过了运行时也会出现奇怪的崩溃。建议先用官方适配仓库里明确标注“可运行”的版本组合我这边使用的是 Flutter 3.x 系列加 OpenHarmony 4.x 标准系统的组合整个开发过程中没有遇到引擎层阻塞。2.2 方法通道的封装思路原生能力向 Flutter 暴露的方式我选择了 MethodChannel没有用 EventChannel原因是 SIM 卡和流量数据更适合“主动拉取”而不是“长连接推送”。比如用户打开页面时需要立即拿到当前的卡槽信息用方法通道一次调用就能返回结果逻辑简单出错也好排查。Dart 侧封装一个平台通道工具类import package:flutter/services.dart; class SimManager { static const MethodChannel _channel MethodChannel(com.example.sim_plugin); static FutureListMapString, dynamic getSimSlotList() async { final Listdynamic result await _channel.invokeMethod(getSimSlotList); return result.map((e) MapString, dynamic.from(e as Map)).toList(); } static FutureMapString, dynamic getFlowUsage(String iccid) async { final Mapdynamic, dynamic result await _channel.invokeMethod(getFlowUsage, {iccid: iccid}); return MapString, dynamic.from(result); } }原生侧在 OpenHarmony 插件里注册同一个 channel 名称实现对应方法返回给 Flutter 的数据统一用 JSON-compatible 的 Map 结构。这里值得强调一个实测经验不要直接返回自定义对象也不要在 MethodChannel 里传大对象最好在原生侧把数据转成普通字典。Flutter 和 ArkTS 之间消息传递是有序列化开销的简单类型传起来最稳遇到数据量大的场景就分段返回或加上缓存。3. SIM 卡状态与流量统计实现3.1 读取 SIM 卡信息SIM 卡信息读取是整个应用的地基。卡槽数量、运营商名称、ICCID、手机号这些数据在 OpenHarmony 上需要通过 telephony 子系统提供的接口获取。我实现的逻辑是遍历所有卡槽位逐个读取卡状态并过滤掉“卡槽为空”的情况。ArkTS 原生侧的关键步骤大概是获取 SIM 卡管理实例。遍历卡槽索引0、1调用获取 SIM 卡信息的接口。判断返回值中的卡状态字段只有已激活的卡才返回给上层。同时读取网络注册状态确认当前卡是否处于服务状态。代码示意如下非完整实现重点看调用流程import { sim } from ohos.telephony.sim; let slots: Arraysim.SimSlotInfo []; let resultList: ArrayObject []; for (let slotId 0; slotId 1; slotId) { let info sim.getSimSlotInfo(slotId); if (info info.simState sim.SimState.SIM_STATE_READY) { let iccid sim.getICCID(slotId); let carrierName sim.getSimOperatorName(slotId); let phoneNumber sim.getSimTelephoneNumber(slotId); resultList.push({ slotId: slotId, iccid: iccid, carrierName: carrierName, phoneNumber: phoneNumber, }); } }读取手机号这个动作要特别注意。很多 SIM 卡并不会把完整号码写入卡内读出来的可能是空字符串或者只有运营商自定义的短号。所以界面上要提前做兼容显示“未知号码”而不是把空 UI 抛给用户。我在项目里还会把 ICCID 作为每张卡的唯一标识因为手机号可能为空而 ICCID 只要 SIM 卡存在就一定有值。3.2 流量数据的获取与缓存策略流量统计这块我调研了 OpenHarmony 提供的网络数据统计接口发现它能拿到不同网络类型Wi-Fi、蜂窝数据的流量增量。使用思路是记录两个时间点的累计值然后做差值计算。如果是统计今日流量可以在当天零点记录一个基准值之后每次读取当前值减去基准值。如果是统计当前计费周期就要结合运营商结算日来设定统计周期。这个结算日不能硬编码我做成用户可配置项默认以自然月为周期。原生侧获取流量的核心代码如下import { dataFlowStatistics } from ohos.telephony.dataFlowStatistics; let total await dataFlowStatistics.getCellularDataTotalBytes(); let todayStart dateUtil.getZeroTimestamp(); let startBytes await dataFlowStatistics.getCellularDataBytes(todayStart); let currentBytes await dataFlowStatistics.getCellularDataBytes(Date.now()); let dailyUsed currentBytes - startBytes;需要注意这个接口返回的是字节数换算成 GB、MB 时要用浮点数运算并且保留两位小数。我遇到过一个问题很多设备上报的累计数值在重启后会清零重置因为统计模块的计数器保存在内存里没有持久化。这会导致界面上的“昨天用量”突然归零。解决思路是在 Flutter 层维护一份本地缓存。每次拿到原生数据后先和上一次记录做比对。如果当前值小于上次值说明系统计数器已重置需要用“历史累计值 本次当前值”来修正。同时把修正后的结果持久化到本地数据库。这套逻辑虽然多写了几十行代码但实际使用中非常关键不然用户看到的数据一重启就变化对“监管”类工具来说完全不可接受。数据项获取方式注意事项卡槽数量遍历系统卡槽索引部分设备存在双卡槽但仅单卡可用运营商名称读取 SIM 卡信息可能为空需兜底显示ICCID读取 SIM 卡信息作唯一标识优选字段当前周期流量流量统计接口需自行记录周期起点历史流量流量统计接口设备重启可能清零需缓存修正4. 数据监控策略与界面联动4.1 双卡场景的管理逻辑移动数据监管最绕不开的就是双卡双待。一张卡负责通话一张卡负责上网这种场景在国内用户里非常普遍。所以我必须让 SIM 卡管理模块明确区分“卡槽”和“逻辑卡”。卡槽是物理位置逻辑卡才是当前正在使用的数据卡。我设计了这样的管理方法获取当前默认数据卡槽位。展示卡 1、卡 2 各自的运营商标识和状态。用户切换默认数据卡时发起原生切换接口并在回调中刷新所有页面数据。切换默认数据卡这个操作在部分系统版本上需要较高权限不是所有第三方应用都能直接调用。我在实现时做了分级处理能直接切换的版本直接切换不能直接切换的版本跳转到系统设置页由用户手动操作App 只负责检测结果并同步状态到界面。这里面有一个比较值得注意的细节双卡切换后原本统计到卡 A 的流量数据不会自动归到卡 B而是体现为“切换后新产生的流量计入卡 B”。所以我在流量报表里加了一个“数据卡切换记录”能清晰看到哪个时刻流量归属发生了变化这样用户不会误以为“上个月卡 A 用了 50GB怎么这个月变成卡 B 用了 50GB”。4.2 界面层的定时刷新与事件驱动界面数据刷新我采用了“定时轮询 主动拉取”混合策略。进入页面时主动拉一次页面可见期间每 30 秒轮询一次同时监听系统 SIM 卡状态变化的广播事件。如果用户插拔卡或者切换双卡原生插件会通过另一个支持的通道通知 Flutter 层刷新避免用户手动下拉刷新。UI 层的数据模型我建了一个统一的SimState类包含卡槽列表、每张卡的数据用量、当前网络类型、默认数据卡槽位。所有页面共用这一个模型每次刷新都替换整个模型对象再用setState驱动界面更新。这样做虽然更新粒度比较粗但对这个应用的数据量来说性能完全够而且逻辑清晰不太会出现某个字段漏刷新的问题。为了兼容慢速设备我在列表页做了“先显示缓存再后台刷新”的策略。用户看到的是上一次成功获取的数据加载动画只出现在第一次冷启动。实际体验下来这个方法极大降低了页面闪烁感。5. 权限与隐私合规细节5.1 权限申请的正确姿势OpenHarmony 的权限模型里SIM 卡状态和流量统计相关的能力属于敏感权限。应用必须在 module.json5 里声明对应权限同时运行时还要动态弹窗申请。我吃过一次亏只在配置里声明了权限没有做动态申请结果安装后发现死活拿不到数据最后检查日志才定位到权限没有在运行时确认。需要申请的关键权限包括获取网络状态信息。获取 SIM 卡信息。读取流量统计数据。在部分功能下还需要获取定位权限因为某些运营商的网络状态查询依赖基站信息。权限申请的顺序也有讲究。我用的是一次性申请弹窗把常用权限全部拉出来用户统一授权。虽然这种方式看起来不够细腻但对工具类 App 来说反而减少了反复打扰。官方建议动态申请时附上用途说明我在弹窗前会先弹一个自定义说明页告诉用户为什么需要这些权限能明显提高授权通过率。5.2 后台统计与省电之间的取舍移动数据监管类 App 很容易做成“后台流氓”动不动就常驻进程不断请求流量数据。我在设计时给自己定了一条原则不为后台统计牺牲系统功耗。实时监控的意义是用户打开 App 时能看到准确数据而不是 7 乘 24 小时都知道流量用了多少。所以我的实现方案是应用在前台时正常轮询退到后台后停止轮询并清除定时器用户下次打开时重新拉取数据并用本地缓存计算后台期间的使用量。如果用户需要低电量提醒或超量提醒我把提醒能力下沉到了系统侧通过设置周期性的后台任务触发而不是自己保活进程。这个方案既保证了核心功能又没有过度占用系统资源实际测试中耗电表现非常正常。在隐私展示上我还在设置页提供了“本地存储数据导出/清除”功能。所有的流量记录默认只保存在本地不主动上传任何服务器。对监管类工具来说数据可信度建立在透明性上这个设计也帮助项目过了多轮内部评审。6. 常见问题与排查实录6.1 方法通道调用无响应我在联调中遇到的第一个大坑就是 Flutter 调用原生方法时回调一直不返回。当时 Flutter 端没有报错也没有超时提示整个调用就像石沉大海。排查后发现问题出在插件注册时机上。OpenHarmony 的插件入口需要在主线程完成注册如果 Flutter 页面启动得比插件注册还早通道就会出现“有名字但没人接听”的情况。解决方法是把插件初始化放到应用入口的生命周期里确保 Flutter 引擎加载完成之前原生侧已经完成插件注册。另外我在 Dart 侧给每个通道方法加了 10 秒超时保护。一旦超时界面会显示可重试的占位状态而不是白屏等死。这个超时保护在后来的设备兼容性测试里救了两次场。6.2 双卡信息读取不全第二类常见问题是某些设备上报的卡槽状态不准确。明明插了两张卡但应用只能读到一张或者读到两张后其中一张的运营商名称为空。我定位后发现部分设备在系统开机后的短暂时间内SIM 卡状态仍处于未就绪立刻调用接口会拿到空值。我的方案是加入重试机制。首次读取失败后间隔 1 秒、2 秒、4 秒做指数退避重试最多尝试三次。如果三次后仍然读不到某张卡就认定该卡槽为空。这种策略在实际运行中几乎能解决 90% 的偶发读取不到问题。还有一部分设备返回的 ICCID 中带有空格或不可见字符我在 Dart 层统一做了字符串清洗比如去除首尾空白、过滤非打印字符避免后续作为唯一标识时出现数据不一致。6.3 流量统计数值对不上流量统计对不上是我花时间最多的一个问题。亏了本地缓存修正逻辑后系统计数器清零的问题解决了但还有一个隐蔽场景两张卡都启用数据连接时系统流量统计接口可能把跨卡流量记到同一张卡上。这个问题的根源在于统计接口的粒度是按网络类型汇总而不是严格按卡维度拆分。我采用的办法是把“按卡统计”改为“按默认数据卡拆分”。默认数据卡之外的卡在未启用数据连接的情况下产生的流量极少可以忽略。对启用副卡数据漫游的场景则在统计逻辑里单独标记并提示用户流量统计可能存在合并计算需要以运营商账单为准。排查过程中我还养成了一个好习惯每次测试 SIM 卡相关功能时同时打开系统自带的流量统计页面做对照。两边数据如果差异超过 5%就说明接口选型或缓存逻辑有问题需要回到原生层重新看数据源。这个方法非常实用比瞎猜快得多。7. 写在最后的实操心得整个 SIM 卡管理模块做完我最大的体会是跨端开发 App难点永远不在 UI而在能力边界。Flutter 帮我们把界面和业务逻辑做得非常舒服但真正决定这类工具 App 能不能用的是底层数据拿得准不准、稳不稳。后面如果要扩展我建议先从流量提醒通知和月度报告导出这两个方向入手这两个功能在现有架构上都能比较平滑地加上。最后再分享一个小技巧在开发阶段一定要在原生侧加一个文件日志模块把每次获取 SIM 卡信息、流量统计的原始返回值完整记录下来。我靠这个日志解决了很多用户反馈“数据不对”的疑难杂症比反复复现问题高效得多。工具型应用的价值就在“能查到准数据”这六个字上把这一件事做好就赢了。