React Native for OpenHarmony 实战:三方库 react-native-quick-md5 的鸿蒙化适配指南
react-native-quick-md5 提供两个同步的 MD5 函数,用于文件校验、缓存键、内容指纹这类场景。它和"普通 RN 库"有两处明显不同:
- 它是 JSI 实现——TurboModule 只暴露一个同步的
install(),作用是往 JS 运行时装一个 C++ 函数;之后调用 MD5 完全不经过桥,也不经过 ArkTS; - 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 |
| 需要权限吗 | ❌ 不需要 |
| 公开 API | 2 个,都是同步: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 TurboModule | JS 侧 getEnforcing('X'),方法返回 Promise,C++ 用 ARK_ASYNC_METHOD_METADATA | 方法名/参数个数要对齐;异步 |
| 纯 C++ TurboModule | C++ 继承 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;
}};
三件事:
- 创建一个 host function(C++ 实现、JS 侧表现为普通函数);
- 把它挂到
globalThis.md5FromArrayBuffer; - 参数类型在 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 |
| 填充边界 | 10 | 0 / 1 / 55 / 56 / 63 / 64 / 65 / 127 / 128 / 129 字节 |
| 大缓冲区 | 3 | 1 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 |
| C | ArrayBuffer 路径(切片 / 修改后重读 / 输入不被修改) | 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 组的三个关键点
- 视图切片:
view.buffer是整个底层缓冲区,必须显式slice(byteOffset, byteOffset + byteLength)才是视图范围。实测切片后的摘要匹配 Node 算出的md5([10, 11]),且与整缓冲的摘要不同; - 修改后重读:改一个字节再算,摘要会变(没有缓存首次结果);
- 不改动输入:算完逐字节核对原数组未变。
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 |
| 适配 TAG | 3.0.9-ohos-1.0.0 |
| ohpm 包名 | @react-native-ohos/react-native-quick-md5 |
| HAR | harmony/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 Native | 0.84.1 |
| React | 19.2.3 |
| RNOH(npm / ohpm) | @react-native-oh/react-native-harmony / @rnoh/react-native-openharmony 0.84.3 |
| Node.js | v24.14.0(用于生成期望值) |
| DevEco Studio | 26.0.0.621 |
| HarmonyOS SDK | API 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
更多推荐




所有评论(0)