HarmonyOS ArkUI实战开发-NAPI 加载原理(上)

诗林  金牌会员 | 2024-9-28 14:54:42 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 720|帖子 720|积分 2160

笔者在前 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 的写法如下:
  1. import testNapi from 'libentry.so';
复制代码
源码中通过关键字 import 引入了 libentry.so 库,那么它被编译成方舟字节码后是什么样子呢?打开 ets 目录里的 modules.abc,发现引入方式如下所示:
  1. 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 的构造方法源码如下图所示:
  1. ArkNativeEngine::ArkNativeEngine(EcmaVM* vm, void* jsEngine, bool isLimitedWorker) : NativeEngine(jsEngine), vm_(vm), topScope_(vm), isLimitedWorker_(isLimitedWorker) {
  2.     // 省略部分代码……
  3.     void* requireData = static_cast<void*>(this);
  4.     // 创建一个requireNapi()方法
  5.     Local<FunctionRef> requireNapi =
  6.         FunctionRef::New(
  7.             vm,
  8.             [](JsiRuntimeCallInfo *info) -> Local<JSValueRef> {
  9.                 // 获取moduleManager
  10.                 NativeModuleManager* moduleManager = NativeModuleManager::GetInstance();
  11.                 NativeModule* module = nullptr;
  12.                 // 调用NativeModuleManager的LoadNativeModule方法加载
  13.                 module = moduleManager->LoadNativeModule();
  14.                 return scope.Escape(exports);
  15.             },
  16.             nullptr,
  17.             requireData);
  18.     // 获取JS引擎的全局对象
  19.     Local<ObjectRef> global = panda::JSNApi::GetGlobalObject(vm);
  20.     // 创建JS引擎侧的方法名requireName
  21.     Local<StringRef> requireName = StringRef::NewFromUtf8(vm, "requireNapi");
  22.     // 注入 requireNapi 方法
  23.     global->Set(vm, requireName, requireNapi);
  24.     Init();
  25.     panda::JSNApi::SetLoop(vm, loop_);
  26. }
复制代码
由源码可知,ArkNativeEngine 在创建的时候接收了一个 EcmaVM 的实例 vm,并向 vm 内部的 global 对象注册了 requireNapi() 方法,当 vm 表明执行到 import testNapi from ‘@app:com.example.ho_0501_nodejs/entry/entry’; 时,vm 会调用 requireNapi() 方法,该方法内部调用了 NativieModuleManager 的 LoadNativeModule() 方法来加载 so 库,LoadNativeModule() 的源码如下:
  1. NativeModule* NativeModuleManager::LoadNativeModule(const char* moduleName,
  2.     const char* path, bool isAppModule, bool internal, const char* relativePath, bool isModuleRestricted)
  3. {
  4.     // 省略部分代码……
  5.     // 首先从缓存加载 NativeModule
  6.     NativeModule* nativeModule = FindNativeModuleByCache(key.c_str());
  7.     // 缓存不存在,从磁盘加载
  8.     if (nativeModule == nullptr) {
  9.         nativeModule = FindNativeModuleByDisk(moduleName, prefix_.c_str(), relativePath, internal, isAppModule);
  10.     }
  11.     // 省略部分代码……
  12.     return nativeModule;
  13. }
复制代码
LoadNativeModule() 方法先实验从缓存中取 NativeModuel,假如缓存不存在则从磁盘上加载,引擎首次加载 libentry.so 时缓存肯定是不存在的,因此直接看从磁盘加载的逻辑,FindNativeModuleByDisk() 源码如下所示:
  1. NativeModule* NativeModuleManager::FindNativeModuleByDisk(
  2.     const char* moduleName, const char* path, const char* relativePath, bool internal, const bool isAppModule)
  3. {
  4.     // 获取共享库的3个路径
  5.     char nativeModulePath[NATIVE_PATH_NUMBER][NAPI_PATH_MAX];
  6.     nativeModulePath[0][0] = 0;
  7.     nativeModulePath[1][0] = 0;
  8.     nativeModulePath[2][0] = 0;
  9.     if (!GetNativeModulePath(moduleName, path, relativePath, isAppModule, nativeModulePath, NAPI_PATH_MAX)) {
  10.         HILOG_WARN("get module '%{public}s' path failed", moduleName);
  11.         return nullptr;
  12.     }
  13.     // 从路径1加载共享库
  14.     char* loadPath = nativeModulePath[0];
  15.     LIBHANDLE lib = LoadModuleLibrary(moduleKey, loadPath, path, isAppModule);
  16.     if (lib == nullptr) {
  17.         // 路径1不存在,则从路径2加载
  18.         loadPath = nativeModulePath[1];
  19.         lib = LoadModuleLibrary(moduleKey, loadPath, path, isAppModule);
  20.     }
  21.     const uint8_t* abcBuffer = nullptr;
  22.     size_t len = 0;
  23.     if (lib == nullptr) {
  24.         // 从路径3加载
  25.         loadPath = nativeModulePath[2];
  26.         abcBuffer = GetFileBuffer(loadPath, moduleKey, len);
  27.         if (!abcBuffer) {
  28.             HILOG_ERROR("all path load module '%{public}s' failed", moduleName);
  29.             return nullptr;
  30.         }
  31.     }
  32.     return lastNativeModule_;
  33. }
复制代码
FindNativeModuleByDisk() 方法先调用 GetNativeModulePath() 方法获取 3 个本地路径,然后调用 LoadModuleLibrary() 方法实验从这 3 个路径加载 so,LoadModuleLibrary() 方法源码如下:
  1. LIBHANDLE NativeModuleManager::LoadModuleLibrary(std::string& moduleKey, const char* path,
  2.                                                  const char* pathKey, const bool isAppModule)
  3. {
  4.     // 先尝试从缓存加载
  5.     LIBHANDLE lib = nullptr;
  6.     lib = GetNativeModuleHandle(moduleKey);
  7.     if (lib != nullptr) {
  8.         // 缓存存在则直接返回
  9.         return lib;
  10.     }
  11.     // 以下代码是根据不同的平台做不同模式的加载操作
  12. #if defined(WINDOWS_PLATFORM)
  13.     lib = LoadLibrary(path);
  14. #elif defined(MAC_PLATFORM) || defined(__BIONIC__) || defined(LINUX_PLATFORM)
  15.     lib = dlopen(path, RTLD_LAZY);
  16. #elif defined(IOS_PLATFORM)
  17.     lib = nullptr;
  18. #else
  19.     if (isAppModule && IsExistedPath(pathKey)) {
  20.         Dl_namespace ns = nsMap_[pathKey];
  21.         lib = dlopen_ns(&ns, path, RTLD_LAZY);
  22.     } else {
  23.         lib = dlopen(path, RTLD_LAZY);
  24.     }
  25. #endif
  26.     EmplaceModuleLib(moduleKey, lib);
  27.     return lib;
  28. }
复制代码
LoadModuleLibrary() 方法里先实验从缓存中取,假如缓存有则直接返回否则根据不同的平台做不同方式的加载,以 LINUX_PLATFORM 平台为例,直接调用系统的 dlopen() 方法加载共享库并把句柄返回,dlopen() 方法简朴说明如下:
   dlopen() 方法是一个在 Unix-like 系统(包括 Linux)中用于动态加载共享库(.so 文件)的函数,它答应步伐在运行时动态地加载和卸载共享库,以及查找共享库中的符号(例如函数和变量)。当使用 dlopen() 方法加载一个共享库(.so 文件)时,它会执行该库中所有的全局构造函数(也称为初始化函数),这些构造函数通常用于初始化库中的静态数据或执行其他一次性设置。
  根据 dlopen() 方法的简介,hello.cpp 中添加了一个全局构造函数 RegisterEntryModule(),代码如下所示:
  1. #include <node_api.h>
  2. static napi_module demoModule = {
  3.     .nm_version = 1,
  4.     .nm_flags = 0,
  5.     .nm_filename = nullptr,
  6.     .nm_register_func = Init,
  7.     .nm_modname = "entry",
  8.     .nm_priv = ((void *)0),
  9.     .reserved = {0},
  10. };
  11. // 全局构造方法,当调用 dlopen() 方法加载时,该方法会首先调用
  12. extern "C" __attribute__((constructor)) void RegisterEntryModule(void) {
  13.     napi_module_register(&demoModule);
  14. }
复制代码
也就是说当调用 dlopen() 方法加载 libentry.so 时,会先调用 RegisterEntryModule() 方法,在该方法内部调用了 napi_module_register()napi_module_register() 源码如下:
  1. NAPI_EXTERN void napi_module_register(napi_module* mod)
  2. {
  3.     NativeModuleManager* moduleManager = NativeModuleManager::GetInstance();
  4.     NativeModule module;
  5.     // 根据传递进来的mod创建一个NativeModule对象,只使用了mod的部分属性
  6.     module.version = mod->nm_version;
  7.     module.fileName = mod->nm_filename;
  8.     module.name = mod->nm_modname;
  9.     module.registerCallback = (RegisterCallback)mod->nm_register_func;
  10.     // 调用NativeModuleManager的Register()方法注册NativeModule
  11.     moduleManager->Register(&module);
  12. }
复制代码
napi_module_register() 的方法很简朴,根据通报进来的 mod 构造一个 NativeModule 实例 module,然后调用 NativeModuleManager 的 Register() 方法注册它。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

诗林

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表