笔者在前 6 小结讲述了NAPI 的基本使用,包括同步和异步实现,本节笔者从源码的角度简单讲解一下NAPI 的加载流程,源码版本为 ArkUI 4.0 Release 版本。
hap 工程结构
工程配置签名后打一个 hap 包出来,然后解压该 hap 文件,目录如下所示:
根据解压后的文件目录可知,hello.cpp
文件被编译成了不同平台的动态库 libentry.so
,ets 目录存放的是源码编译后的产物 abc 字节码和 map 文件,resources 是打包后的应用资源,比如字符串、图片啥的。当把 hap 安装到设备上时,本质上就是对其解压和拷贝,系统最终把 libentry.so
拷贝到如 app/bundlename/libs/arm64-v8a/libentry.so 的路径下。
动态库加载原理
编译后的 libentry.so
库是什么时机加载的呢?我们在 Index.ets
源码中引入 libentry.so
的写法如下:
import testNapi from 'libentry.so';
源码中通过关键字 import 引入了 libentry.so
库,那么它被编译成方舟字节码后是什么样子呢?打开 ets 目录里的 modules.abc
,发现引入方式如下所示:
import testNapi from '@app:com.example.ho_0501_nodejs/entry/entry';
根据编译前后的对比可以发现,引入方式由 from libentry.so 转变成了 from @app:com.example.ho_0501_nodejs/entry/entry,在前文笔者提到过方舟字节码是由方舟引擎内部的 EcmaVM
负责解释执行的,每一个应用在进程初始化的时候都会创建一个方舟引擎实例 ArkNativeEngine
,ArkNativeEngine
的构造方法源码如下图所示:
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> {
// 获取moduleManager
NativeModuleManager* moduleManager = NativeModuleManager::GetInstance();
NativeModule* module = nullptr;
// 调用NativeModuleManager的LoadNativeModule方法加载
module = moduleManager->LoadNativeModule();
return scope.Escape(exports);
},
nullptr,
requireData);
// 获取JS引擎的全局对象
Local<ObjectRef> global = panda::JSNApi::GetGlobalObject(vm);
// 创建JS引擎侧的方法名requireName
Local<StringRef> requireName = StringRef::NewFromUtf8(vm, "requireNapi");
// 注入 requireNapi 方法
global->Set(vm, requireName, requireNapi);
Init();
panda::JSNApi::SetLoop(vm, loop_);
}
由源码可知,ArkNativeEngine
在创建的时候接收了一个 EcmaVM
的实例 vm
,并向 vm
内部的 global
对象注册了 requireNapi() 方法,当 vm
解释执行到 import testNapi from ‘@app:com.example.ho_0501_nodejs/entry/entry’; 时,vm
会调用 requireNapi() 方法,该方法内部调用了 NativieModuleManager
的 LoadNativeModule() 方法来加载 so
库,LoadNativeModule() 的源码如下:
NativeModule* NativeModuleManager::LoadNativeModule(const char* moduleName,
const char* path, bool isAppModule, bool internal, const char* relativePath, bool isModuleRestricted)
{
// 省略部分代码……
// 首先从缓存加载 NativeModule
NativeModule* nativeModule = FindNativeModuleByCache(key.c_str());
// 缓存不存在,从磁盘加载
if (nativeModule == nullptr) {
nativeModule = FindNativeModuleByDisk(moduleName, prefix_.c_str(), relativePath, internal, isAppModule);
}
// 省略部分代码……
return nativeModule;
}
LoadNativeModule() 方法先尝试从缓存中取 NativeModuel
,如果缓存不存在则从磁盘上加载,引擎首次加载 libentry.so
时缓存肯定是不存在的,因此直接看从磁盘加载的逻辑,FindNativeModuleByDisk() 源码如下所示:
NativeModule* NativeModuleManager::FindNativeModuleByDisk(
const char* moduleName, const char* path, const char* relativePath, bool internal, const bool isAppModule)
{
// 获取共享库的3个路径
char nativeModulePath[NATIVE_PATH_NUMBER][NAPI_PATH_MAX];
nativeModulePath[0][0] = 0;
nativeModulePath[1][0] = 0;
nativeModulePath[2][0] = 0;
if (!GetNativeModulePath(moduleName, path, relativePath, isAppModule, nativeModulePath, NAPI_PATH_MAX)) {
HILOG_WARN("get module '%{public}s' path failed", moduleName);
return nullptr;
}
// 从路径1加载共享库
char* loadPath = nativeModulePath[0];
LIBHANDLE lib = LoadModuleLibrary(moduleKey, loadPath, path, isAppModule);
if (lib == nullptr) {
// 路径1不存在,则从路径2加载
loadPath = nativeModulePath[1];
lib = LoadModuleLibrary(moduleKey, loadPath, path, isAppModule);
}
const uint8_t* abcBuffer = nullptr;
size_t len = 0;
if (lib == nullptr) {
// 从路径3加载
loadPath = nativeModulePath[2];
abcBuffer = GetFileBuffer(loadPath, moduleKey, len);
if (!abcBuffer) {
HILOG_ERROR("all path load module '%{public}s' failed", moduleName);
return nullptr;
}
}
return lastNativeModule_;
}
FindNativeModuleByDisk() 方法先调用 GetNativeModulePath() 方法获取 3 个本地路径,然后调用 LoadModuleLibrary() 方法尝试从这 3 个路径加载 so
,LoadModuleLibrary() 方法源码如下:
LIBHANDLE NativeModuleManager::LoadModuleLibrary(std::string& moduleKey, const char* path,
const char* pathKey, const bool isAppModule)
{
// 先尝试从缓存加载
LIBHANDLE lib = nullptr;
lib = GetNativeModuleHandle(moduleKey);
if (lib != nullptr) {
// 缓存存在则直接返回
return lib;
}
// 以下代码是根据不同的平台做不同模式的加载操作
#if defined(WINDOWS_PLATFORM)
lib = LoadLibrary(path);
#elif defined(MAC_PLATFORM) || defined(__BIONIC__) || defined(LINUX_PLATFORM)
lib = dlopen(path, RTLD_LAZY);
#elif defined(IOS_PLATFORM)
lib = nullptr;
#else
if (isAppModule && IsExistedPath(pathKey)) {
Dl_namespace ns = nsMap_[pathKey];
lib = dlopen_ns(&ns, path, RTLD_LAZY);
} else {
lib = dlopen(path, RTLD_LAZY);
}
#endif
EmplaceModuleLib(moduleKey, lib);
return lib;
}
LoadModuleLibrary() 方法里先尝试从缓存中取,如果缓存有则直接返回否则根据不同的平台做不同方式的加载,以 LINUX_PLATFORM
平台为例,直接调用系统的 dlopen() 方法加载共享库并把句柄返回,dlopen() 方法简单说明如下:
dlopen() 方法是一个在 Unix-like 系统(包括 Linux)中用于动态加载共享库(.so 文件)的函数,它允许程序在运行时动态地加载和卸载共享库,以及查找共享库中的符号(例如函数和变量)。当使用 dlopen() 方法加载一个共享库(.so 文件)时,它会执行该库中所有的全局构造函数(也称为初始化函数),这些构造函数通常用于初始化库中的静态数据或执行其他一次性设置。
根据 dlopen() 方法的简介,hello.cpp
中添加了一个全局构造函数 RegisterEntryModule(),代码如下所示:
#include <node_api.h>
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},
};
// 全局构造方法,当调用 dlopen() 方法加载时,该方法会首先调用
extern "C" __attribute__((constructor)) void RegisterEntryModule(void) {
napi_module_register(&demoModule);
}
也就是说当调用 dlopen() 方法加载 libentry.so
时,会先调用 RegisterEntryModule() 方法,在该方法内部调用了 napi_module_register(),napi_module_register() 源码如下:
NAPI_EXTERN void napi_module_register(napi_module* mod)
{
NativeModuleManager* moduleManager = NativeModuleManager::GetInstance();
NativeModule module;
// 根据传递进来的mod创建一个NativeModule对象,只使用了mod的部分属性
module.version = mod->nm_version;
module.fileName = mod->nm_filename;
module.name = mod->nm_modname;
module.registerCallback = (RegisterCallback)mod->nm_register_func;
// 调用NativeModuleManager的Register()方法注册NativeModule
moduleManager->Register(&module);
}
napi_module_register() 的方法很简单,根据传递进来的 mod
构造一个 NativeModule
实例 module
,然后调用 NativeModuleManager
的 Register() 方法注册它。
📢:从创建 NativeModule
的源码可知,hello.cpp
里 demoModule 设置的 nm_flags
,nm_priv
,reserved
参数暂时是无用的。
Register() 方法源码如下所示:
void NativeModuleManager::Register(NativeModule* nativeModule)
{
std::lock_guard<std::mutex> lock(nativeModuleListMutex_);
// 创建链表并给lastNativeModule_赋值
if (!CreateNewNativeModule()) {
HILOG_ERROR("create new nativeModule failed");
return;
}
// 把nativeModule的值传递给尾结点
lastNativeModule_->version = nativeModule->version;
lastNativeModule_->fileName = nativeModule->fileName;
lastNativeModule_->isAppModule = isAppModule_;
lastNativeModule_->name = moduleName;
lastNativeModule_->refCount = nativeModule->refCount;
lastNativeModule_->registerCallback = nativeModule->registerCallback;
lastNativeModule_->getJSCode = nativeModule->getJSCode;
lastNativeModule_->getABCCode = nativeModule->getABCCode;
lastNativeModule_->next = nullptr;
lastNativeModule_->moduleLoaded = true;
}
// 创建一个链表并给尾结点lastNativeModule_赋值,链表头结点为firstNativeModule_,
bool NativeModuleManager::CreateNewNativeModule()
{
if (firstNativeModule_ == lastNativeModule_ && lastNativeModule_ == nullptr) {
firstNativeModule_ = new NativeModule();
if (firstNativeModule_ == nullptr) {
HILOG_ERROR("first NativeModule create failed");
return false;
}
lastNativeModule_ = firstNativeModule_;
} else {
auto next = new NativeModule();
if (next == nullptr) {
HILOG_ERROR("next NativeModule create failed");
return false;
}
if (lastNativeModule_) {
lastNativeModule_->next = next;
lastNativeModule_ = lastNativeModule_->next;
}
}
return true;
}
Register() 方法的执行逻辑很清楚,先调用 CreateNewNativeModule() 创建一个NativeModule 链表,该链表头结点是 firstNativeModule_
,尾结点是 lastNativeModule_
,最后把传递进来的 nativeModule
的值赋值给尾结点 lastNativeModule_
,总结起来就是 Register() 方法负责把传递进来的 NativeModule
加入链表的末尾。
小结
由于篇幅原因,本节笔者简单讲解了 JS 引擎解释执行到 import
语句时会由 NativieModuleManager 加载动态库,加载的过程就是把 NativeModule 添加到 NativieModuleManager 的内部链接末尾,下一小节笔者介绍 JS 引擎解释执行 testNapi.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.鸿蒙南向开发方向