在鸿蒙中调用 FFmpeg 命令行工具
HarmonyOS FFmpeg 工具库 —— 在鸿蒙中调用 FFmpeg 命令行工具(fftools),最终驱动 FFmpeg.so 执行音视频处理任务。
文章目录
前言
学习要符合如下的标准化链条:了解概念->探究原理->深入思考->总结提炼->底层实现->延伸应用"
01.学习概述
- 学习主题:
- 知识类型:
- ✅Android/
- ✅01.基础组件与机制
- ✅四大组件
- ✅IPC机制
- ✅消息机制
- ✅事件分发机制
- ✅View与渲染体系(含Window、复杂控件、动画)
- ✅存储与数据安全(SharedPreferences/DataStore/Room/Scoped Storage)
- ✅02. 架构与工程化
- ✅架构模式(MVC/MVP/MVVM/MVI)
- ✅依赖注入(Koin/Hilt/Dagger)
- ✅路由与模块化(ARouter、Navigation)
- ✅Gradle与构建优化
- ✅插件化与动态化
- ✅插桩与监控框架
- ✅03.性能优化与故障诊断
- ✅ANR分析与优化
- ✅启动耗时优化
- ✅内存泄漏监控
- ✅监控与诊断工具
- ✅04.Jetpack与生态框架
- ✅Room
- ✅Paging
- ✅WorkManager
- ✅Compose
- ✅05.Framework与系统机制
- ✅ActivityManagerService (含ANR触发机制)
- ✅Binder机制
- ✅01.基础组件与机制
- ✅音视频开发/
- ✅01.基础知识
- ✅02.OpenGL渲染视频
- ✅03.FFmpeg音视频解码
- ✅ Java/
- ✅01.基础知识
- ✅02.集合框架
- ✅03.异常处理
- ✅04.多线程与并发
- ✅06.JVM
- ✅ Kotlin/
- ✅01.基础语法
- ✅02.高阶扩展
- ✅03.协程和流
- ✅ Flutter/
- ✅01.基础知识
- ✅Dart 语言基础
- ✅Widget 基础与生命周期
- ✅Flutter 基础组件
- ✅布局与约束
- ✅绘制与渲染体系
- ✅状态管理
- ✅事件处理与手势系统
- ✅原生通信
- ✅02.路由与导航
- ✅03.性能优化与故障诊断
- ✅04.异步编程
- ✅05.项目经验与案例沉淀
- ✅01.基础知识
- ✅ 自我管理/
- ✅01.内观
- ✅ 项目经验/
- ✅01.启动逻辑
- ✅02.云值守
- ✅03.智控平台
- ✅04.视频巡店
- ✅Android/
- 学习来源:
- 重要程度:⭐⭐⭐⭐⭐
- 学习日期:2025.
- 记录人:@panruiqi
1.1 学习目标
- 了解概念->探究原理->深入思考->总结提炼->底层实现->延伸应用"
1.2 前置知识
- [ ]
02.核心概念
2.1 需求场景
录像下载,要求下载对应url的录像,并转换为mp4格式
比如:有一个URL:https://sns-video-al.xhscdn.com/stream/110/405/01e583cb6e0fed5a010370038c8ad962fb_405.flv.
我们进行录像下载,下载后还要转换为mp4格式输出
2.2 实现方案
将FFmpeg命令行工具(fftools)封装成可供ArkTS调用的Native库。然后通过FFmpeg命令去控制ffmpeg.so执行相关任务
- 类似:
- ffmpeg -i https://sns-video-al.xhscdn.com/stream/110/405/01e583cb6e0fed5a010370038c8ad962fb_405.avi -c:v copy -c:a copy -f avi -y /data/storage/el2/base/haps/entry/files/test_output.mp4
其实本质就是:通过命令行调用ffmpeg.so执行相关任务,就这么简单。
本质很简单,但是实现很困难,涉及到多个部分:
- ArkTs如何与C层通信?
- fftools源码本身不为鸿蒙设计,其异常时会直接退出应用进程,我们要修改他,让他可以满足我们app的需求
- 需要编译鸿蒙侧的FFmpeg.so,并补全其依赖库,如:rtmpdump等。为上层提供基础能力,这涉及到CMakeList和交叉编译(还要开启一个fPIC)
- fftools源码依赖很多.h,我们要有引入补全他们。
整体而言,上层反而是最容易的,难点在于native和原生通信,ffmpeg.so的交叉编译,以及fftools工具的修改引入
2.3 技术选型
| 技术 | 用途 |
|---|---|
AKI框架 (@ohos/aki) |
ArkTS与c层通信,简化NAPI开发 |
| FFmpeg静态库 | 预编译的arm64-v8a架构FFmpeg库,提供ffmpeg核心能力 |
| fftools源码 | FFmpeg命令行工具源码,手动修改内部实现以支持鸿蒙侧进行调用 |
03.整体流程
3.1 架构设计
-
整体如下:
-
ARKTS应用层:其他lib通过调用当前lib_FFmpeg_Tools执行相关功能
-
ETS封装层:其就是当前lib_FFmpeg_Tools,会将其他lib的调用封装成一个ffmpegtools工具的cmd命令。并通过任务池管理和进行调度执行,最终执行是通过委托模式通过AKI JSBind委托给native层处理
-
AKi接受到任务后会在自身线程池中启动线程执行 napi_ffmpeg.cpp 的executeFFmpegCommandAPP;该命令会调用到fftools工具
-
fftools调用到底层的ffmpeg的静态库:libavcodec.a;libavfilter.a等
-
至此整体调用流程完成
-
┌─────────────────────────────────────────────────────────────┐ │ ArkTS 应用层 │ │ (调用 FFmpegManager.execute() 执行视频处理任务) │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ ETS 封装层 (src/main/ets/) │ │ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │ │ │FFmpegManager│→ │TaskDispatcher│→ │ FFMpegUtils │ │ │ │ (入口) │ │ (调度器) │ │ (Native桥接) │ │ │ └─────────────┘ └──────────────┘ └─────────────────┘ │ └─────────────────────────────────────────────────────────────┘ │ AKI JSBind ▼ ┌─────────────────────────────────────────────────────────────┐ │ Native 桥接层 (src/main/cpp/) │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ napi_ffmpeg.cpp │ │ │ │ - executeFFmpegCommandAPP() : 主执行函数 │ │ │ │ - 回调桥接: onFFmpegProgress/Fail/Success │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ │ C函数调用 ▼ ┌─────────────────────────────────────────────────────────────┐ │ FFmpeg 执行层 (src/main/cpp/fftools/) │ │ ┌──────────┐ ┌───────────┐ ┌────────────┐ │ │ │ ffmpeg.c │ │ffmpeg_opt │ │ffmpeg_demux│ ... │ │ │(主入口) │ │ (选项解析)│ │ (解封装) │ │ │ └──────────┘ └───────────┘ └────────────┘ │ │ │ │ 核心函数: exe_ffmpeg_cmd(argc, argv, callbacks) │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ FFmpeg 静态库 (src/main/cpp/ffmpeg/arm64-v8a) │ │ libavformat.a | libavcodec.a | libavfilter.a | ... │ └─────────────────────────────────────────────────────────────┘
-
-
这里的native和ets通信全是依赖aki实现的(ets调用native 和 native回调ets)
3.2 代码执行流
-
看看代码的执行流
-
┌──────────────────────────────────────────────────────────────────────────┐ │ 应用层调用 │ │ FFmpegManager.getInstance().execute(commands, duration, callback) │ └──────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────────────────┐ │ FFmpegManager.executeWithConfig() │ │ ├─ 1. 生成唯一taskId (UUID) │ │ ├─ 2. 创建Task对象,封装commands/duration/config/callback │ │ ├─ 3. 记录到activeTasks Map │ │ └─ 4. 调用 taskDispatcher.enqueue(task) │ └──────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────────────────┐ │ TaskDispatcher.enqueue() │ │ ├─ 1. 按优先级插入taskQueue(HIGH > NORMAL > LOW) │ │ └─ 2. 调度循环(每100ms)检查队列,取出任务提交给WorkerPool │ └──────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────────────────┐ │ WorkerPool.submit() │ │ ├─ 1. 等待有空闲Worker(activeWorkers < maxWorkers) │ │ ├─ 2. 创建FFmpegWorker │ │ └─ 3. 异步执行 worker.execute() │ └──────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────────────────┐ │ FFmpegWorker.execute() │ │ ├─ 1. 通过CallbackDispatcher分发onStart回调 │ │ ├─ 2. 创建FFmpegExecutor │ │ └─ 3. 调用 executor.execute(callback) │ └──────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────────────────┐ │ FFmpegExecutor.execute() │ │ └─ 调用 FFMpegUtils.executeFFmpegCommand({cmds, callbacks}) │ └──────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────────────────┐ │ FFMpegUtils.executeFFmpegCommand() 【关键:ETS→Native桥接】 │ │ ├─ 1. 生成32位UUID │ │ ├─ 2. 通过AKI绑定回调函数到全局: │ │ │ libAddon.JSBind.bindFunction(uuid+"_onFFmpegProgress", callback) │ │ │ libAddon.JSBind.bindFunction(uuid+"_onFFmpegFail", callback) │ │ │ libAddon.JSBind.bindFunction(uuid+"_onFFmpegSuccess", callback) │ │ └─ 3. 调用Native: libAddon.executeFFmpegCommandAPP(uuid, cmdLen, cmds) │ └──────────────────────────────────────────────────────────────────────────┘ │ AKI JSBind ▼ ┌──────────────────────────────────────────────────────────────────────────┐ │ napi_ffmpeg.cpp :: executeFFmpegCommandAPP() 【Native层】 │ │ ├─ 1. 转换参数: vector<string> → char** │ │ ├─ 2. 获取ArkTS回调函数指针: │ │ │ aki::JSBind::GetJSFunction(uuid + "_onFFmpegProgress") │ │ ├─ 3. 构建C回调结构体 Callbacks │ │ └─ 4. 调用 exe_ffmpeg_cmd(argc, argv, &callbacks) │ └──────────────────────────────────────────────────────────────────────────┘ │ C函数调用 ▼ ┌──────────────────────────────────────────────────────────────────────────┐ │ ffmpeg.c :: exe_ffmpeg_cmd() 【FFmpeg执行层】 │ │ ├─ 解析命令行参数 │ │ ├─ 打开输入文件/流 │ │ ├─ 创建输出文件 │ │ ├─ 转码/复制流 │ │ ├─ 执行过程中调用 callbacks->onFFmpegProgress(progress) │ │ └─ 完成后返回结果码 │ └──────────────────────────────────────────────────────────────────────────┘
-
04. 核心代码讲解
难点在于native和原生通信,ffmpeg.so的交叉编译,以及fftools工具的修改引入
但是就代码而言,目前能看的只有native层与原生的通信机制,以及fftools工具的修改
4.1 ETS与Native通信机制
AKI框架:
- 作用:简化了napi,提供相关封装,我们通过对其调用可以快速实现和native层通信
原生调用native流程
-
Native层注册函数
-
// napi_ffmpeg.cpp // 1. 注册模块名 JSBIND_ADDON(ffmpegutils) // 对应 libffmpegutils.so // 2. 注册全局函数 JSBIND_GLOBAL() { JSBIND_PFUNCTION(executeFFmpegCommandAPP); // 异步 JSBIND_FUNCTION(showLog); // 同步 } - `JSBIND_PFUNCTION` 让函数在AKI线程池中异步执行,返回Promise - `JSBIND_FUNCTION` 在JS主线程同步执行
-
-
ETS层调用
-
// FFMpegUtils.ets import libAddon from 'libffmpegutils.so' // 加载.so库 // 直接调用Native函数(AKI自动映射) libAddon.executeFFmpegCommandAPP(uuid, cmdLen, cmds); libAddon.showLog(true);
-
-
此时,通过函数名的映射,我们很自然的调用到了so中的c代码执行位置
native层回调原生
-
如果是Android,那么我们要获得JVM虚拟机和env。然后保存方法ID,通过方法ID找到jMethod,并执行其static方法,也就是JNI机制
-
ETS层:注册回调
-
我们直接将回调函数绑定到全局,然后native层只传递uuid
-
// FFMpegUtils.ets let uuid = FFMpegUtils.generateUUID32(); // 生成唯一标识 // 将回调函数绑定到全局,以 uuid_函数名 命名 libAddon.JSBind.bindFunction(uuid + "_onFFmpegProgress", options.onFFmpegProgress); libAddon.JSBind.bindFunction(uuid + "_onFFmpegFail", options.onFFmpegFail); libAddon.JSBind.bindFunction(uuid + "_onFFmpegSuccess", options.onFFmpegSuccess); // 调用Native,传递uuid libAddon.executeFFmpegCommandAPP(uuid, options.cmds.length, options.cmds);
-
-
Native层:获取回调
-
// napi_ffmpeg.cpp int executeFFmpegCommandAPP(std::string uuid, int cmdLen, std::vector<std::string> argv) { // 通过uuid找到对应的JS回调函数 CallBackInfo onActionListener; onActionListener.onFFmpegProgress = aki::JSBind::GetJSFunction(uuid + "_onFFmpegProgress"); onActionListener.onFFmpegFail = aki::JSBind::GetJSFunction(uuid + "_onFFmpegFail"); onActionListener.onFFmpegSuccess = aki::JSBind::GetJSFunction(uuid + "_onFFmpegSuccess"); // 调用JS回调 onActionListener.onFFmpegProgress->Invoke<void>(progress); } -
看起来很简单,通过名称从aki中找到js回调函数
-
关键是aki的机制,我们通过上面的api就可以实现ets层和native的通信。那么原理呢?其实这就是跨语言通信的过程,我们后面5.跨语言通信原理(虚拟机语言 ↔ Native)中说
4.2 fftools封装与处理(重点)
问题: 原生FFmpeg的 exit_program() 会调用 exit() 直接终止进程,这在库调用场景下是不可接受的,因为会导致我们的应用进程崩溃。
解决方案: 使用 setjmp/longjmp 实现非局部跳转,优雅返回错误码。
-
原代码(ffmpeg原版):
-
void exit_program(int ret) { if (program_exit) program_exit(ret); exit(ret); // ❌ 直接退出进程 }
-
-
修改后(cmdutils.c):
-
// 线程局部存储的返回值 __thread volatile int longjmp_value; void exit_program(int ret) { if (program_exit) program_exit(ret); // 保存返回值并跳转 longjmp_value = ret; longjmp(ex_buf__, ret); // 跳转到setjmp处 } -
我们在哪setjmp呢?
-
在exe_ffmpeg_cmd中setjmp,根据返回值进行相关处理,并最终return执行的结果码
-
int exe_ffmpeg_cmd(int argc, char **argv, Callbacks *callbacks) { int ret; // 保存回调指针 g_callbacks = callbacks; // 设置跳转点 int savedCode = setjmp(ex_buf__); if (savedCode == 0) { // ===== 正常执行流程 ===== init_dynload(); register_exit(ffmpeg_cleanup); avformat_network_init(); ret = ffmpeg_parse_options(argc, argv); // 解析参数 if (ret < 0) exit_program(1); // 会跳转到else分支 if (transcode() < 0) // 执行转码 exit_program(1); exit_program(received_nb_signals ? 255 : main_return_code); } else { // ===== longjmp跳转后到达这里 ===== main_return_code = (received_nb_signals) ? 255 : longjmp_value; } return main_return_code; // 优雅返回,不会exit }
-
4.3 CMake构建配置
-
如下
-
将native层一并构建为一个ffmpegtools.so
-
# CMakeLists.txt 关键配置 # FFmpeg静态库链接顺序(很重要!) target_link_libraries(ffmpegutils PRIVATE ${FFMPEG_LIB}/libavdevice.a ${FFMPEG_LIB}/libavfilter.a ${FFMPEG_LIB}/libavformat.a ${FFMPEG_LIB}/libavcodec.a ${FFMPEG_LIB}/libswresample.a ${FFMPEG_LIB}/libswscale.a ${FFMPEG_LIB}/libavutil.a # 依赖库 ${FFMPEG_LIB}/librtmp.a ${FFMPEG_LIB}/libssl.a ${FFMPEG_LIB}/libcrypto.a # 鸿蒙系统库 libace_napi.z.so libhilog_ndk.z.so # AKI框架 Aki::libjsbind )
-
05.跨语言通信原理(虚拟机语言 ↔ Native)
5.1 为什么需要跨语言通信机制
跨语言通信的难点在于:两者内存模型完全不同,无法直接互操作。
-
┌─────────────────────────────────────────────────────────────────────────┐ │ 两个完全不同的世界 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 虚拟机语言 (Java/ArkTS/JS) Native语言 (C/C++) │ │ ┌─────────────────────────┐ ┌─────────────────────────┐ │ │ │ • 运行在虚拟机/引擎上 │ │ • 直接运行在CPU上 │ │ │ │ • 托管堆,GC自动管理内存 │ │ • Native堆/栈 │ │ │ │ • 动态类型 │ │ • malloc/free手动管理 │ │ │ │ • 对象可能被GC移动 │ │ • 静态类型 │ │ │ │ • 安全但性能受限 │ │ • 指针直接访问内存 │ │ │ └─────────────────────────┘ │ • 高性能但需谨慎 │ │ │ └─────────────────────────┘ │ │ │ │ 应用场景:UI、业务逻辑 应用场景:音视频、加密、图形 │ └─────────────────────────────────────────────────────────────────────────┘
5.2 核心机制
句柄(Handle)—— 间接访问
-
问题:GC会移动对象,Native不能直接持有虚拟机堆地址。
-
解决:通过句柄间接访问。
-
┌─────────────────────────────────────────────────────────────────────────┐ │ 句柄机制 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ Native代码持有: napi_value / jobject (句柄,如索引5) │ │ │ │ │ ▼ │ │ 句柄表: [5] → 0x1000 ← 引擎维护,GC移动对象时自动更新 │ │ │ │ │ ▼ │ │ 虚拟机堆: 0x1000 存放实际对象 │ │ │ │ ───────────────────────────────────────────────────────────────────── │ │ │ │ GC移动对象后: │ │ 句柄表: [5] → 0x2000 ← 自动更新 │ │ 虚拟机堆: 0x2000 存放实际对象(被移动了) │ │ │ │ Native的句柄(5)不变,但访问到的是新地址 ✓ │ └─────────────────────────────────────────────────────────────────────────┘
-
数据传递 —— 复制而非共享
-
虚拟机传递数据到native是通过复制数据到native堆/栈中
-
┌─────────────────────────────────────────────────────────────────────────┐ │ 数据传递是复制,不是共享 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 虚拟机堆 Native堆/栈 │ │ ┌──────────────────┐ ┌──────────────────┐ │ │ │ JSString "hello" │ ──复制数据──→ │ char buf[]="hello"│ │ │ │ (GC管理) │ │ (手动管理) │ │ │ └──────────────────┘ └──────────────────┘ │ │ │ │ 两份独立的数据,互不影响 │ │ - 修改buf不会影响JSString │ │ - GC回收JSString不会影响buf │ └─────────────────────────────────────────────────────────────────────────┘
-
-
native传递数据到虚拟机则是通过创建虚拟机中的对象,然后把句柄传给原生层
-
┌─────────────────────────────────────────────────────────────────────────┐ │ 返回值是"创建",不是"写入" │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ Native执行完毕: │ │ int result = 42; │ │ │ │ │ ▼ napi_create_int32(env, 42, &js_result) │ │ │ │ 引擎内部: │ │ 1. 在虚拟机堆中分配空间 │ │ 2. 创建JSNumber对象,值为42 │ │ 3. 返回句柄给Native │ │ │ │ │ ▼ │ │ Native拿到句柄,return给JS层 │ └─────────────────────────────────────────────────────────────────────────┘
-
引用管理 —— 防止GC回收
-
┌─────────────────────────────────────────────────────────────────────────┐ │ 引用类型 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 临时句柄 (Local Reference) │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ • 函数调用期间有效 │ │ │ │ • 函数返回后自动失效 │ │ │ │ • 适用于:参数、临时变量 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ 持久引用 (Global Reference / napi_ref) │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ • 手动创建,手动释放 │ │ │ │ • 告诉GC:这个对象被Native持有,不要回收 │ │ │ │ • 适用于:回调函数、长期缓存的对象 │ │ │ │ │ │ │ │ 创建: napi_create_reference(env, obj, 1, &ref) │ │ │ │ 使用: napi_get_reference_value(env, ref, &obj) │ │ │ │ 释放: napi_delete_reference(env, ref) │ │ │ └─────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘
5.3 完整调用流程
虚拟机 → Native
-
如下
-
┌─────────────────────────────────────────────────────────────────────────┐ │ JS/ArkTS 调用 Native 函数 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ Step 1: JS发起调用 │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ libAddon.executeCommand("hello", 123, [1,2,3]) │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ Step 2: 引擎准备调用 │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ • 查找Native函数指针 │ │ │ │ • 将JS参数转为句柄数组: [handle1, handle2, handle3] │ │ │ │ • 创建 napi_callback_info │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ Step 3: Native函数执行 │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ napi_value my_func(napi_env env, napi_callback_info info) { │ │ │ │ // 获取参数句柄 │ │ │ │ napi_get_cb_info(env, info, &argc, argv, ...); │ │ │ │ │ │ │ │ // 通过句柄复制数据到Native │ │ │ │ napi_get_value_string_utf8(env, argv[0], buf, ...); │ │ │ │ // 现在 buf = "hello" │ │ │ │ │ │ │ │ // 执行C逻辑... │ │ │ │ int result = do_something(buf); │ │ │ │ │ │ │ │ // 创建返回值(在虚拟机堆中创建新对象) │ │ │ │ napi_value js_result; │ │ │ │ napi_create_int32(env, result, &js_result); │ │ │ │ return js_result; │ │ │ │ } │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ Step 4: 返回JS层 │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ JS拿到返回值,继续执行 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘
-
Native → 虚拟机(回调)
-
如下
-
┌─────────────────────────────────────────────────────────────────────────┐ │ Native 调用 JS 回调函数 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ Step 1: JS注册回调 │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ libAddon.bindCallback("onProgress", (progress) => { │ │ │ │ console.log(progress); │ │ │ │ }); │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ Step 2: Native保存引用 │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ // 创建持久引用,防止GC回收 │ │ │ │ napi_create_reference(env, js_callback, 1, &g_callback_ref); │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ Step 3: Native需要回调时 │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ // 从引用获取函数句柄 │ │ │ │ napi_value callback; │ │ │ │ napi_get_reference_value(env, g_callback_ref, &callback); │ │ │ │ │ │ │ │ // 准备参数(创建JS对象) │ │ │ │ napi_value args[1]; │ │ │ │ napi_create_int32(env, 50, &args[0]); // progress = 50 │ │ │ │ │ │ │ │ // 调用JS函数 │ │ │ │ napi_call_function(env, nullptr, callback, 1, args, &result); │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ Step 4: JS回调执行 │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ console.log(50); // 输出进度 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘
-
5.4 aki的价值
原生NAPI非常繁琐,AKI等框架通过模板元编程自动生成:
-
看看两者对比
-
┌─────────────────────────────────────────────────────────────────────────┐ │ 手写NAPI vs AKI │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 手写NAPI(约50行): │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ napi_value func(napi_env env, napi_callback_info info) { │ │ │ │ size_t argc = 2; │ │ │ │ napi_value argv[2]; │ │ │ │ napi_get_cb_info(env, info, &argc, argv, NULL, NULL); │ │ │ │ │ │ │ │ // 手动转换每个参数... │ │ │ │ size_t len; │ │ │ │ napi_get_value_string_utf8(env, argv[0], NULL, 0, &len); │ │ │ │ char* buf = malloc(len + 1); │ │ │ │ napi_get_value_string_utf8(env, argv[0], buf, len+1, &len);│ │ │ │ // ... 更多转换代码 ... │ │ │ │ } │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ 使用AKI(约5行): │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ int myFunc(std::string str, int num) { │ │ │ │ // 直接使用C++类型,AKI自动转换 │ │ │ │ return 0; │ │ │ │ } │ │ │ │ JSBIND_FUNCTION(myFunc); │ │ │ └─────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘
-
05.深度思考
5.1 关键问题探究
5.2 设计对比
06.实践验证
6.1 行为验证代码
6.2 性能测试
07.应用场景
7.1 最佳实践
7.2 使用禁忌
08.总结提炼
8.1 核心收获
8.2 知识图谱
8.3 延伸思考
09.参考资料
其他介绍
01.关于我的博客
-
csdn:http://my.csdn.net/qq_35829566
-
掘金:https://juejin.im/user/499639464759898
-
github:https://github.com/jjjjjjava
-
邮箱:[934137388@qq.com]
更多推荐


所有评论(0)