交叉编译
鸿蒙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这二个文件。
image-20240821101057465.png
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来编译。
