
前段时间有同事在鸿蒙设备上跑一套 Flutter 项目时碰到一个特别诡异的问题后台接口返回的设备编号在 Android 上显示完全正常换到鸿蒙真机上就末尾几位开始跳动日志里看起来还没有任何异常。排查到最后问题就出在 64 位整数被当作普通 JS 数字跨端流转精度悄悄丢了。做过金融业务、做供应链系统、做物联网设备接入的同学大概率都踩过这种坑。这也是我这次决定把 fixnum 的鸿蒙化适配单独写一篇的原因它不是一个需要改源码的三方库但如果你不知道它的边界在哪会在 64 位算术、溢出安全和跨端通信上吃大亏。fixnum 是 Dart 语言里专门处理 64 位有符号/无符号整数的类库核心提供了 Int64 和 Uint64 两类对象。Flutter 项目跑上鸿蒙以后Dart VM 和原生 ArkTS 侧的数值语义并不完全一致尤其在跨端通信或者 WebView 场景里普通数字很容易退化成 IEEE 754 双精度浮点数。这篇内容适合正在做鸿蒙 Flutter 应用迁移、又需要处理时间戳、金额、位运算、协议字段的开发者我会从为什么丢精度、怎么用 fixnum、怎么在鸿蒙通道里安全传输再到金融级计算实战把思路一次性捋清楚。1. 为什么 Flutter 和鸿蒙一碰到 64 位数字就出问题很多人刚开始不理解Dart 的 int 不也是 64 位的吗鸿蒙的 ArkTS 不也支持数字类型吗为什么还要单独拿一个库出来讲。这里的问题不在于“有没有 int 类型”而在于不同运行环境对数字的表示上限完全不同尤其是跨端通信时会自动走 JSON 或者标准消息编解码一编一解精度就崩了。1.1 前端世界里的“半个整数”在原生 Dart VM 上int 确实是 64 位有符号整数范围可以到 9.22e18 左右。但问题在于Flutter 跑在鸿蒙上不可避免地要和 ArkTS 侧打交道比如 MethodChannel、EventChannel或者通过 JavaScript 引擎做混合渲染。ArkTS 遵循的是 ECMAScript 规范里面只有一种数字类型就是双精度浮点数安全整数范围只有 2 的 53 次方减 1也就是 9007199254740991。超过这个值以后连续整数之间就出现了“空隙”比如 9007199254740992 和 9007199254740993 在双精度浮点里可能被解析成同一个数。把 64 位整数的低 12 位丢掉还算轻的碰上 64 位完整范围末尾几位直接变成随机变化。设备 ID、订单号、时间戳纳秒值、低频通信里的序列号几乎全是重灾区。所以不要把 fixnum 当成一个“增加内存开销的花架子”它真正解决的是环境边界问题不管底层是 Dart VM、Web 引擎还是鸿蒙 ArkTS只要你显式使用 Int64 类型计算和转换都不会因为运行环境不同而改变语义。1.2 ArkTS 与 Flutter 通信时的精度陷阱鸿蒙 NEXT 生态里Flutter 和 ArkTS 之间的通信通常要经过消息序列化。Dart 侧如果直接发一个 int平台侧收到以后往往会转成 JS Number然后再做处理。你以为你传的是 64 位整数实际上对方拿到的是 double。反过来也一样ArkTS 侧返回一个超出安全范围的整数Flutter 侧收到的也不再是准确数值。在这个场景里单纯的“类型转换”解决不了问题。你得在传输协议层面做设计要么把 64 位数字拆成两个 32 位部分要么直接转成字符串按文本传要么用字节数组按固定大小端编码。fixnum 的价值就在于它给了你稳定的二进制表达和解析能力让这些跨端方案有统一的落点。后面我会给出一套可以直接用的传输格式。2. 认识 fixnum它到底给我们封装了什么fixnum 最早是 Google 的 Dart 团队为 Protocol Buffers 准备的因为 protobuf 里的 int64、uint64、sint64 字段必须对应到固定 64 位语义。后来因为太好用很多非 protobuf 项目也拿它来做精确计算。它的核心内容并不复杂但每一层都有讲究。2.1 Int64 和 Uint64 的内部表示不知道为什么很多人以为 fixnum 是把 64 位拆成了两个 32 位 int 来存储。早期版本确实是这个思路但现在的 fixnum 在 Dart VM 上直接用原生 int 存储同时用算法去保证 64 位截断、符号扩展和溢出规则。这样做的优势很明显计算路径短性能比拆字段模拟高得多。对外暴露的就两个核心类Int64有符号 64 位范围从 -9223372036854775808 到 9223372036854775807。Uint64无符号 64 位范围从 0 到 18446744073709551615。这两个类都实现了 Dart 的Comparable、hashCode、operator等接口你可以像使用普通数字一样做加法、减法、乘法、除法、取模、位运算、比较、序列化。唯一要注意的是它们本质上还是对象不能完全替代基础类型。2.2 谁在实际项目里依赖 fixnum最典型的是 protobuf 生成的 Dart 代码。你可以打开一个 Flutter 项目看只要pubspec.yaml里有protobuf依赖几乎一定会带上fixnum。因为.proto文件中的int64字段生成后默认就是Int64类型转换函数里也会调用 fixnum 的方法。第二个典型场景是跨平台文件格式解析。比如你解析数据库文件、日志文件、影像头文件里面经常有 8 字节的长整型字段用 Dart 原生 int 去读 ByteData 很容易出现符号扩展问题用 Int64 配合字节操作就干净很多。第三类就是金融计算。金额字段用分、厘甚至微做单位以后累加、拆账、计息很容易突破 53 位安全整数这时候 fixnum 不是“锦上添花”而是“唯一选择”。3. fixnum 在鸿蒙项目里的适配实操很多人看到“鸿蒙化适配”几个字会本能地觉得要改源码、改编译配置。实际上 fixnum 是纯 Dart 实现不依赖dart:io、不依赖 ffi、不依赖 PlatformView所以它在鸿蒙 Flutter 环境下天然可以编译。真正的适配工作集中在依赖引入、代码替换、跨端传输方案三个层面。3.1 纯 Dart 库为什么能做到零成本迁移这可能出乎你意料但从鸿蒙 Flutter 工具链的角度看只要一个包只用了dart:core级别的 API它就能直接编译进鸿蒙产物。fixnum 正是这种“干净”的三方库它没有原生插件、没有平台通道、没有任何反射调用。所以我在做适配时第一步其实只是把依赖写进pubspec.yamldependencies: flutter: sdk: flutter fixnum: ^1.1.0然后执行flutter pub get跑一遍编译确认鸿蒙构建链路上没有报 MissingPluginException 之类的问题。如果这一步通过了后面就是纯代码层的替换和适配工作了。真正需要花心思的是你项目里到底哪些地方用了可能超过安全范围的整数。我的习惯是先在项目里全局搜索这几种模式DateTime.now().microsecondsSinceEpoch、id.toInt()、接口返回的dynamic转 int、还有ByteData.getInt64的调用点。每一个都值得问一句这个数值会不会超过 9007199254740991。3.2 代码替换从 dart int 到 Int64 的迁移要点替换不是简单地把int改成Int64就结束。Int64是对象不能用、-这些运算符直接和普通 int 混算必须显式调用方法或者通过构造函数转换。我给你一个实战中很好用的替换模板import package:fixnum/fixnum.dart; // 以前写法 int deviceId json[device_id]; int nextId deviceId 1; // 替换写法 Int64 deviceId Int64.parseInt(json[device_id].toString()); Int64 nextId deviceId Int64.ONE;这里的Int64.ONE、Int64.ZERO是库里预定义的常量能省去重复构造对象的开销。在需要和普通 int 混用的地方再用.toInt()转回来但要非常小心如果数值已经超过 53 位安全范围toInt()在 Web 或鸿蒙 ArkTS 侧又会产生误差。更推荐的做法是金融计算全程用 Int64所有加法、乘法、比较都不落回 int只有到了展示层才把 Int64 转成字符串交给 UI 渲染。转字符串这一步没有精度损失而且显示的效果也更符合金融业务要求。3.3 跨端传输方案鸿蒙和 Flutter 之间怎么传 64 位数字这是整个适配里最有含金量的一部分。如果你的 Flutter 鸿蒙应用只是单端跑 Dart 逻辑不跟 ArkTS 通信那 fixnum 的作用主要是防内部溢出。但现实情况是很多应用要接入鸿蒙的推送、支付、安全能力或者要从原生侧拿设备信息跨端传 64 位数字几乎是绕不开的。我们团队最后沉淀出一套比较稳的传输格式核心原则是绝对不在跨端 JSON 里传超过 2^53 的数字对象统一用字符串或字节数组。如果你走 MethodChannel我推荐字符串方案因为可读性好、排查方便// Dart 侧发送 final Int64 value Int64.parse(123456789012345678); final MapString, Object args {value: value.toString()}; await channel.invokeMethod(sendInt64, args);ArkTS 侧接收以后用BigInt(value as string)转成 ArkTS 的 bigint 处理。反过来ArkTS 侧要回传大整数时也先转字符串// ArkTS 侧返回 let big BigInt(123456789012345678); return { value: big.toString() };如果你要传高频数据比如每帧都有时间戳字符串方案会有额外的编解码开销。这种场景建议用固定 8 字节的字节数组Dart 侧按大端序写入ArkTS 侧用 DataView 读取。两种方案各有利弊方案优点缺点适用场景字符串可读性好、跨语言兼容性最强编解码有开销、数据量变大低频调用、接口调试字节数组体积小、解析快、无语义歧义需要统一字节序、可读性差高频流、协议字段传输拆两个 int32兼容老旧 JSON 结构容易漏转换、代码冗余老系统接口改造无论选哪种我都建议在 Dart 侧封装一个工具类把 Int64 的序列化和反序列化收敛到同一个文件里。这样以后一旦要换传输方案只改一个文件。4. 溢出安全与金融级高精度计算实战fixnum 这个名字听起来很底层但它在业务代码里最大的价值其实是“兜底”。现代金融系统、电商计费系统、供应链结算系统只要有一行代码没考虑到溢出安全线上就有可能出现金额对不上的事故。下面我会用几个实操场景把溢出安全和位运算这部分讲透。4.1 先看一次典型的溢出事故是怎么发生的假设你在做积分系统用户积分余额来自服务端类型是 int64。Dart 侧拿到以后默认存成int在 Android 原生 VM 上跑没问题因为底层确实能表示 64 位。但只要你把积分值传给鸿蒙的 ArkTS 侧做一次动画展示或者经过一次 JavaScript 引擎数值就可能开始“漂移”。更隐蔽的是本地计算溢出。比如你写int totalMicros duration.inMicroseconds * count;如果count比较大duration.inMicroseconds又在千万级别乘积完全可能超过 2^53。单看 Dart VM 还是 safe因为 64 位 int 还没满但一旦这个值被 JSON 序列化或者被某个只认双精度浮点的组件消费精度就开始坏了。这种 bug 特别难复现因为你本地调试可能没事到了真机或者特定数据量级就偶现。用了 fixnum 以后你会强制自己把所有可能超界的整数都放进 Int64 的语义里。Int64 * Int64的结果会做 64 位截断符号位、溢出位都按照补码规则走。虽然生产环境里我们不希望溢出真的发生但至少行为是确定的。4.2 金融计算里 Int64 的标准写法在金融项目里金额的常规做法是用最小货币单位。人民币就是分如果涉及更细的计价有的系统会用到厘甚至微。假设你做一个年化收益计算本金是 1000000.00 元存成分为Int64(100000000)年化利率是 3.65%你按天计息一天的利息就是Int64 capital Int64(100000000); // 单位分 Int64 annualRateBp Int64(365); // 3.65% 用万分之 365 表示 Int64 days Int64(1); // 利息 本金 * 年利率 / 365 / 10000注意放大倍数防丢失 Int64 interest (capital * annualRateBp * days) ~/ Int64(3650000);这里有个细节中间计算会先放大再缩小如果不小心先除以 10000 再乘 365整数除法直接丢掉小数部分金额就差出来了。我建议所有利率、费率都用“万分之”“十万分之”这种定标整数表示整个链路都用 Int64最后一步再换算成 UI 展示字符串。封装展示层时可以这样保留精度String formatFenToYuan(Int64 fen) { final yuan fen ~/ Int64(100); final jiao (fen % Int64(100)) ~/ Int64(10); final li fen % Int64(10); return $yuan.$jiao$li; }用 Int64 做完除法和取模以后toString()不会出现浮点尾巴也不会有科学计数法。这一点在账单明细、报表导出、对账系统里尤其重要。4.3 位运算和位移的正确姿势位运算场景里fixnum 同样提供了和 C 语言一致的行为。shl、shr、and、or、xor这些方法都有使用时要注意操作数类型。比如你要从一个 uint64 字段里取出高 32 位和低 32 位Int64 value Int64.parse(0x1234567890ABCDEF); Int64 high value Int64(32); Int64 low value Int64(0xFFFFFFFF);这里的运算符是 fixnum 重载过的移位位数要用 Int64 类型不能直接传普通 int。这一点最容易被新手忽略编译器通常也会给提示但如果你写了一堆二进制的标志位判断可能一时半会儿注意不到。如果你要处理的是无符号语义记得使用Uint64。例如解析网络协议里的长度字段无符号数最高位不是符号位如果错误地用 Int64 去解释超过 2^63 的数值会变成负数后面所有比较逻辑都乱掉。5. 我在鸿蒙化适配过程中踩过的坑这部分是我最想分享的。很多坑不是 fixnum 本身的问题而是鸿蒙 Flutter 混编环境带来的。下面几条经验每条都是我花实际时间填出来的。5.1 随手 toInt() 导致精度二次丢失我见过最典型的错误是费了半天劲把数据转成 Int64算完以后又因为“要返回给原生”或者“要传给某个旧模块”直接.toInt()。如果这个值已经超过 53 位安全整数这一步就把修复全毁了。凡是要跨端的数据一律不落回 int保持 Int64 或者 String。实在要转回普通 int先判断value Int64(9007199254740991)超出就直接抛异常或走字符串方案绝不做静默截断。5.2 字节序不对导致数据全乱字节数组方案里Dart 侧默认用大端序写高位在前如果 ArkTS 侧用了小端序去读传出来的数完全是另一个值。我建议统一在注释里写明“Big Endian”并且出一个 8 字节的黄金测试用例比如0x0102030405060708两边打通以后再跑业务数据。我在实际项目里专门写了一个发送端测试方法把固定字节发过去ArkTS 侧转成字符串回传两边比对原值。这套验证流程不是可有可无是每天的 CI 都要跑。5.3 日志里出现 E/flutter 31173 之类的报错鸿蒙 Flutter 跑起来以后日志系统有时会输出和 Dart VM 初始化相关的错误比如E/flutter (31173)或dart_vm_initializer.cc开头的信息。很多人一看到就以为和 fixnum 有关其实大部分是引擎侧的基础环境问题或者是某个插件在鸿蒙上没找到原生实现。排查思路是先把 fixnum 相关的代码注释掉看错误是否消失如果还在说明问题在插件注册或引擎配置链路。千万不要因为一个错误码本身长得吓人就怀疑库有问题。fixnum 是纯 Dart 逻辑它本身不产生这种引擎级日志。5.4 大数字在 JSON 里被科学计数法显示有些日志系统会把 double 自动转成科学计数法比如1.2345678901234568e18一眼看过去完全不像整数。此时别慌先确认数据是不是已经从 Int64 变成 double再查是不是在 JSON 序列化之前做了隐式转换。我建议所有接口模型里64 位字段都用String或者Int64接收不要用dynamic。用dynamic意味着把类型判断交给了运行时的实现细节这在鸿蒙混合环境里等于放弃精度控制权。6. 性能优化什么时候该用什么时候别用虽然 fixnum 用起来很好但它不是“银弹”。Int64 是对象每次运算都会产生新的实例在高频循环里如果滥用GC 压力和内存分配量都会上去。我的经验是金额、费率、ID、位数等业务核心字段用 Int64安全第一。计数器、数组下标、短循环里的临时变量用原生 int性能第一。高频发送的时间戳优先用 Int64 固定格式但避免在循环里反复parse和toString。如果你要连续处理十万个 Int64 的加法建议把数据放进列表后一次性批量计算或者提前.toInt()到安全范围内再算。从实际压测看fixnum 的四则运算比原生 int 慢一个数量级左右但业务系统里大部分场景不是计算密集型慢的那点时间完全可以接受。真正的性能杀手是你为了“保险”而把每一个数字都包一层 Int64导致整个链路里创建了大量临时对象。再补充一个优化点Int64.parseInt在解析超长字符串时相对较重如果你能从底层拿到字节数组优先用Int64.fromBytes或者先读取 ByteData 再构造。这种写法在解析二进制协议时能省掉一次字符串拷贝。7. 把 fixnum 的适配能力沉淀成团队规范说到最后我想强调一个观点fixnum 的鸿蒙化适配不是靠一两个文件改完就结束的它应该变成团队里的约定。我在项目里会要求所有涉及外部接口的 64 位字段都使用明确类型禁止直接写裸int接大数字跨端传输禁止直接塞数字必须走字符串或字节数组封装所有金额字段默认以最小单位 Int64 存储展示层统一格式化。另外我会在代码评审里专门检查一种反模式把 Int64 和 int 混合运算却不做转换。很多编译器只能做类型提示不会真的帮你拦下所有精度损失。要是团队里有人图省事直接在金融模型里用 double 做了除法问题不会马上暴露但数据一旦去重对账绝对够喝一壶的。我自己在实际适配中的体会是fixnum 这个库本身不需要什么高深改造真正的挑战在于识别出鸿蒙生态里那些隐式的数值降级点。你只要把每一处跨端、跨引擎、跨 JSON 的 64 位数据都纳入管理用统一封装收口后面的业务代码反而会变得非常清爽。等这样做过一轮以后你会发现自己已经成了团队里最懂鸿蒙底层数值细节的那个人这也是标题里“鸿蒙级底层数值专家”的真正含义。