拓冰建站拓冰建站
首页 / 资讯中心 / 正文

OpenHarmony+Flutter SIM卡管理实战:跨平台开发的坑与解法

去年年底接手了一个有意思的项目在OpenHarmony设备上做一款移动数据使用监管助手重点之一就是SIM卡管理。这个需求放在普通安卓环境里稀松平常但落到OpenHarmony加Flutter这套组合上坑比想象中多。这篇实战记录不是什么官方教程是我把完整开发过程中踩过的坑、调通的接口、验证过的方案整理出来给正在做或者准备做同类项目的朋友一个参考。项目本身不复杂核心就是读取SIM卡状态、运营商信息、ICCID、IMSI这类基础数据再结合流量统计做数据使用监管。难点在于OpenHarmony的生态资料少、版本碎片化严重而且Flutter官方对OpenHarmony的支持还在持续完善中很多安卓上理所当然的API在这里要么没有、要么改了个名字。如果你也在折腾Flutter for OpenHarmony或者想了解SIM卡管理在鸿蒙生态里到底怎么实现这篇文章应该能帮你少走不少弯路。1. 项目到底做什么移动数据监管助手的需求拆解1.1 这不是普通流量统计App先把这个项目的定位说清楚。市面上的流量统计工具一大堆但“移动数据使用监管助手”不是简单地记录你本月用了多少MB它更像一个数据管家自动识别当前插入的SIM卡、区分双卡流量、监控后台应用的实时流量消耗、设置流量阈值并在超额时提醒甚至断网。核心是“监管”不是“展示”。这就意味着SIM卡管理不能只做到“读出来”这一步还得和业务逻辑联动。比如双卡设备上用户用卡1还是卡2上网流量统计口径完全不同再比如设备漫游状态变化时监管策略要能自动调整。所以在设计阶段就要把SIM卡信息抽象成数据模型而不是在UI层到处直接调API。我当时拿到需求后第一反应是列了一张功能清单SIM卡插拔状态实时监听SIM卡基础信息读取运营商、ICCID、IMSI、电话号码、SPN多卡设备的主卡/副卡识别与切换数据流量统计与阈值告警联动漫游状态检测清单看着不长但每一项在OpenHarmony上都能衍生出不少细节问题后面我会逐个拆开讲。1.2 为什么选Flutter加OpenHarmony这套组合先说OpenHarmony。这玩意和HarmonyOS不是一回事OpenHarmony是开源底座HarmonyOS是商业发行版很多面向消费者的能力在OpenHarmony里是不完整的。我们项目用的是OpenHarmony设备所以要自己处理很多底层细节。选Flutter的原因很实际团队之前有Flutter积累Dart语言写业务逻辑效率高UI层跨平台一致性有保障。虽然OpenHarmony官方主推的是ArkTS加ArkUI但用Flutter也不是不行OpenHarmony社区已经有专门的Flutter适配分支而且经过几个版本迭代稳定性已经能支撑真实业务落地。不过要泼一盆冷水Flutter for OpenHarmony和Flutter for Android/iOS在体验上有差距。最明显的就是热重载偶尔失效、部分插件需要自己写平台通道适配、Impeller渲染引擎在鸿蒙设备上还不完全兼容。后面我会专门讲这些坑怎么绕。技术选型时我做了个简单对比方案优点缺点ArkTS ArkUI官方主推底层能力对接最顺团队需要重新学语言和框架Flutter OpenHarmony分支跨平台复用Dart开发效率高生态不成熟部分插件需自行适配原生C JS混合性能最佳开发效率低维护成本高最终选了Flutter核心原因是“跨平台复用”这四个字。项目后期要扩展到其他设备形态Flutter的UI层可以直接复用只需要在平台通道上分别适配。1.3 整体架构从UI到SIM卡底层的通信链路架构上我没有搞太复杂就是标准的Flutter分层UI层Flutter Widget负责数据展示和用户交互业务层Dart编写处理流量统计逻辑、阈值判断、状态机管理数据层通过MethodChannel调用OpenHarmony原生能力原生层用ArkTS或C封装OpenHarmony的Telephony Kit接口这里最关键的是MethodChannel的设计。你可以把MethodChannel理解成一条电话线Dart端拨号、原生端接听两边约好一套“暗号”也就是方法名和参数格式。我建议一上来就定义一套规范的协议不要边写边改不然后期维护会非常痛苦。MethodChannel命名规范com.yourapp.sim_manager 方法名规范sim.getState、sim.getIccId、sim.getOperator为什么统一用MethodChannel而不用EventChannel因为SIM卡管理的核心是一次性查询状态变化可以用轮询加广播监听实现。EventChannel适合连续数据流比如传感器数据、定位数据用在SIM卡场景有点大材小用。2. 环境搭建与工程初始化把地基打好2.1 Flutter SDK与OpenHarmony SDK的版本匹配这步是新手最容易翻车的地方。别以为装个Flutter就能直接编译鸿蒙应用Flutter官方的稳定分支根本不认识OpenHarmony必须用社区维护的OpenHarmony分支。我推荐直接用OpenHarmony团队维护的Flutter SDK地址在Gitee上。版本选择有个原则尽量用和OpenHarmony SDK版本配套的发行版不要追新。我当时用的组合是Flutter 3.7.12-ohos加OpenHarmony API 9稳定性还不错。安装方式有两种方式一直接把Flutter-ohos仓库克隆下来配置PATH环境变量 方式二用FVMFlutter Version Management管理多版本强烈建议用FVM。原因很简单这个项目你可能同时要维护安卓版本和鸿蒙版本两个版本对应的Flutter SDK不同没有版本管理工具你会疯掉。FVM的配置文件如下{ flutterSdkVersion: 3.7.12-ohos, flavors: { openharmony: 3.7.12-ohos, android: 3.7.12 } }装好后记得执行fvm use 3.7.12-ohos切换版本然后用fvm flutter doctor检查环境是否正常。如果提示找不到OpenHarmony SDK需要在local.properties或环境变量里配置ohos.sdk.dir。2.2 创建支持OpenHarmony的Flutter工程这里有个细节你自己创建的Flutter工程默认是不带OpenHarmony工程结构的需要手动添加。OpenHarmony适配分支的flutter create命令会生成一个带有ohos目录的工程。fvm flutter create --platforms ohos sim_manager_app如果你的Flutter-ohos分支版本比较老可能不支持--platforms ohos参数那就先创建一个普通工程再把ohos目录从官方示例工程里拷贝过来改一下包名和项目名。手动添加方式也没多难核心是确保ohos目录下的build-profile.json5和oh-package.json5配置正确。工程创建完后的目录结构大致是这个样子sim_manager_app/ ├── lib/ # Dart代码 ├── ohos/ # OpenHarmony工程 │ ├── entry/src/main/ # 入口模块的ArkTS代码 │ ├── entry/src/main/ets/ # 原生层代码 │ └── build-profile.json5 ├── pubspec.yaml └── ...第一次跑通工程大概会花掉半天到一天时间大部分时间都耗在SDK配置和依赖拉取上。2.3 首次联调的坑镜像源与依赖拉取如果你在国内开发第一道坎就是依赖拉取。pub.dev和GitHub的访问速度都不稳定需要配置镜像。Flutter-ohos分支的很多依赖是从Gitee和华为仓库拉取的所以需要额外配置华为的OpenHarmony仓库地址。我的pubspec.yaml里的依赖配置长这样dependencies: flutter: sdk: flutter provider: ^6.0.5 event_bus: ^2.0.0 intl: ^0.18.1原生层的依赖配置在oh-package.json5里需要引入ohos/telephony这个包{ dependencies: { ohos/telephony: 1.0.0 } }这里有一个容易踩的坑OpenHarmony的SDK版本不同ohos/telephony的接口签名会有差异。API 9和API 10在电话能力模块上变化不小如果你照着老版本API写的代码在API 10上编译报错先去官方文档查一下接口是否被废弃或移动了包路径。3. 核心功能实现SIM卡管理从底层到业务3.1 权限申请没有权限一切都是空谈SIM卡信息属于敏感数据OpenHarmony这边同样有严格的权限管控。搞权限的时候我翻了不少文档最终确认涉及的核心权限有三个ohos.permission.GET_TELEPHONY_STATE // 获取电话状态和SIM卡信息 ohos.permission.READ_CALL_LOG // 如果需要读取通话记录关联数据 ohos.permission.GET_NETWORK_INFO // 获取网络信息权限申请分两步。第一步在module.json5里声明第二步在运行时动态请求。OpenHarmony的权限分为system_grant系统授权和user_grant用户授权两类SIM卡相关权限大部分属于user_grant必须在运行时弹窗请求。// module.json5 中的配置 { requestPermissions: [ { name: ohos.permission.GET_TELEPHONY_STATE, reason: $string:sim_info_permission_reason, usedScene: { abilities: [MainAbility], when: inuse } } ] }运行时请求权限的代码在ArkTS侧通过abilityAccessCtrl实现import abilityAccessCtrl from ohos.abilityAccessCtrl; import bundleManager from ohos.bundleManager; let atManager abilityAccessCtrl.createAtManager(); let bundleInfo await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION); let tokenId bundleInfo.appIndex; let permissions: ArrayPermissions [ohos.permission.GET_TELEPHONY_STATE]; let result await atManager.requestPermissionsFromUser(context, permissions); // 检查result.authResults数组确认权限是否授予这里有个细节坑requestPermissionsFromUser返回的授权结果是异步的而且如果用户之前在系统设置里手动关闭了权限再调用这个方法不会重新弹窗会直接返回denied。这种情况下只能引导用户去系统设置里手动开启这也是很多安卓开发者的老熟人操作。3.2 封装平台通道Flutter与OpenHarmony的“传话筒”平台通道是整个项目的命脉。我在Dart侧定义了一个统一的SIM管理接口原生侧实现具体逻辑两边通过MethodChannel通信。Dart侧的核心封装// lib/services/sim_service.dart class SimService { static const MethodChannel _channel MethodChannel(com.yourapp.sim_manager); static FutureSimInfo getSimInfo(int slotIndex) async { try { final Mapdynamic, dynamic result await _channel.invokeMethod( sim/getInfo, {slotIndex: slotIndex}, ); return SimInfo.fromMap(MapString, dynamic.from(result)); } on PlatformException catch (e) { throw SimException(e.code, e.message ?? Unknown error); } } static Futurevoid registerSimListener() async { await _channel.invokeMethod(sim/registerListener); } }这边接口设计的几个细节所有方法都有明确的错误码返回Dart侧按错误码分类处理而不是笼统抛一个异常方法名统一用模块/动作格式比如sim/getInfo方便后期扩展和排查涉及多卡的操作必须传slotIndex参数不能默认只查卡0原生侧ArkTS实现// entry/src/main/ets/MainAbility/SimManager.ets import sim from ohos.telephony.sim; import data from ohos.telephony.data; export class SimManager { private static instance: SimManager; private constructor() {} static getInstance(): SimManager { if (!SimManager.instance) { SimManager.instance new SimManager(); } return SimManager.instance; } async getSimInfo(slotIndex: number): PromiseObject { let simInfo: Recordstring, Object {}; try { simInfo[state] await sim.getSimState(slotIndex); simInfo[iccid] await sim.getSimIccId(slotIndex); simInfo[imsi] await sim.getIMSI(slotIndex); simInfo[telephoneNumber] await sim.getTelephoneNumber(slotIndex); simInfo[operator] await sim.getSimOperatorNumeric(slotIndex); simInfo[spn] await sim.getSimSpn(slotIndex); simInfo[isMultiActive] await sim.isSimActive(slotIndex); return simInfo; } catch (error) { throw new Error(getSimInfo failed: ${JSON.stringify(error)}); } } }原生侧拿到结果后返回给Dart的是一个扁平化的JSON对象Dart侧再解析成强类型模型。这里我习惯在两边各做一次数据校验原生侧保证字段不为空Dart侧保证类型转换失败时有默认值兜底。3.3 SIM卡状态与信息读取把卡里能读的都读出来SIM卡状态读取看起来简单实际上不同设备返回的状态值定义有细微差异。OpenHarmony的getSimState返回值定义如下状态值含义说明0SIM_STATE_UNKNOWN未知状态常见于热插拔瞬间1SIM_STATE_NOT_PRESENT卡槽无卡2SIM_STATE_LOCKED需要PIN/PUK解锁3SIM_STATE_READY正常就绪4SIM_STATE_LOADED已加载可读取完整信息5SIM_STATE_DEACTIVATED卡已被禁用这里有个容易误判的场景双卡设备上如果其中一个卡槽是空的很多开发者会直接认为设备是单卡。实际上必须遍历所有卡槽来检查。推荐的做法是遍历0到getMaxSimCount() - 1逐个判断状态才能在UI层正确显示卡1、卡2的占位信息。IMSI和ICCID这类敏感信息在部分设备上可能返回空字符串或者****脱敏值。这跟设备厂商的安全策略有关不是你的代码有问题。我当时接手的时候一度以为是API调错了后来用鸿蒙自带的设置App对比发现系统设置里同样显示脱敏值这才确认是设备策略。运营商信息这一块getSimOperatorNumeric返回的是MCCMNC组合比如46000代表中国移动。但OpenHarmony不同的API版本返回格式有差异有的返回46000有的返回460 00。我建议拿到后统一做格式化处理按三位MCC加两位或三位MNC重新拼接不要直接用原始字符串去匹配。SPNService Provider Name是运营商名称理论上应该返回“中国移动”“中国联通”这类中文名称。但实测发现个别设备厂商会把SPN写成英文缩写或者干脆不写入SPN字段。这时候需要在业务层做一次运营商名称映射兜底根据MCC/MNC查表得到中文名称。3.4 数据流量统计与监管策略联动数据流量这一块OpenHarmony提供了ohos.telephony.data模块的getDefaultCellularDataSlotId和getCellularDataFlow接口。这个设计比安卓要细它明确区分了默认数据卡和语音卡流量统计默认跟着数据卡走。核心统计逻辑// 流量统计实现 import data from ohos.telephony.data; let slotId await data.getDefaultCellularDataSlotId(); let flowInfo await data.getCellularDataFlow(slotId); // flowInfo包含rxBytes(下行)和txBytes(上行) // 统计周期内的增量 let lastRxBytes await getLocalStorage(last_rx_bytes); let lastTxBytes await getLocalStorage(last_tx_bytes); let periodRx flowInfo.rxBytes - lastRxBytes; let periodTx flowInfo.txBytes - lastTxBytes;这里踩过一个大坑getCellularDataFlow返回的是设备开机以来的累计流量不是从月初算的。所以必须自己持久化上次统计值做差值计算。持久化方案我选的是轻量级偏好存储也就是SharedPreferences在鸿蒙上的对应实现ohos.data.preferences不要用数据库杀鸡不用牛刀。监管策略联动是业务层的重点。我在Dart侧设计了一个简单的状态机enum DataUsageStatus { normal, // 正常 warning, // 已用80% critical, // 已用100% exceeded, // 已超额度 }每次流量数据刷新后先算当前周期已用总量再和用户设置的阈值比较触发不同状态。状态变化时通过EventBus通知UI层刷新同时调用原生层的通知服务弹出提醒。这里我建议加一个定时轮询机制不要只依赖被动回调。因为OpenHarmony的流量变化回调在某些设备上不稳定我用的妥协方案是前后台切换时强制刷新一次另外每5分钟在后台拉一次数据。配合状态机的幂等判断既不会漏消息也不会重复弹通知。3.5 界面呈现让用户看得懂的数据面板UI层面没有太多黑科技重点是把多卡信息、流量状态、阈值进度这些数据组织得清晰。我整套界面就三个核心页面首页概览、SIM卡管理、流量详情。首页概览顶部是当前数据卡的状态卡片显示运营商、信号格数、SIM卡状态。下面是一个圆形进度指示器展示本月流量使用百分比。点击进度环可以跳转到流量详情页按应用维度查看流量消耗排行。SIM卡管理页是这次改造的重头戏。双卡设备上需要展示两个卡片每张卡片标注卡槽位置、运营商、号码以及当前是否为主卡。用户点击卡片可以切换默认数据卡这个操作在底层是调用data.setDefaultCellularDataSlotId实现的。// Dart侧切换默认数据卡 Futurevoid switchDefaultDataSlot(int slotIndex) async { await _channel.invokeMethod(sim/setDefaultDataSlot, { slotIndex: slotIndex, }); // 切换成功后刷新页面状态 await _loadSimInfo(); }需要注意切换默认数据卡这个操作在部分OpenHarmony设备上需要系统权限普通应用调用会被拒绝。实测下来有些设备允许有些直接抛SecurityException。我的处理方式是调用前先查一下设备是否有这个权限没有就在UI上禁用切换按钮避免用户点了没反应造成困惑。4. 真机调试与常见问题排查实录4.1 权限弹窗反复出现这个问题折磨了我一整天。现象是第一次启动请求权限用户点了允许功能正常下次冷启动后权限又被重置了系统再次弹出权限请求框。排查过程从两个方向同时进行检查module.json5里的权限声明是否和实际使用一致确认应用是否在权限被拒后走了错误的分支逻辑最终定位到的问题很无语代码里有两个地方调用了权限检查一个是MainAbility的onWindowStageCreate另一个是Flutter侧通过MethodChannel触发。两个调用点的逻辑不一致Flutter侧判断“无权限”后没有重新走系统授权流程而是直接进了一个无法恢复的死分支。下次启动时权限状态没有被正确记录系统又弹了一次。修复方式是在Dart侧加了一个权限状态缓存首次获取到用户授权结果后就写入偏好存储后续所有页面都从缓存读取不再重复触发系统请求。权限状态缓存 - 已授权正常进入功能 - 已拒绝显示引导页提供“去设置开启”按钮 - 未请求首次弹出系统授权框4.2 双卡设备上读到同一张卡的IMSI这个问题也很有迷惑性。设备明明插了两张卡但读取卡2的信息时返回的内容和卡1一模一样。最开始的判断是API调用参数传错了检查了好几遍代码slotIndex参数确实传的是1。后来用OpenHarmony自带的网络调试工具验证发现系统设置里显示的卡2信息也是错的这才确认是设备或系统的bug不是应用层的问题。这种情况在模拟器上更容易出现。OpenHarmony的模拟器对双卡的模拟支持很不完善很多API直接返回的是同一份数据。我的建议是遇到这种问题先升级系统版本不行就换一台真机验证。如果确定是系统bug在代码里做一个防御性校验检测到两卡信息完全相同时提示用户可能有系统兼容性问题。后来我在代码里加了一个简单的数据校验逻辑if (simInfo1.iccid simInfo2.iccid simInfo1.iccid.isNotEmpty) { // 双卡数据异常可能是系统层模拟器或设备问题 }4.3 MethodChannel调用超时Flutter侧有时会收到PlatformException(error: null, message: null)或者调用后迟迟没有返回。排查下来有几个原因OpenHarmony原生侧执行耗时操作时阻塞了UI线程MethodChannel的方法名或参数类型不匹配原生侧抛了未捕获的异常Flutter-ohos分支本身的通道通信存在已知bug部分版本在频繁调用时丢消息对于前两个原因解决方式是在原生侧把耗时的SIM卡读取操作放到子线程执行通过TaskPool或Worker异步处理避免阻塞主线程。第三类问题只能尽量避免精简调用频率同时调用时加一个超时保护FutureT invokeWithTimeoutT(MethodChannel channel, String method, Map arguments, {Duration timeout const Duration(seconds: 5)}) async { return channel.invokeMethodT(method, arguments).timeout(timeout); }超时后做兜底处理在UI层提示“数据加载失败请重试”不要卡死页面。我还遇到过一种特殊情况首次调用MethodChannel时通道还没完全初始化等一秒钟再调就正常了。解决办法是在main()里提前执行一次通道预热比如调用一个空的sim/ping方法确保通道建立后再拉数据。4.4 Flutter渲染异常与热重载失效OpenHarmony适配分支的Flutter在部分设备上会出现渲染异常。我遇到的现象是切换页面时偶发白屏或闪烁尤其是使用hero动画和PageView的场景有明显撕裂感。这和Flutter的渲染引擎有关OpenHarmony分支默认用的是Skia支持Impeller的设备很少而且Impeller在鸿蒙上兼容性也不稳定。解决思路很简单不要用太花哨的转场动画尤其是涉及图片缩放、模糊滤镜的高开销操作能省则省。动画这块我用的是最简单的FadeTransition和SlideTransition复杂效果直接不做。热重载失效的坑更常见。Flutter开发最依赖热重载但在OpenHarmony设备上经常出现改完代码按r没有反应或者热重载后页面状态错乱的情况。我的解决方案是优先用fvm flutter run --hot启动热重载失败时用R冷重启涉及原生层代码修改时必须完全重新编译安装Dart代码热重载对原生层改动无效每次改动后先看控制台日志是否出现“reload failed”提示及时切冷重启这里提醒一下如果是修改了module.json5或者原生权限配置一定要完全卸载应用再安装否则权限状态不会更新会出现刚才提到的那种权限被系统缓存的情况。5. 踩过坑之后的几点体会说真的Flutter for OpenHarmony这个组合做生产级应用可行但要有心理准备。它不是那种装好环境就能顺畅开发的体验很多细节需要自己摸索。经过这个项目的打磨我有几点经验想分享在SIM卡管理这块最核心的认知是不要把OpenHarmony看成类安卓系统。虽然接口设计上有很多相似之处但底层实现、权限模型、API稳定性都有自己的一套逻辑。安卓上直接照搬代码大概率要踩坑。最好的方式是先读官方API文档理解当前版本的接口设计再动手写代码。其次是做好防御性编程。SIM卡信息读取受设备、系统、运营商三方面影响任何环节都可能出现异常数据。空值兜底、状态校验、超时保护这些处理不是浪费时间是真正上线之后保命的。最后是版本管理一定要抓牢。OpenHarmony和Flutter-ohos分支都在快速迭代今天能编译通过的代码换个SDK版本可能全是报错。我习惯把SDK版本、OpenHarmony版本、关键依赖版本都锁死每次升级前先跑完整测试。这个项目后续如果要适配新版本系统至少能有一个稳定的基线可以回退。这个项目从启动到完成前前后后用了大概三周时间。如果把环境搭建和踩坑的时间都算上差不多一半时间是在和工具链较劲。但当你看到Flutter界面在OpenHarmony设备上流畅跑起来的那一刻还是会觉得值得。素材积累了不少后续如果再碰SIM卡管理的深度功能比如5G网络状态监控、eSIM远程配置这些我会继续整理出来分享。最后再给一个小建议选设备时尽量选厂商适配积极的型号别用太冷门的开发板。设备兼容性差异在SIM卡这种涉及底层通信的功能上体现得尤为明显用主流的开发板能省掉大量“为什么这个设备读不出来”的排查时间。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门