上一节笔者给大家讲解了 JS 引擎解释执行到 import
语句的加载流程,总结起来就是利用 dlopen() 方法的加载特性向 NativeModuleManager
内部的链接尾部添加一个 NativeModule,没有阅读过上节文章的小伙伴,笔者强烈建议阅读一下,本节笔者继续给大家讲解 JS 调用 C++ 方法的实现过程。
回看requireNapi方法
根据上节课的讲解,napi_module_register() 方法只是通过 demoModule
的配置创建一个 NativeModule 后并把它加入到 NativeModuleManager 内部的链表尾部,当在 JS 侧调用 C++ 的对应方法时,如何能精准调用到对应方法的呢?我们再回头看下 ArkNativeEngine 构造方法中注册的 requireNapi() 方法内的执行过程,省略部分源码如下所示:
ArkNativeEngine::ArkNativeEngine(EcmaVM* vm, void* jsEngine, bool isLimitedWorker) : NativeEngine(jsEngine), vm_(vm), topScope_(vm), isLimitedWorker_(isLimitedWorker) {
// 省略部分代码……
void* requireData = static_cast<void*>(this);
// 创建一个requireNapi()方法
Local<FunctionRef> requireNapi =
FunctionRef::New(
vm,
[](JsiRuntimeCallInfo *info) -> Local<JSValueRef> {
NativeModuleManager* moduleManager = NativeModuleManager::GetInstance();
NativeModule* module = nullptr;
// 调用NativeModuleManager的LoadNativeModule方法加载
module = moduleManager->LoadNativeModule();
if (module != nullptr) {
// 先判断 module 的 jsABCCode 或者 jsCode 是否为空则
if (module->jsABCCode != nullptr || module->jsCode != nullptr) {
// 省略部分代码……
} else if (module->registerCallback != nullptr) {
// 如果 module 的 registerCallback 不为空,则执行registerCallback() 方法
module->registerCallback(reinterpret_cast<napi_env>(arkNativeEngine), JsValueFromLocalValue(exportObj));
} else {
HILOG_ERROR("init module failed");
return scope.Escape(exports);
}
}
return scope.Escape(exports);
},
nullptr,
requireData);
Local<ObjectRef> global = panda::JSNApi::GetGlobalObject(vm);
Local<StringRef> requireName = StringRef::NewFromUtf8(vm, "requireNapi");
// 注入 requireNapi 方法
global->Set(vm, requireName, requireNapi);
Init();
panda::JSNApi::SetLoop(vm, loop_);
}
requireNapi() 方法内部先调用 NativeModuleManager
的 LoadNativeModule() 方法加载动态库并返回一个 module
,如果 module
非空,则判断 module
中的 jsABCCode
或者 jsCode
是否为空,如果有一个非空则条件成立进入 if 语句,那么 jsABCCode
或者 jsCode
什么时候非空呢?比如加载的是项目中的一个模块而非一个单纯的动态库时条件才成立或者在跨平台的场景需要加载 abc 时条件成立,本文的样例只是加载了一个 libentry.so
,因此条件不成立,接着判断 module
的 registerCallback
是否为空,registerCallback
是什么时机赋值的呢?笔者在上一节讲 napi_module_register() 中讲到过赋值,源码如下所示:
NAPI_EXTERN void napi_module_register(napi_module* mod)
{
NativeModuleManager* moduleManager = NativeModuleManager::GetInstance();
NativeModule module;
module.version = mod->nm_version;
module.fileName = mod->nm_filename;
module.name = mod->nm_modname;
// registerCallback 是 mod 中配置的nm_register_func方法
module.registerCallback = (RegisterCallback)mod->nm_register_func;
moduleManager->Register(&module);
}
在 napi_module_register() 方法内部把 mod
中配置的 nm_register_func
强制转换成 RegisterCallback
后赋值给了 NativeModule
的 registerCallback
,这里可以进行强制转换利用的是 C++ 的一个特性:
在 C++ 中,函数指针类型的转换需要满足源类型和目标类型的函数签名(参数类型和数量,以及返回类型)完全相同。本样例中 nm_register_func 和 RegisterCallback 类型定义分别如下所示:
> typedef napi_value (*napi_addon_register_func)(napi_env env, napi_value exports);
>
> typedef napi_value (*RegisterCallback)(napi_env, napi_value);
它们都接收两个参数:一个
napi_env
类型的env
和一个napi_value
类型的exports
,并返回一个napi_value
类型的值,所以它们的函数签名是完全相同的,因此一个napi_addon_register_func
类型的函数指针可以被强制转换为RegisterCallback
类型的函数指针。
nm_register_func 就是在 hello.cpp
中配置的 Init() 方法,hello.cpp
的源码如下:
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
// 创建一个napi_property_descriptor数组,napi_property_descriptor的每一项只配置了
napi_property_descriptor desc[] = {
{"add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr},
{"getMd5Sync", nullptr, GetMd5Sync, nullptr, nullptr, nullptr, napi_default, nullptr},
{"getMd5", nullptr, GetMd5, nullptr, nullptr, nullptr, napi_default, nullptr},
};
// 调用napi_define_properties方法
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_register_func被配置为 Init 方法
.nm_modname = "entry",
.nm_priv = ((void *)0),
.reserved = {0},
};
extern "C" __attribute__((constructor)) void RegisterEntryModule(void) {
napi_module_register(&demoModule);
}
综上所述,在 requireNapi() 方法内执行 module->registerCallback() 方法时就是执行的 hello.cpp
中配置的 Init() 方法,在 Init() 方法中先创建一个 napi_property_descriptor
类型的数组 desc
,每一个 napi_property_descriptor
数据只配置了 utf8name,method 和 attributes 这 3 项,然后调用 napi_define_properties() 方法,napi_define_properties() 方法源码如下所示:
NAPI_EXTERN napi_status napi_define_properties(napi_env env,
napi_value object,
size_t property_count,
const napi_property_descriptor* properties)
{
// 省略部分代码……
for (size_t i = 0; i < property_count; i++) {
NapiPropertyDescriptor property;
// 有值
property.utf8name = properties[i].utf8name;
// 无值
property.name = properties[i].name;
// 有值
property.method = reinterpret_cast<NapiNativeCallback>(properties[i].method);
// 无值
property.getter = reinterpret_cast<NapiNativeCallback>(properties[i].getter);
// 无值
property.setter = reinterpret_cast<NapiNativeCallback>(properties[i].setter);
// 无值
property.value = properties[i].value;
// 有值且值为0
property.attributes = (uint32_t)properties[i].attributes;
// 无值
property.data = properties[i].data;
// 调用NapiDefineProperty方法
NapiDefineProperty(env, nativeObject, property);
}
return napi_clear_last_error(env);
}
为了便于后续分析源码,笔者加上了详细的注释,napi_define_properties() 方法内部循环遍历传递进来的每一个 napi_property_descriptor
,把每一个 napi_property_descriptor
转化成 NapiPropertyDescriptor
的 property
并调用 NapiDefineProperty() 方法完成 JS 方法和 C++方法的映射,NapiDefineProperty() 方法源码如下所示:
bool NapiDefineProperty(napi_env env, Local<panda::ObjectRef> &obj, NapiPropertyDescriptor propertyDescriptor)
{
auto engine = reinterpret_cast<NativeEngine*>(env);
auto vm = engine->GetEcmaVm();
bool result = false;
// 根据utf8name的名字创建一个JS引擎侧的字符串值赋值给propertyName
Local<panda::StringRef> propertyName = panda::StringRef::NewFromUtf8(vm, propertyDescriptor.utf8name);
// 校验attributes是否有设置其它值,本样例中attributes默认设置的是0,因此writable,enumable和configable都是false
// writable: 属性是否可读可修改,enumable:属性是否允许遍历,configable:属性是否允许删除
bool writable = (propertyDescriptor.attributes & NATIVE_WRITABLE) != 0;
bool enumable = (propertyDescriptor.attributes & NATIVE_ENUMERABLE) != 0;
bool configable = (propertyDescriptor.attributes & NATIVE_CONFIGURABLE) != 0;
std::string fullName("");
// 本样例中getter和setter都是为null
if (propertyDescriptor.getter != nullptr || propertyDescriptor.setter != nullptr) {
// 省略部分代码……
} else if (propertyDescriptor.method != nullptr) { // 本样例中method非空,配置的是C++端对应的方法名
fullName += propertyDescriptor.utf8name;
// 调用 NapiNativeCreateFunction方法创建一个 JS 引擎侧的方法cbObj
Local<panda::JSValueRef> cbObj = NapiNativeCreateFunction(env, fullName.c_str(), propertyDescriptor.method, propertyDescriptor.data);
// 创建一个PropertyAttribute类型的attr实例
PropertyAttribute attr(cbObj, writable, enumable, configable);
// 调用JS引擎侧的JSObject对象的DefineProperty()方法完成对vm添加额外的属性操作
result = obj->DefineProperty(vm, propertyName, attr);
} else {
Local<panda::JSValueRef> val = LocalValueFromJsValue(propertyDescriptor.value);
PropertyAttribute attr(val, writable, enumable, configable);
result = obj->DefineProperty(vm, propertyName, attr);
}
Local<panda::ObjectRef> excep = panda::JSNApi::GetUncaughtException(vm);
if (!excep.IsNull()) {
HILOG_ERROR("ArkNativeObject::DefineProperty occur Exception");
panda::JSNApi::GetAndClearUncaughtException(vm);
}
return result;
}
NapiDefineProperty() 方法的内注释的比较清楚,主要是先根据 utf8name
创建一个 JS 引擎侧的方法名 propertyName
,然后判断 getter
和 setter
是否为空,本样例中它们都是空,接着判断 method
是否是空, 因为method
是我们在 hello.cpp
中定义的本地方法,所以条件成立进入当前分支语句中,fullName
表示 JS 侧的方法名,接着调用 NapiNativeCreateFunction() 方法创建一个 JS 引擎侧实例 cbObj
,然后创建一个 PropertyAttribute
类型的 attr
实例,最后调用 JS 引擎侧的 JSObject 对象的 DefineProperty() 方法完成对 vm
添加额外的属性操作,也就是说代码分析到这里, JS 引擎内部已经保存了 JS 侧的方法名 和 C++ 侧的方法的映射关系。
好了,到目前为止,JS 侧的方法和 C++ 方法的关联我们已经清楚了,接下来看如何调用到 C++ 的方法……
JS调用C++方法
目前已经清楚了 JS 引擎已经保存了 JS 侧的方法名 和 C++ 侧的方法的映射关系,当 JS 侧需要调用 C++ 方法时,代码如下所示:
import testNapi from 'libentry.so'
Text(this.message)
.fontSize(25)
.fontWeight(FontWeight.Bold)
.backgroundColor(Color.Pink)
.onClick(() => {
var result = testNapi.add(2, 3);
this.message = "OpenHarmony, value: " + result;
console.log(this.message);
})
笔者给 Text
添加了一个点击事件,当点击 Text
组件时执行了 testNapi.add(2, 3) 语句,JS 引擎解释执行到 testNapi.add() 方法时,就去查引擎内部维护的映射表,根据映射表可以找到 C++ 中定义的 Add() 方法,后续就是执行 C++ 中 Add() 方法的流程了……
码牛课堂也为了积极培养鸿蒙生态人才,让大家都能学习到鸿蒙开发最新的技术,针对一些在职人员、0基础小白、应届生/计算机专业、鸿蒙爱好者等人群,整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线。大家可以进行参考学习:https://qr21.cn/FV7h05
①全方位,更合理的学习路径:
路线图包括ArkTS基础语法、鸿蒙应用APP开发、鸿蒙能力集APP开发、次开发多端部署开发、物联网开发等九大模块,六大实战项目贯穿始终,由浅入深,层层递进,深入理解鸿蒙开发原理!②多层次,更多的鸿蒙原生应用:
路线图将包含完全基于鸿蒙内核开发的应用,比如一次开发多端部署、自由流转、元服务、端云一体化等,多方位的学习内容让学生能够高效掌握鸿蒙开发,少走弯路,真正理解并应用鸿蒙的核心技术和理念。③实战化,更贴合企业需求的技术点:
学习路线图中的每一个技术点都能够紧贴企业需求,经过多次真实实践,每一个知识点、每一个项目,都是码牛课堂鸿蒙研发团队精心打磨和深度解析的成果,注重对学生的细致教学,每一步都确保学生能够真正理解和掌握。
为了能让大家更好的学习鸿蒙(HarmonyOS NEXT)开发技术,这边特意整理了《鸿蒙开发学习手册》(共计890页),希望对大家有所帮助:https://qr21.cn/FV7h05
《鸿蒙开发学习手册》:https://qr21.cn/FV7h05
如何快速入门:
- 基本概念
- 构建第一个ArkTS应用
- ……
开发基础知识:https://qr21.cn/FV7h05
- 应用基础知识
- 配置文件
- 应用数据管理
- 应用安全管理
- 应用隐私保护
- 三方应用调用管控机制
- 资源分类与访问
- 学习ArkTS语言
- ……
基于ArkTS 开发:https://qr21.cn/FV7h05
- Ability开发
- UI开发
- 公共事件与通知
- 窗口管理
- 媒体
- 安全
- 网络与链接
- 电话服务
- 数据管理
- 后台任务(Background Task)管理
- 设备管理
- 设备使用信息统计
- DFX
- 国际化开发
- 折叠屏系列
- ……
鸿蒙开发面试真题(含参考答案):https://qr21.cn/FV7h05
大厂鸿蒙面试题::https://qr18.cn/F781PH
鸿蒙开发面试大盘集篇(共计319页):https://qr18.cn/F781PH
1.项目开发必备面试题
2.性能优化方向
3.架构方向
4.鸿蒙开发系统底层方向
5.鸿蒙音视频开发方向
6.鸿蒙车载开发方向
7.鸿蒙南向开发方向