从年初开始,我们就在做一款面向开源鸿蒙的移动数据监管助手App,最核心也最折磨人的模块就是“省流模式”。先说结论:这套功能在Android上已经有一堆成熟范式,但换到Flutter for OpenHarmony环境,光是把流量数据拉准、把省流策略真正落到系统侧,就花掉了整个项目三分之一的排期。UI层的仪表盘、饼图、流量明细列表反而不是最难啃的骨头。如果你也想在OpenHarmony上做流量监控或者其它需要深度调用系统能力的Flutter应用,这篇实战记录应该能帮你省不少弯路。
我尽量按真实的开发顺序来写:先讲为什么选Flutter而非原生方案,再拆流量采集与策略引擎的架构,最后重点聊Flutter双端通信和实测里踩过的坑。所有代码片段都是项目里抽出来的简化版本,接口名称不一定跟你手上的SDK完全一致,但逻辑路径是走得通的。
1. 为什么流量监管助手会把原生方案换成Flutter:OpenHarmony侧的选型复盘
1.1 痛点:原生UI迭代节奏撑不起高频改动
最初版本是用OpenHarmony原生ArkTS写的,功能其实不算多:一个实时流量仪表盘、月度配额进度条、App流量排行、省流模式开关页。但真正跑起来之后,几个问题让我特别头疼。
第一是UI性能。流量仪表盘需要每秒刷新数据,饼图在低端开发板上掉帧严重。ArkTS的声明式UI在复杂列表和频繁数据更新场景下,性能调优手段相对有限,折腾了很久也只能做到“勉强能看”。第二是发版节奏。团队人少,需求却常变,今天要加“夜间省流时段”,明天要调“超额后的弹窗策略”,原生方案每次改动都要走编译、签名、真机验证的完整链路,一天能迭代三次就算快了。
第三是跨设备一致性。OpenHarmony的设备形态太杂了:开发板、平板、手机,UI适配工作量成倍增长。Flutter的自绘引擎天然屏蔽了这部分差异,一套代码在不同分辨率下表现很稳定。这三点叠加在一起,让我们决定把UI层整体迁到Flutter。
1.2 Flutter for OpenHarmony的适配现状与边界
选择Flutter for OpenHarmony之前,我们最担心的是适配成熟度。目前OpenHarmony上的Flutter主要由社区维护,适配版本通常落后Flutter主干稳定版一到两个大版本,但基础渲染、文本输入、列表滚动这些常用能力都已经能用了。
真正需要留心的是插件生态。国内开发者习惯用的很多Flutter包,比如带原生依赖的定位、推送、支付SDK,在OpenHarmony上统统不可用。Firebase全家桶、某些热更新SDK也别指望。但这恰好是我们选择Flutter的重要理由:流量监管App的系统能力需求非常集中,无非就是读流量统计、查应用列表、设置网络策略,这些都可以收敛成少数几个自定义插件,不需要依赖庞杂的三方生态。
我的选型建议是:如果应用属于“UI交互重、系统能力少且集中”,Flutter for OpenHarmony完全值得试。反之,如果大量依赖蓝牙、NFC、传感器这类碎片化能力,还是老老实实写原生ArkTS。我们App里真正需要对接的原生能力只有三个:流量数据查询、应用信息读取、省流策略下发。边界清晰了,架构上就稳了。
2. 省流模式的根基:流量数据的准确采集与口径对齐
2.1 按应用维度统计:先拿到“流量账单”
省流模式做得再花哨,底层数据不准就是空中楼阁。流量统计要解决的核心问题是:某个App在某个时段到底用了多少移动数据,其中多少在前台产生、多少在后台偷跑。
OpenHarmony系统侧其实已经有网络统计相关服务在跑,能按UID维度记录每个应用的收发字节数。我们做的第一件事,就是把UID和包名对应关系建好,然后通过系统查询接口周期性拉取快照。
这里有一个新手容易掉进去的坑:有人想直接读/proc/net下的内核文件来统计流量。这个方案在开发板上看似可行,但实际不可靠。一方面,不同网口、不同网络类型的聚合逻辑非常复杂,Wi-Fi和移动数据的流量你得分清楚;另一方面,Linux内核暴露的计数器包含系统进程和其它非应用流量,口径对不上用户感知的“流量包剩余量”。最终我们放弃直接读内核,改为调用系统侧统计服务。
原生侧查询大致的逻辑是这样:
// 原生侧,ArkTS伪代码,实际以当前SDK接口为准 import systemDataQuery from '@ohos.system.dataQuery'; let totalMobileBytes = 0; let statsList = await systemDataQuery.getUsageStats({ startTime: startTimestamp, endTime: endTimestamp, networkType: NetworkType.MOBILE }); statsList.forEach((item) => { let uid = item.uid; let appInfo = appInfoMap.get(uid); if (appInfo == null) return; totalMobileBytes += item.rxBytes + item.txBytes; });有了总增量还不够,我们还需要区分前台和后台流量。大部分系统统计服务会把流量拆成前台字节和后台字节两个字段。如果拿不到细分字段,可以用另一种间接方案:监听应用前后台切换事件,切换时记录当前累计值,用差值近似估算。但这方法在应用多开、分屏场景下误差较大,只能作为降级方案。
2.2 前后台流量区分与Wi-Fi/移动数据双通道
我们最终在数据层做了四通道统计:
| 通道 | 含义 | 省流模式重点 |
|---|---|---|
| 移动数据前台 | 屏幕可见时消耗的蜂窝流量 | 一般不限制 |
| 移动数据后台 | App在后台时的蜂窝流量 | 重点限制对象 |
| Wi-Fi前台 | 屏幕可见时消耗的无线流量 | 不限制 |
| Wi-Fi后台 | App在后台时的无线流量 | 可选限制 |
之所以分这么细,是因为省流模式的策略要区分场景:用户在Wi-Fi下打视频电话,我们不应该去掐断;但同一款应用在移动数据下后台频繁刷广告,就该限制。
统计模块采用定时采样加事件触发双机制。定时器每5分钟做一次增量计算,同时监听网络切换事件和唤醒事件,一旦发现网络类型变化就立刻触发一次采样,避免数据漏记。每次采样后把增量提交到本地数据库,Flutter侧按需查询。
这轮的教训是:别在原生层做过度封装。最初我想把统计逻辑全部封装在原生侧,Flutter只拿结果。后来发现原生侧调试UI不方便,规则调整周期赶不上Flutter的迭代速度。最终拆成两段:原生侧只负责“采集增量数据并持久化”,Flutter侧通过MethodChannel拉取并按规则聚合。这样职责清晰,两边都可以快速调试。
3. 省流策略引擎:从“一刀切”到基于场景的动态控制
3.1 三级省流档位:关闭、智能、强力
省流模式最忌讳的是简单粗暴地限制所有App联网,那结果必然是天天收到用户差评。我们设计了三个档位:
- 关闭模式:不干预任何网络请求,只保留统计与提醒。
- 智能模式:允许前台App正常联网,限制后台App的移动数据使用。高频刷新的应用(比如新闻资讯)在流量紧张时自动改成“仅Wi-Fi联网”。
- 强力模式:只允许白名单App联网,其余App的移动数据一律拦截,同时触发图片压缩和视频清晰度限制规则。
每个模式都不是固定参数,而是一组规则集合。我们把规则定义成可配置的JSON,从Flutter侧下发到原生侧执行:
{ "level": "smart", "rules": [ { "type": "background_mobile_block", "enabled": true, "whitelist": ["com.tencent.mm", "com.dingtalk"], "timeRange": {"start": "22:00", "end": "07:00"} }, { "type": "image_compress", "enabled": false, "minSizeKB": 300 } ] }这里有个日常开发中容易忽略的点:规则下发要支持灰度切换。我们最初是用户一改动开关,Flutter直接拉到原生层生效,结果真机验证时发现部分设备策略没有立即生效,排查半天原来是系统网络策略服务有缓存,需要刷新接口。后来在设置档位后强制调用一次策略刷新,问题解决。
3.2 配额剩余驱动的自适应规则
省流模式的“智能”不光体现在前后台区分上,还要结合用户每月的流量配额动态调整。我们有三个阈值:剩余60%、剩余30%、剩余10%。
- 剩余60%以上:仅限制后台移动数据流量较大的App,其它规则不启用。
- 剩余30%时:启动“后台全拦截”,此时所有App不允许在后台使用移动数据,前台视频类的自动降清晰度。
- 剩余10%时:进入“白名单模式”,只保留微信、地图等必要应用可用,其余应用前台联网也拦截,并在状态栏常驻提醒。
这个逻辑虽然简单,但效果非常明显。用户并不会因为流量快用完就主动去设置里翻省流选项,他们需要的是一种“不用管太多”的自动保护。我们在策略引擎里维护一个状态机,每次流量采样更新后就重新评估当前状态并决定是否触发规则切换。
为了避免频繁抖动,状态迁移要加滞回。比如说,剩余额度在10%和11%之间反复横跳时,不能一会儿白名单模式一会儿强力模式,这样不仅体验差,还会反复弹通知。我们在迁移条件里加了“进入低档位需要连续3次采样满足阈值,回升高档位需要连续5次采样满足”,实测抖动基本消失。
3.3 原生侧如何真正“掐断”网络请求
策略引擎的难点不只是决策逻辑,更在于如何真正让限制生效。我们尝试过三种方案:
- 应用级联网拦截:通过系统网络策略服务,按应用配置网络访问白名单。这个方案最直接,缺点是只对移动数据生效,无法区分前台和后台。
- 代理方案:在原生侧搭建一个本地代理,所有流量经由此代理转发,从而在转发时做压缩、拦截和限速。效果好但实现复杂,还有不兼容部分非HTTP流量的风险。
- 系统级省流量开关:类似通知栏快捷开关,但这类接口通常会同时限制Wi-Fi,需要谨慎使用。
最终我们选择了“策略服务为主、代理压缩为辅”的组合拳。后台流量限制交给原生策略服务处理,图片压缩和视频清晰度限制则通过代理模块实现。
代理模块实现的细节很多,这里只说关键:代理只对非白名单App的HTTP/HTTPS流量做处理,遇到无法解析的流量直接放行,绝不能在代理层把用户的支付、登录这类加密请求卡死。压缩策略也要小心翼翼,只针对图片类型Content-Type做降质,不能盲目压缩所有资源。
4. Flutter与OpenHarmony双端协作:MethodChannel之外的效率优化
4.1 用EventChannel做流量事件流,绕过轮询地狱
如果流量更新全部靠Flutter端用MethodChannel主动轮询,会出现两个问题:一是通道调用频繁导致UI卡顿,二是流量变化事件无法及时感知。我们在原生侧用EventChannel把事件流推给Flutter:每隔5分钟自动推送一次流量增量,流量到达阈值(比如每50MB)时立刻追加一次推送,省流档位切换时收到策略服务回调也会推送。
Dart侧的事件监听简化后长这样:
const EventChannel _usageEventChannel = EventChannel('dev.datahelper/usage_events'); Stream<UsageSnapshot> _snapshotStream() { return _usageEventChannel .receiveBroadcastStream() .cast<Map<Object?, Object?>>() .map((event) => UsageSnapshot.fromMap(event)); }原生侧推送时要注意节流。最初我们没有做合并,短时间内多个事件连续发射,Flutter侧根本无法响应,白白增加功耗。后来在原生侧加了一个简单合并队列:100毫秒内到达的多个事件合并成一条,再推给Flutter,UI侧压力小了很多。
4.2 UI侧流量仪表盘的刷新策略
流量仪表盘是非常典型的“数据密集”页面,每秒都要展示变化的数字。Flutter自绘引擎处理这类页面性能很好,但不能把每一条新数据都直接触发setState,否则再好的引擎也顶不住高频重建。
我们的做法是维护一个“UI快照”:原生侧推送流量事件后,Flutter侧先把数据存到内存模型里,再通过节流阀门触发页面刷新。节流逻辑是“至少间隔1秒,且与上次显示数值差异超过1MB才刷新”。这样在流量稳定时,UI基本不动;只有数据有实质变化时才重绘,功耗和性能都得到保证。
图表部分用了fl_chart,但动画要小心。刚接入时开启动画后每秒钟图表都在重绘,开发板直接升温。后来把图表动画关闭,只保留“切换时间段”时的过渡,流畅度立刻上来了。
仪表盘之外还有个常驻通知的细节:省流模式开启后,系统通知栏要实时显示今日流量消耗和剩余配额。这个通知由原生侧负责,Flutter无法直接操作通知栏。Flutter侧只需把最新的配额数据同步给原生侧,由原生侧更新通知文案。两边数据同源,否则会出现“UI显示剩余2.3GB,通知栏却显示2.5GB”这种尴尬。
5. 实测踩坑:权限、后台保活与系统省电的相爱相杀
5.1 数据统计权限不是一劳永逸的
流量统计权限在所有平台上都是敏感能力,OpenHarmony也不例外。用户必须到系统设置里手动授权“数据使用访问权限”,App才有资格读取其他应用的流量使用情况。这个授权没法用代码直接弹窗获取,只能引导用户去设置页手动开启。
第一版我们只在安装启动时引导了一次,结果不少用户用着用着发现“数据不更新”,一查才知道是权限被系统优化工具自动收回了。后来做了两层兜底:一是App启动和流量页每次进入时都检查权限状态,权限丢失时显示一个可点击的提示条;二是权限丢失后,本地数据库里保留最近7天的缓存数据,不至于一授权就全部归零。
权限状态可以通过系统接口查询,Flutter侧只需要维护一个布尔值:
Future<bool> checkUsageAccess() async { return await _channel.invokeMethod('hasUsageAccess'); }5.2 长时任务申请与系统省电的冲突
流量监管App不需要常驻前台,但省流模式生效期间必须保证后台进程不被系统杀死。OpenHarmony的后台任务管理有明确分级,我们申请了长时任务权限,理由填写“数据监控与网络策略维护”。申请之后,系统会降低进程回收优先级,但不是百分百保活。
实测发现,部分设备的系统省电模式在夜间会强制执行“暂停后台应用”策略,即使我们申请了长时任务也会被冻结。唯一有效的方案是引导用户把App加入“允许后台运行”白名单。我们把这个引导做进了首次开启省流模式的流程里,而不是塞在设置深处,否则多数用户根本不会发现。
另一个经验:不要使用双进程守护这种方案。在OpenHarmony上搞双进程不仅实现复杂,而且系统升级后很容易被判定为恶意行为,得不偿失。
5.3 不同设备的行为差异与兼容策略
OpenHarmony的碎片化程度比Android还夸张。我们目前在开发板、平板、手机三类设备上做过测试,差异主要在三方面:
- 后台任务管理策略不同:开发板几乎没有后台限制,手机则比较严格,策略验证必须在手机上做。
- 网络统计服务的字段完整性不同:部分设备拿不到“前台/后台”分流量字段,只能拿到总计数值,此时需要降级用前后台切换事件估算。
- 代理模块兼容性不同:部分设备对非标准代理设置支持不好,压缩策略无法生效,需要自动检测并降级为“仅白名单拦截”。
为应对这些差异,我们在原生侧做了一个能力探测模块,App启动时动态检测设备支持的接口和字段,生成一张“能力位图”上报给Flutter侧。Flutter侧根据能力位图决定显示哪些功能入口、隐藏哪些不支持的选项。这个方法虽然土,但非常有效,至少没有让用户点开一个功能却提示“此设备不支持”。
测试矩阵一定要尽早铺开。我们前期只在一台手机上反复验证,结果发布前拿到开发板测试时,发现代理模块直接无法启动。没有能力探测模块的话,这种问题只能靠用户反馈才能发现。
最后再分享几个实战后的优化技巧
流量统计的准确度决定了整个App的口碑。如果本地统计和运营商账单对不上,用户会彻底失去信任。我们做了个小功能:允许用户在上个月账单出来后手动校准一次“当前剩余额度”,把差值作为补偿因子注入后续计算。这个补偿机制很小,但用户满意度提升明显。
省流模式接管网络后,如果用户明显感知“某App打不开了”,第一反应肯定是把这个功能关掉。所以策略引擎一定要支持“放行单次”操作:用户在弹窗里点“允许一次联网”,本次网络请求直接放行。这个看似细小的交互,能避免很多误拦截冲突。
如果你也准备在OpenHarmony上用Flutter做这类管家型App,我的建议是:先把权限和流量数据准确性搞定,再做省流策略,最后才碰UI。数据地基不稳,后面再漂亮的图表和动画都是徒劳。目前这套架构已经稳定跑了一段时间,后续我们打算把省流规则做成本地配置模板,允许用户按标签批量给应用分组,让规则配置从“专家模式”走向“傻瓜模式”,这又是另一个值得展开的实战话题了。