react-native-quick-md5 提供两个同步的 MD5 函数,用于文件校验、缓存键、内容指纹这类场景。它和"普通 RN 库"有两处明显不同:

  1. 它是 JSI 实现——TurboModule 只暴露一个同步的 install(),作用是往 JS 运行时装一个 C++ 函数;之后调用 MD5 完全不经过桥,也不经过 ArkTS;
  2. MD5 算法本身就是 C++ 写的——适配时原样复用了上游的算法源码,而不是重新实现一遍。

这两点决定了适配和验证的方式。本文会讲清:怎么识别"JSI 类"原生实现、install() 这条链路怎么工作、两个公开入口为什么对中文文本会给出不同结果、以及确定性算法该怎么验才可信。

环境准备:本文不重复环境搭建步骤。RNOH(React Native for OpenHarmony)开发环境的完整配置见官方开发者指南:
https://atomgit.com/CPF-RN/docs/blob/main/开发者指南/02-搭建准备/环境初始化.md

在这里插入图片描述


一、先说结论

项结论
需要原生适配吗✅ 需要
适配要补什么一个鸿蒙原生模块(HAR + C++ TurboModule),并把上游的 MD5 算法源码一起带上
补的体量C++ 里含 vendored 的上游算法(md5.cpp 约 11 KB);HAR 7.3 KB
需要权限吗❌ 不需要
公开 API2 个,都是同步:stringMd5(string)、binaryMd5(string | ArrayBuffer)
返回值32 位小写十六进制字符串

二、判定过程:怎么确认必须做原生适配、以及是哪一类

第一步:看 package.json 有没有 harmony 字段

npm view <包名> harmony --json

有 harmony.autolinking(ohPackageName / etsPackageClassName / cppPackageClassName / cmakeLibraryTargetName 四个名字)的,一定是带原生实现的适配包。

第二步:看上游包里有哪些平台的实现

$ npm pack react-native-quick-md5@3.0.9
$ tar -xzf react-native-quick-md5-3.0.9.tgz
$ ls package
android/  ios/  cpp/  lib/  ...

有 android/、ios/,没有 harmony/ → 必须补一个鸿蒙实现。

第三步:读 JS 入口,判断是哪一类原生实现

// src/index.ts(交付版)
const runtime = globalThis as ... {md5FromArrayBuffer?: (data: string | ArrayBuffer) => string};
if (!NativeQuickMd5.install() || typeof runtime.md5FromArrayBuffer !== 'function') {
  throw new Error('react-native-quick-md5: native JSI installation failed');
}
const nativeMd5 = runtime.md5FromArrayBuffer;

这里出现了一个关键信号:原生模块暴露的不是业务方法,而是一个 install(),它往 globalThis 上装函数。这是 JSI 的典型形态——把 C++ 函数直接注册进 JS 运行时,调用时不走桥。

"选库阶段"能区分出三类原生实现:

形态特征适配时要注意
ArkTS TurboModuleJS 侧 getEnforcing('X'),方法返回 Promise,C++ 用 ARK_ASYNC_METHOD_METADATA方法名/参数个数要对齐;异步
纯 C++ TurboModuleC++ 继承 rnoh::TurboModule,方法实现全在 C++日志标签不同(见第五节)
JSI原生暴露 install(),把函数挂到 globalThis同步调用;安装失败要显式处理

本库属于第三类,并且它的 TurboModule 是**纯 C++**的(class QuickMd5 : public TurboModule),不是 ArkTSTurboModule。

三、适配实现:install() 这条链路

harmony/quick_md5/ 的结构:

harmony/quick_md5/
├── Index.ets                                   # 导出 QuickMd5Package
├── oh-package.json5                            # 声明包名 @react-native-ohos/react-native-quick-md5
├── build-profile.json5
└── src/main/
    ├── module.json5
    ├── cpp/
    │   ├── CMakeLists.txt
    │   ├── QuickMd5Package.h                   # Package + TurboModule 工厂
    │   ├── QuickMd5Package.cpp                 # ★ install() 的实现
    │   ├── QuickMd5Digest.h                    # ★ 分块喂给算法的包装
    │   └── upstream/                           # ★ 原样保留的上游 MD5 算法
    │       ├── md5.cpp
    │       └── md5.h
    └── ets/
        └── QuickMd5Package.ets                 # 只提供自动链接入口,不承担哈希运算

install() 做了什么

methodMap_["install"] = {0, [](jsi::Runtime& runtime, facebook::react::TurboModule&,
    const jsi::Value*, size_t count) -> jsi::Value {
  if (count != 0) throw jsi::JSError(runtime, "QuickMd5.install expects no arguments");
  auto function = jsi::Function::createFromHostFunction(runtime,
      jsi::PropNameID::forAscii(runtime, "md5FromArrayBuffer"), 1,
      [](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t size) -> jsi::Value {
        if (size != 1) throw jsi::JSError(rt, "MD5 expects one string or ArrayBuffer");
        std::string result;
        if (args[0].isString()) {
          const auto text = args[0].getString(rt).utf8(rt);
          result = quick_md5::digest(reinterpret_cast<const uint8_t*>(text.data()), text.size());
        } else if (args[0].isObject() && args[0].getObject(rt).isArrayBuffer(rt)) {
          const auto buffer = args[0].getObject(rt).getArrayBuffer(rt);
          result = quick_md5::digest(buffer.data(rt), buffer.size(rt));
        } else {
          throw jsi::JSError(rt, "MD5 expects one string or ArrayBuffer");
        }
        return jsi::String::createFromUtf8(rt, result);
      });
  runtime.global().setProperty(runtime, "md5FromArrayBuffer", std::move(function));
  return true;
}};

三件事:

  1. 创建一个 host function(C++ 实现、JS 侧表现为普通函数);
  2. 把它挂到 globalThis.md5FromArrayBuffer;
  3. 参数类型在 C++ 侧就分流:string 走 JSI 的 UTF-8 解码,ArrayBuffer 拿原始内存指针。

注意 ArrayBuffer 那条分支:buffer.data(rt) 直接给出内存指针,没有拷贝。这也是"quick"的来源——大缓冲区的摘要不需要先把字节搬过桥。

一个容易忽略的细节:1 MiB 分块

inline std::string digest(const uint8_t* bytes, size_t length) {
  MD5 value;
  // The upstream update length is 32-bit; feed bounded blocks without copying the ArrayBuffer.
  while (length > 0) {
    const auto size = static_cast<MD5::size_type>(std::min(length, size_t{1024 * 1024}));
    value.update(bytes, size);
    bytes += size;
    length -= size;
  }
  return value.finalize().hexdigest();
}

上游 update() 的长度参数是 32 位,大缓冲区传进去会截断。用 1 MiB 分块规避,而且不复制数据(只是把指针往前推)。

这个包装层只有 20 行,但它解决的是一个真实存在、且只有在大输入下才会暴露的问题。本次验证特意覆盖了 1 MiB、1 MiB+1、2 MiB+17 三个尺寸。

算法源码是原样复用的

cpp/upstream/md5.cpp / md5.h 字节未改,来自上游仓库(RSA Data Security 的 MD5 算法 + Frank Thilo 的 C++ 转换)。交付包里有 THIRD_PARTY_NOTICES.md 专门说明来源,并明确一句:

The root MIT LICENSE covers react-native-quick-md5; it does not replace the notices embedded in the algorithm sources.

——根许可证不替代算法源码里内嵌的声明。这种把第三方许可边界写清楚的做法值得学:把别人的算法源码放进自己的仓库时,"我用了它"和"它的许可怎么算"是两件事。

不提供降级路径

上游在有 JSI 的环境失败时会退回纯 JS 的 SparkMD5。交付版去掉了这条降级:

if (!NativeQuickMd5.install() || typeof runtime.md5FromArrayBuffer !== 'function') {
  throw new Error('react-native-quick-md5: native JSI installation failed');
}

在模块初始化阶段就抛错,而不是悄悄用一个"结果可能不同"的 JS 实现顶上。对摘要类接口来说这是对的选择——摘要不一致比直接报错危险得多。

四、两个公开入口的语义差异

这是本库最容易踩的地方。

export function stringMd5(data: string): string {
  if (typeof data !== 'string') throw new TypeError('stringMd5 expects a string');
  return nativeMd5(data);
}

export function binaryMd5(data: string | ArrayBuffer): string {
  if (typeof data === 'string') {
    const bytes = new Uint8Array(data.length);
    for (let index = 0; index < data.length; index++) bytes[index] = data.charCodeAt(index);
    return nativeMd5(bytes.buffer);
  }
  if (!(data instanceof ArrayBuffer)) throw new TypeError('binaryMd5 expects a binary string or ArrayBuffer');
  return nativeMd5(data);
}
调用语义
stringMd5(text)按 UTF-8 编码后求摘要
binaryMd5(arrayBuffer)按实际字节求摘要
binaryMd5(text)按每个 UTF-16 代码单元的低 8 位当字节

第三行是关键:Uint8Array 赋值会把值截断到 8 位,所以"低 8 位"就是 charCodeAt(i) & 0xff——不是 UTF-8。

实测确认两者在非 ASCII 下必然不同:

stringMd5('鸿蒙 RN') = <UTF-8 摘要>
binaryMd5('鸿蒙 RN') = <低 8 位摘要>     ← 与上一行不同

而纯 ASCII 与空串下两者相同(ASCII 的 UTF-8 编码就等于其低 8 位;空串都走空字节)。

实践含义:用 binaryMd5 处理中文文本会得到"另一套"摘要,它不会报错。要算 UTF-8 文本的 MD5,就用 stringMd5。

ArrayBuffer 与视图

Uint8Array 等视图不是合法输入类型——只接受 string 或 ArrayBuffer。要算缓冲区的一部分,必须显式切片:

const bytes = new Uint8Array([9, 10, 11, 12]);
const view = bytes.subarray(1, 3);
// ❌ binaryMd5(view.buffer)        —— 会包含整个底层缓冲区 [9,10,11,12]
// ✅ 显式切片
const part = binaryMd5(view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength));

这是一个很容易静默算错的地方:view.buffer 返回的是整个底层缓冲区,不是视图范围。传错了不会报错,只会算出一个"看起来很正常的"错误摘要。

五、接入宿主:三处改动面(外加一处自动生成的)

库本身不能独立运行,必须有一个 RNOH 宿主 App。这里用 RNOH084Demo(RNOH 0.84.3 的多库验证宿主),它自带 rnAppKey 机制,一个宿主可以挂很多独立测试页:

hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey QuickMd5TestApp

接入要改的地方

第一处:package.json。

"react-native-quick-md5": "file:../react-native-quick-md5"

第二处:两级 oh-package.json5 都要写 HAR。

"@react-native-ohos/react-native-quick-md5":
  "file:../node_modules/react-native-quick-md5/harmony/quick_md5.har",

harmony/oh-package.json5 管工程级、harmony/entry/oh-package.json5 管模块级,两处都要加。只加一处会出现"能找到包但链接不上"。

这里有个很容易漏的点:跑 link-harmony 时,它只会自动更新工程级那一份,模块级那份要你自己加。

第三处:在 ETS 侧注册 Package。

// harmony/entry/src/main/ets/RNOHPackagesFactory.ets
import type { RNPackageContext, RNOHPackage } from '@rnoh/react-native-openharmony';
import QuickMd5Package from '@react-native-ohos/react-native-quick-md5';

export function createRNOHPackages(ctx: RNPackageContext): RNOHPackage[] {
  return [
    new QuickMd5Package(ctx),
  ];
}

代码写在哪,这里说清楚:手工改动面就是这三个文件(外加 metro.config.js 的 watchFolders)。C++ 侧不用手改——CAPI 架构下 PackageProvider.cpp 会自动消费 autolinking 生成的 RNOHPackagesFactory.h。

那"自动生成的一处"是什么? 执行 link-harmony 时,它会一次性重写这四个文件:

• harmony/entry/src/main/cpp/RNOHPackagesFactory.h   # C++ 侧注册
• harmony/entry/src/main/cpp/autolinking.cmake       # add_subdirectory + 链接
• harmony/entry/src/main/ets/RNOHPackagesFactory.ets # ETS 侧注册
• harmony/oh-package.json5                           # 工程级 HAR 依赖

这四个文件头部都写着 DO NOT modify it manually, your changes WILL be overwritten.——别手改。

抓日志时的坑:这个库的日志标签不一样

纯 C++ TurboModule 的创建日志不在 ArkTS 模块那条通道上:

#RNOH_CPP: TurboModuleFactory.cpp:54> Creating Turbo Module: QuickMd5

而 ArkTS 实现的 TurboModule 是:

#RNOH_ARK: RNInstance::TurboModuleProvider  TM created: <名字>

用 grep 'TM created' 抓这个库会一无所获。 改用:

hdc shell "hilog -x | grep -i 'Creating Turbo Module'"
hdc shell "hilog -x | grep -i 'quickmd5'"

六、构建与运行

# 1) 装 JS 依赖 + 自动链接
npm install
./node_modules/.bin/react-native link-harmony

# 2) 生成调试签名 + 装 ohpm 依赖
cd harmony
devecocli signature generate
ohpm install --all

# 3) 打包 JS bundle(输出到 harmony/entry/src/main/resources/rawfile/)
cd ..
npm run dev

# 4) 编译 HAP
cd harmony
hvigorw --mode module -p product=default -p module=entry@default assembleHap --no-daemon

# 5) 安装 + 启动测试页
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
# 换页参数只在「冷启动」时生效:先 force-stop 再起
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey QuickMd5TestApp

耗时:在已有原生编译缓存的宿主上增量加入这个库,assembleHap 用了 6 分 33 秒;HAP 从 80.39 MB 涨到 80.71 MB(约 +320 KB,比纯 ArkTS 的库多——因为要编译 C++ 算法)。

如果宿主是全新 clone(没有原生编译缓存),首次构建会到 30–40 分钟量级;只改 JS 重新打包也要 6–7 分钟。

按文本点击(页面高度会随结果卡片变化,别记固定坐标):

. E:\rnoh-work\click-label.ps1
Click-Label -Pattern '跑全部四组断言'

七、验证设计:确定性算法怎么验才可信

MD5 是确定性算法——同样的输入永远得到同样的输出。这既让验证变得容易(可以逐字节对已知值),也意味着**"调用没报错"完全没有意义**。

验证要回答三个层次的问题:

层次问题怎么做
算法正确性算出来的摘要对不对已知值断言:RFC 1321 标准向量 + PC 端用 Node 独立算出的期望值
边界正确性在填充临界点、大输入下还对不对覆盖 MD5 的填充边界(56 字节附近)与 1 MiB 分块边界
接口契约语义、类型、错误处理对不对两个入口的语义差异、同步返回、非法输入拒绝、输入不被修改

期望值从哪来

不是从被测库拿的,而是 PC 端 Node.js 的 crypto 现场生成:

const md5 = bytes => crypto.createHash('md5').update(bytes).digest('hex');
const md5Utf8 = text => md5(Buffer.from(text, 'utf8'));

生成 33 行向量、展开成 37 项断言。大缓冲区用同一个确定性公式在两端生成同样的字节:

bytes[i] = (i * 31 + 7) % 256

A 组的向量构成

类别条数说明
RFC 1321 §A.5 标准向量7"" / "a" / "abc" / "message digest" / 字母表 / 字母数字 / 80 位数字串
UTF-8 文本9中文、emoji、内嵌 NUL、孤立高/低代理项、组合字符与预组合字符、1000 字符长文本
低 8 位路径4含代理对与中文的字符串
UTF-8 对照4同一批字符串走 stringMd5
填充边界100 / 1 / 55 / 56 / 63 / 64 / 65 / 127 / 128 / 129 字节
大缓冲区31 MiB / 1 MiB+1 / 2 MiB+17

两处边界值得单说:

  • 56 字节是 MD5 的填充临界点(消息长度 ≡ 56 mod 64 时不需要额外填充块),把它和 55、63、64、65 一起测,能覆盖填充逻辑的所有分支;
  • 1 MiB 是实现自己的分块边界,用 1 MiB、1 MiB+1、2 MiB+17 三个尺寸卡它。

实测结果:65 / 65 全部通过

组内容断言数结果
A已知值(RFC 1321 / UTF-8 / 低 8 位 / 填充 / 1 MiB 边界)37✅ 37/37
B两个入口的语义差异4✅ 4/4
CArrayBuffer 路径(切片 / 修改后重读 / 输入不被修改)5✅ 5/5
D契约与错误(同步 / 格式 / 全局函数 / 状态独立 / 非法输入)19✅ 19/19
合计65✅ 65/65

在这里插入图片描述

具体值样例

stringMd5('abc')      = 900150983cd24fb0d6963f7d28e17f72
stringMd5('')         = d41d8cd98f00b204e9800998ecf8427e
stringMd5('鸿蒙 RN')  = <UTF-8 摘要>
binaryMd5('鸿蒙 RN')  = <低 8 位摘要>   ← 与上一行不同

md5("abc") 与 md5("") 的值与 RFC 1321 附录一致,也与交付 README 的示例一致——三方对得上。

在这里插入图片描述

C 组的三个关键点

  1. 视图切片:view.buffer 是整个底层缓冲区,必须显式 slice(byteOffset, byteOffset + byteLength) 才是视图范围。实测切片后的摘要匹配 Node 算出的 md5([10, 11]),且与整缓冲的摘要不同;
  2. 修改后重读:改一个字节再算,摘要会变(没有缓存首次结果);
  3. 不改动输入:算完逐字节核对原数组未变。

D 组的契约项

  • stringMd5 同步返回 string(不是 Promise);
  • 返回值匹配 /^[a-f0-9]{32}$/(32 位小写十六进制);
  • globalThis.md5FromArrayBuffer 确实是 function(JSI 安装成功);
  • 计算独立性:交错调用 50 次(每次先算 item-N 再算空串),空串摘要始终一致——说明每次调用创建独立上下文,没有状态残留;
  • 14 种非法输入(undefined / null / 数字 / 布尔 / 空对象 / 空数组 / Uint8Array 视图 × 两个入口)全部抛 TypeError;
  • 非法输入之后仍能正常计算。

八、真机验证

验证环境:Pura X View 模拟器,HarmonyOS 7.0.0(26.0.0) Beta2,API 26,ohos-x64。

原生模块创建

#RNOH_CPP: TurboModuleFactory.cpp:54> Creating Turbo Module: QuickMd5

完整读数

[quick-md5-test] A. 已知值(RFC 1321 / UTF-8 / 低8位 / 填充 / 1MiB 边界) -> 37/37 全部通过
[quick-md5-test] B. 两个入口的语义差异(UTF-8 vs 低 8 位) -> 4/4 全部通过
[quick-md5-test] C. ArrayBuffer 路径(切片 / 修改 / 不改输入) -> 5/5 全部通过
[quick-md5-test] D. 契约与错误(同步 / 格式 / 独立性 / 非法输入) -> 19/19 全部通过

能力对照

能力结果
stringMd5 UTF-8 摘要✅ 含中文 / emoji / 内嵌 NUL / 孤立代理项,全部匹配 Node
binaryMd5(ArrayBuffer)✅ 含填充边界与 1 MiB 分块边界
binaryMd5(string) 低 8 位语义✅ 与 UTF-8 路径的结果确实不同,且各自匹配 Node
同步契约✅ 直接返回字符串,不是 Promise
JSI 安装✅ globalThis.md5FromArrayBuffer 为 function
计算独立性✅ 交错调用 50 次无状态残留
非法输入✅ 14 种全部拒绝,之后仍能正常计算
输入不被修改✅
权限✅ 不需要

九、已知限制

一、两个字符串入口语义不同,容易用错。 stringMd5 走 UTF-8;binaryMd5(string) 走"每个 UTF-16 代码单元的低 8 位"。用 binaryMd5 处理中文文本不会报错,但会得到另一套摘要。需要 UTF-8 语义时用 stringMd5。

二、Uint8Array 等视图不是合法输入。 只接受 string 或 ArrayBuffer;而且 view.buffer 是整个底层缓冲区,切片必须显式 slice(byteOffset, byteOffset + byteLength),否则会静默算错范围。

三、MD5 本身不适合安全用途。 MD5 早就不抗碰撞,不要用于密码存储、数字签名或防篡改判断。它只适合"兼容既有摘要"或"非对抗性的内容校验"(比如缓存键、去重)。真要用在这个场景,应考虑换更强的摘要算法——但本库只提供 MD5。

四、没有 JS 降级路径。 上游在缺 JSI 时会退回 SparkMD5;本交付去掉了这条路径——原生安装失败时在模块初始化阶段直接抛错。这是有意的(摘要不一致比报错危险),但意味着某些调试环境可能起不来。

五、同步调用会阻塞 JS 线程。 两个 API 都是同步的,大输入(比如几百 MB 的文件)会卡住 JS 线程。实现用 1 MiB 分块规避的是长度截断,不是性能问题——分块不等于异步。

六、本次只验证到 2 MiB + 17 字节。 极大内存分配、已分离(detached)的 ArrayBuffer、worker 运行时、引擎热重载都未测试。

七、没有做原生速度基准。 交付报告自己就写明"耗时含验证页的数据准备与完整性检查,不能作为原生算法性能基准"。本次同样只验证正确性。

八、未在真机上验证,也未做跨平台一致性对照。 只在模拟器上跑;iOS/Android 侧没有对照环境,只在 PC 端用 Node 的 MD5 做了算法级对照。

九、其他 ROM 未验证。 适配方记录的是 OpenHarmony-7.0.0.105。

顺带说一处这个交付包做得好的地方:它的 spec.json 记录了上游仓库与基线 commit,validation 明细到了确定性值数量、非法调用次数、截图数和 HAP 的 SHA-256;有独立的 THIRD_PARTY_NOTICES.md 说明 vendored 算法的许可边界;契约测试是真的 import 了库(用 vm 注入 mock),其中一组还会编译上游 C++ 并与 Node 比对。这些都是"可追溯、可复现"该有的样子。

十、常见问题

Q:这个库为什么必须做原生适配?
A:它是 JSI 实现:原生侧暴露一个 install(),作用是把 C++ 函数挂到 globalThis 上,真正的 MD5 运算在 C++ 里。上游只提供了 iOS 与 Android 实现,没有鸿蒙实现,所以必须补一个鸿蒙原生模块(连同上游的算法源码)。

Q:怎么判断一个库是 JSI 类实现?
A:看 JS 入口——如果它调用原生的 install()、然后从 globalThis 上取一个函数来用,而不是直接调用原生模块的方法,那就是 JSI。这类实现的调用是同步的,不走桥。

Q:为什么 grep 'TM created' 抓不到这个库?
A:因为它是纯 C++ TurboModule,创建日志在另一条通道上:#RNOH_CPP: TurboModuleFactory.cpp:54> Creating Turbo Module: QuickMd5。而 ArkTS 实现的模块才是 #RNOH_ARK: ... TM created: <名字>。

Q:stringMd5 和 binaryMd5 有什么区别?为什么同一个中文串结果不一样?
A:stringMd5 按 UTF-8 编码;binaryMd5 对字符串按每个 UTF-16 代码单元的低 8 位当字节(和上游一致)。中文的码元远大于 255,两种编码完全不同,所以摘要必然不同。算 UTF-8 文本的摘要就用 stringMd5。

Q:为什么我传 view.buffer 得到的摘要不对?
A:因为 view.buffer 是整个底层缓冲区,不是视图范围。必须显式切片:

binaryMd5(view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength));

直接传 view.buffer 不会报错,只会算出一个"看起来正常"的错误摘要。

Q:返回值是 Promise 吗?
A:不是,两个 API 都是同步返回字符串的。这也是 JSI 的特点——同步调用、不经过桥。

Q:大文件能用吗?会阻塞吗?
A:会阻塞。实现用 1 MiB 分块是为了规避上游 update() 的 32 位长度截断,不是异步。超大输入会卡住 JS 线程,业务侧要自己考虑分片或放到 worker 里(但 worker 运行时未在本次验证范围内)。

Q:为什么这个库没有"原生不可用时退回纯 JS"的兜底?
A:交付版去掉了上游的 SparkMD5 降级,安装失败直接抛错。这是有意的设计:摘要类接口最怕的是"悄悄换了一套实现、结果不一致",宁可启动就报错。代价是某些调试环境可能起不来。

Q:怎么验证一个摘要算法库是真的对?
A:三层。① 算法层:用标准文档官方向量(MD5 是 RFC 1321 §A.5 的 7 条),任何人可核对;② 规模化:PC 端用等价实现(如 Node 的 crypto)现场生成一批期望值,覆盖 UTF-8、二进制、边界长度;③ 边界层:填充临界点(56 字节附近)与实现自己的分块边界(1 MiB)。本次合计 65 项断言,全部逐字节比对十六进制输出。

Q:为什么我在模拟器上编译要这么久?
A:宿主已有原生编译缓存时,增量加一个库约 6–7 分钟;这个库因为要编译 C++ 算法,HAP 增量约 320 KB(比纯 ArkTS 的库大)。全新克隆的宿主首次编译要 30–40 分钟。只改 JS 重新打包也是 6–7 分钟。

小结

这个库有三层值得记:

第一层是"JSI 类"原生实现的识别。 它和常见的 ArkTS TurboModule 不一样:原生只暴露一个 install(),作用是把 C++ 函数挂到 globalThis 上。识别出来之后,很多事就顺了——调用是同步的、日志标签不同(Creating Turbo Module 而不是 TM created)、ArrayBuffer 的内存是直接给 C++ 用的。判断"这是哪一类原生实现",比笼统地知道"它需要适配"更有用。

第二层是那两个入口的语义差异。 stringMd5 走 UTF-8,binaryMd5(string) 走"每个 UTF-16 代码单元的低 8 位"。同一个中文字符串,两个入口给出不同摘要,而且都不报错。 这类"两种都合法、但语义不同"的接口,文档写得再清楚也总有人踩——验证时把它们的结果并排放出来,比文字描述有效。

第三层是确定性算法该怎么验。 MD5 输出确定,所以"跑通"毫无意义,得逐字节对已知值。而期望值本身要有可信来源:

  • 先用标准文档官方向量(RFC 1321 §A.5 的 7 条)——这是任何人都能核对的锚点;
  • 再用 PC 端等价实现批量生成期望值,覆盖 UTF-8 / 二进制 / 各种长度;
  • 最后卡两类边界:算法自己的填充临界点(56 字节),和实现自己引入的边界(1 MiB 分块)。

第三条最容易被忽略:适配时新增的任何包装逻辑,都引入了新的边界。1 MiB 分块是这版适配为了规避上游 32 位长度截断而加的,那它就必须被单独验证——否则"分块"这个动作本身可能悄悄算错,而且只在输入超过 1 MiB 时才暴露。


本篇用到的库

项内容
三方库react-native-quick-md5(上游 3.0.9 的鸿蒙适配版)
交付仓库https://atomgit.com/oh-react-native/react-native-quick-md5
适配 TAG3.0.9-ohos-1.0.0
ohpm 包名@react-native-ohos/react-native-quick-md5
HARharmony/quick_md5.har(7.3 KB)
原生模块名QuickMd5
是否需要权限不需要
上游仓库https://github.com/craftzdog/react-native-quick-md5(基线 commit 18cd4f176f3cfe839d01f4e138067a7691d99cc8)
宿主工程RNOH084Demo(测试页 rnAppKey = QuickMd5TestApp)
"react-native-quick-md5": "git+https://atomgit.com/oh-react-native/react-native-quick-md5.git#3.0.9-ohos-1.0.0"
// harmony/oh-package.json5 与 harmony/entry/oh-package.json5 都要加
"@react-native-ohos/react-native-quick-md5":
  "file:../node_modules/react-native-quick-md5/harmony/quick_md5.har",
import {binaryMd5, stringMd5} from 'react-native-quick-md5';

// UTF-8 文本摘要(同步返回)
const text = stringMd5('abc');                       // 900150983cd24fb0d6963f7d28e17f72

// 实际字节摘要
const binary = binaryMd5(new Uint8Array([0, 1, 127, 128, 255]).buffer);

// 切片:必须显式切,直接传 view.buffer 会包含整个底层缓冲区
const bytes = new Uint8Array([9, 10, 11, 12]);
const view = bytes.subarray(1, 3);
const part = binaryMd5(view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength));

// 注意:binaryMd5 对字符串走「每个 UTF-16 代码单元的低 8 位」,不是 UTF-8
// 中文文本要用 stringMd5
# 换页启动测试页(force-stop 不能省,换页参数只在冷启动生效)
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey QuickMd5TestApp

验证环境

项版本
React Native0.84.1
React19.2.3
RNOH(npm / ohpm)@react-native-oh/react-native-harmony / @rnoh/react-native-openharmony 0.84.3
Node.jsv24.14.0(用于生成期望值)
DevEco Studio26.0.0.621
HarmonyOS SDKAPI 26(26.0.0.32)
设备HarmonyOS 7.0.0(26.0.0) Beta2 模拟器 Pura X View(ohos-x64)
宿主 HAP 产物entry-default-signed.hap(80.71 MB)
本次增量构建assembleHap 6 分 33 秒
验证规模设备侧 65 项断言全通过(含 RFC 1321 七个标准向量 + 填充边界 + 1 MiB 分块边界)

欢迎加入 CPF-RN 鸿蒙社区:https://atomgit.com/CPF-RN

React Native for OpenHarmony 组织:https://atomgit.com/oh-react-native

RN 三方库鸿蒙适配清单:https://atomgit.com/oh-react-native/rn-ohos-adaptation-overview

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐