交叉编译
鸿蒙Next
是不兼容安卓之前编译的so
库的,需要利用鸿蒙提供的工具进行交叉编译才行,交叉工具的安装有二种方式:
如果安装了
Deveco
工具的话,可以在安装目录下找到编译工具,Mac上在应用程序上找到Deveco
右键显示包内容,然后找到contents/sdk/HarmonyOS-NEXT-DB3/openharmony/native
目录下,里面有二个文件夹build-tools
和build
,其中build/cmake/ohos.toolchain.cmake
和build-tools/cmake/bin/cmake
二个文件是我们需要使用的。-
也可以手动下载 OpenHarmony SDK选择对应系统的版本即可,
SDK
分为full
和public
二个版本,这二个版本的SDK
中关于c/c++
编译的部分是相同的,随便下载一个就行。下载好后进行解压,然后进去到ohos-sdk/darwin/native
下,可以看到有build
和build-tools
二个文件夹,这和上面一样,我们同样要用到cmake
和ohos.toolchain.cmake
这二个文件。
opencv源码下载及编译
去github
上找到opencv
的源码下载下载,opencv
是支持cmake编译的,然后源码文件夹下新建ohos64build
文件夹用来存放编译后的文件。
然后进入到ohos64build
文件夹下,这里我们采用上面第二种方式的cmake
工具在编译,执行如下命令生成makefile
文件:
/Library/OpenHarmony4.0.10.5/ohos-sdk/darwin/native/build-tools/cmake/bin/cmake -DCMAKE_TOOLCHAIN_FILE=/Library/OpenHarmony4.0.10.5/ohos-sdk/darwin/native/build/cmake/ohos.toolchain.cmake ..
主要前面的
cmake
和后面的ohos.toolchain.cmake
都是鸿蒙sdk
里面的,替换成自己的路径,后面的二个点不能遗漏
最后执行结果如下,省略了很多中间过程:
然后执行make && make install
即可编译出so
库,编译完成后文件都在install
文件夹下,其中so
库在lib
文件下:
头文件在include
文件夹下:
上面的命令编译出来的
so
库默认架构是ARM aarch64
,如果要编译arm32
的需要在cmake
时加上参数-DOHOS_ARCH=armeabi-v7a
集成
集成到项目里面是有多种方式的,可以通过源码,也可以通过so
库的方式,这里我们采用刚刚编译出的so
库来集成,首先利用Deveco
新建一个native
项目,新建项目的过程请看官网介绍,这里不在赘述,我们要实现的是利用opencv
来灰度处理图片,效果如下:
查看so库的soname
由于我们编译的是动态库,动态库的引用是通过soname
来查找的,我们需要查看编译出来so
库的soname
,然后把so
库重命名为对应的名字,查看soname
的命令是如下:
/Library/OpenHarmony4.0.10.5/ohos-sdk/darwin/native/llvm/bin/llvm-readobj -d libopencv_calib3d.so.4.8.1
细节如下,可以看到soname
名字是so.408
结尾,所以需要把lib
下的所有库后缀为so.4.8.1
的重命名为so.408
:
集成到项目
so库集成
把上面的重命名的so
库放到entry
下的lib
文件夹下,具体如下图所示:
头文件集成
在src/main/cpp
下面新建一个thirdparty
文件夹,然后新建opencv
文件夹以及对应的架构文件夹,同时新建一个include
文件夹,在把对应的头文件放进去,我们把上面编译出来的整个opencv4
直接拷贝进去,具体如下图所示:
完成上面的后,需要在cpp
下的CMakeLists.txt
增加如下内容,以便应用so
和头文件:
nopi代码的编写
native侧实现
鸿蒙ArkTS
和c/c++
交互是通过nopi
来实现的,和安卓的jni
类似,需要编写中间层代码,鸿蒙采用的是node
来实现的,具体的用法请看官网,这里仅给出实现:
#include "napi/native_api.h"
#include "opencv2/opencv.hpp"
#include <vector>
static napi_value ProcessImage(napi_env env, napi_callback_info info)
{
size_t argc = 1;
napi_value args[1] = {nullptr};
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
if (argc < 1) {
napi_throw_error(env, nullptr, "图片数据参数缺失");
return nullptr;
}
// 检查参数是否是 ArrayBuffer 类型
bool isArrayBuffer = false;
napi_status status = napi_is_arraybuffer(env, args[0], &isArrayBuffer);
if (status != napi_ok || !isArrayBuffer) {
napi_throw_error(env, nullptr, "参数不是有效的 ArrayBuffer");
return nullptr;
}
// 获取传入的图片数据 (Buffer)
void* bufferData;
size_t bufferLength;
status = napi_get_arraybuffer_info(env, args[0], &bufferData, &bufferLength);
if (status != napi_ok) {
napi_throw_error(env, nullptr, "无法获取 ArrayBuffer 信息");
return nullptr;
}
// 将 Buffer 数据转换为 OpenCV Mat 对象
std::vector<unsigned char> imgData((unsigned char*)bufferData, (unsigned char*)bufferData + bufferLength);
cv::Mat img = cv::imdecode(imgData, cv::IMREAD_COLOR);
if (img.empty()) {
napi_throw_error(env, nullptr, "无法解码图像数据");
return nullptr;
}
// 转换为灰度图
cv::Mat grayImg;
cv::cvtColor(img, grayImg, cv::COLOR_BGR2GRAY);
// 将处理后的图片转换回 Buffer
std::vector<unsigned char> encodedImg;
cv::imencode(".png", grayImg, encodedImg);
// 创建一个新的 ArrayBuffer 来存储编码后的图像数据
napi_value resultArrayBuffer;
void* arrayBufferData = nullptr;
status = napi_create_arraybuffer(env, encodedImg.size(), &arrayBufferData, &resultArrayBuffer);
if (status != napi_ok) {
napi_throw_error(env, nullptr, "无法创建 ArrayBuffer");
return nullptr;
}
// 将图像数据复制到 ArrayBuffer 中
std::memcpy(arrayBufferData, encodedImg.data(), encodedImg.size());
// 返回 ArrayBuffer
return resultArrayBuffer;
}
static napi_value Add(napi_env env, napi_callback_info info)
{
size_t argc = 2;
napi_value args[2] = {nullptr};
napi_get_cb_info(env, info, &argc, args , nullptr, nullptr);
napi_valuetype valuetype0;
napi_typeof(env, args[0], &valuetype0);
napi_valuetype valuetype1;
napi_typeof(env, args[1], &valuetype1);
double value0;
napi_get_value_double(env, args[0], &value0);
double value1;
napi_get_value_double(env, args[1], &value1);
napi_value sum;
napi_create_double(env, value0 + value1, &sum);
return sum;
}
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports)
{
napi_property_descriptor desc[] = {
{ "add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "processImage", nullptr, ProcessImage, nullptr, nullptr, nullptr, napi_default, nullptr }
};
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
return exports;
}
EXTERN_C_END
static napi_module demoModule = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = Init,
.nm_modname = "entry",
.nm_priv = ((void*)0),
.reserved = { 0 },
};
extern "C" __attribute__((constructor)) void RegisterEntryModule(void)
{
napi_module_register(&demoModule);
}
上面的
add
方法是官方新建项目生成的,可以去掉,这里主要是接收ArkUI
侧传递过来的图片buffer
数据,然后利用opencv
进行灰度处理,在将灰度图片的buffer
数据回传给ArkUI
侧
在types/libentry下的Index.d.ts下面新增processImage方法,具体如下:
export const add: (a: number, b: number) => number;
export const processImage: (imageBuffer: ArrayBuffer) => ArrayBuffer;
ArkUI侧的实现
ArkUI
侧的全部实现如下:
import { hilog } from '@kit.PerformanceAnalysisKit';
import testNapi from 'libentry.so';
import fileIO from '@ohos.fileio';
import { resourceManager } from '@kit.LocalizationKit';
import { BusinessError } from '@kit.BasicServicesKit';
const context : Context = getContext(this);
// 获取resourceManager资源管理器
const resourceMgr : resourceManager.ResourceManager = context.resourceManager;
// 将 ArrayBuffer 转换为 Base64 字符串的辅助函数
function arrayBufferToBase64(buffer: ArrayBuffer): string {
const base64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
let base64 = '';
const bytes = new Uint8Array(buffer);
const byteLength = bytes.byteLength;
for (let i = 0; i < byteLength; i += 3) {
const b1 = bytes[i];
const b2 = i + 1 < byteLength ? bytes[i + 1] : 0;
const b3 = i + 2 < byteLength ? bytes[i + 2] : 0;
const chunk = (b1 << 16) | (b2 << 8) | b3;
base64 += base64chars.charAt((chunk >> 18) & 0x3F);
base64 += base64chars.charAt((chunk >> 12) & 0x3F);
base64 += base64chars.charAt((chunk >> 6) & 0x3F);
base64 += base64chars.charAt(chunk & 0x3F);
}
// 根据字节数计算需要添加的填充符号
const padding = byteLength % 3;
if (padding > 0) {
base64 = base64.slice(0, padding - 3) + '==='.slice(padding);
}
return base64;
}
@Entry
@Component
struct Index {
@State image: PixelMap | undefined = undefined;
@State originalImageSrc: string = 'opencv_test.png';
@State base64Img: string = '';
@State processedImageSrc: string = '';
async loadImageBuffer(imagePath: string): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
resourceMgr.getRawFileContent(imagePath).then((fileData : Uint8Array) => {
console.log("Succeeded in getting RawFileContent")
// 获取图片的ArrayBuffer
const buffer = fileData.buffer.slice(0);
resolve(buffer);
}).catch((err : BusinessError) => {
reject('Failed to get RawFileContent');
});
});
};
build() {
Row() {
Column() {
Image($rawfile(this.originalImageSrc))
.width(200)
Button('点击后利用opencv来灰度图片')
.onClick(async () => {
hilog.info(0x0000, 'testTag', '这是利用CMake编译的so库的结果:Test NAPI 10 + 15 = %{public}d', testNapi.add(10,15));
hilog.info(0x0000, 'testTag', "点击了");
// 先加载图片的buffer
this.loadImageBuffer(this.originalImageSrc).then( buffer => {
// 利用opencv进行灰度处理
let new_buffer = testNapi.processImage(buffer);
let base64 = arrayBufferToBase64(new_buffer);
this.base64Img = base64;
hilog.info(0x0000, 'testTag', "图片转成的base64 是%{public}s",base64);
}
).catch( (msg:string) => {
hilog.info(0x0000, 'testTag', '发生了错误%{public}s',msg);
}
);
})
Image(`data:image/png;base64,${this.base64Img}`).width(200)
}
.width('100%')
}
.height('100%')
}
}
点击按钮的实现首先利用
ResourceManager
来获取图片的buffer
数据,然后调用native
侧的processImage
方法或者灰度处理后的buffer
,然后转成base64
,最后利用Image
组件来显示base64
的图片,图片放到src/main/resources/rawfile
目录下。
总结
本位主要是讲解了如何利用鸿蒙提供的工具来交叉编译三方库,然后集成到native项目中,当然也可以使用第三方工具lycium来编译。