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

2024-04-23 11:52

本文主要是介绍HarmonyOS ArkUI实战开发-NAPI 加载原理(上),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

笔者在前 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 负责解释执行的,每一个应用在进程初始化的时候都会创建一个方舟引擎实例 ArkNativeEngineArkNativeEngine 的构造方法源码如下图所示:

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> {// 获取moduleManagerNativeModuleManager* 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引擎侧的方法名requireNameLocal<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)
{// 省略部分代码……// 首先从缓存加载 NativeModuleNativeModule* 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 个路径加载 soLoadModuleLibrary() 方法源码如下:

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;
#elseif (isAppModule && IsExistedPath(pathKey)) {Dl_namespace ns = nsMap_[pathKey];lib = dlopen_ns(&ns, path, RTLD_LAZY);} else {lib = dlopen(path, RTLD_LAZY);}
#endifEmplaceModuleLib(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()方法注册NativeModulemoduleManager->Register(&module);
}

napi_module_register() 的方法很简单,根据传递进来的 mod 构造一个 NativeModule 实例 module,然后调用 NativeModuleManager 的 Register() 方法注册它。

📢:从创建 NativeModule 的源码可知,hello.cpp 里 demoModule 设置的 nm_flagsnm_privreserved 参数暂时是无用的。

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

如何快速入门:

  1. 基本概念
  2. 构建第一个ArkTS应用
  3. ……

开发基础知识:https://qr21.cn/FV7h05

  1. 应用基础知识
  2. 配置文件
  3. 应用数据管理
  4. 应用安全管理
  5. 应用隐私保护
  6. 三方应用调用管控机制
  7. 资源分类与访问
  8. 学习ArkTS语言
  9. ……

基于ArkTS 开发:https://qr21.cn/FV7h05

  1. Ability开发
  2. UI开发
  3. 公共事件与通知
  4. 窗口管理
  5. 媒体
  6. 安全
  7. 网络与链接
  8. 电话服务
  9. 数据管理
  10. 后台任务(Background Task)管理
  11. 设备管理
  12. 设备使用信息统计
  13. DFX
  14. 国际化开发
  15. 折叠屏系列
  16. ……

鸿蒙开发面试真题(含参考答案):https://qr21.cn/FV7h05

大厂鸿蒙面试题::https://qr18.cn/F781PH

鸿蒙开发面试大盘集篇(共计319页):https://qr18.cn/F781PH

1.项目开发必备面试题
2.性能优化方向
3.架构方向
4.鸿蒙开发系统底层方向
5.鸿蒙音视频开发方向
6.鸿蒙车载开发方向
7.鸿蒙南向开发方向

这篇关于HarmonyOS ArkUI实战开发-NAPI 加载原理(上)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/928719

相关文章

Android 悬浮窗开发示例((动态权限请求 | 前台服务和通知 | 悬浮窗创建 )

《Android悬浮窗开发示例((动态权限请求|前台服务和通知|悬浮窗创建)》本文介绍了Android悬浮窗的实现效果,包括动态权限请求、前台服务和通知的使用,悬浮窗权限需要动态申请并引导... 目录一、悬浮窗 动态权限请求1、动态请求权限2、悬浮窗权限说明3、检查动态权限4、申请动态权限5、权限设置完毕后

使用 sql-research-assistant进行 SQL 数据库研究的实战指南(代码实现演示)

《使用sql-research-assistant进行SQL数据库研究的实战指南(代码实现演示)》本文介绍了sql-research-assistant工具,该工具基于LangChain框架,集... 目录技术背景介绍核心原理解析代码实现演示安装和配置项目集成LangSmith 配置(可选)启动服务应用场景

关于Spring @Bean 相同加载顺序不同结果不同的问题记录

《关于Spring@Bean相同加载顺序不同结果不同的问题记录》本文主要探讨了在Spring5.1.3.RELEASE版本下,当有两个全注解类定义相同类型的Bean时,由于加载顺序不同,最终生成的... 目录问题说明测试输出1测试输出2@Bean注解的BeanDefiChina编程nition加入时机总结问题说明

在Java中使用ModelMapper简化Shapefile属性转JavaBean实战过程

《在Java中使用ModelMapper简化Shapefile属性转JavaBean实战过程》本文介绍了在Java中使用ModelMapper库简化Shapefile属性转JavaBean的过程,对比... 目录前言一、原始的处理办法1、使用Set方法来转换2、使用构造方法转换二、基于ModelMapper

Java实战之自助进行多张图片合成拼接

《Java实战之自助进行多张图片合成拼接》在当今数字化时代,图像处理技术在各个领域都发挥着至关重要的作用,本文为大家详细介绍了如何使用Java实现多张图片合成拼接,需要的可以了解下... 目录前言一、图片合成需求描述二、图片合成设计与实现1、编程语言2、基础数据准备3、图片合成流程4、图片合成实现三、总结前

基于Python开发PPTX压缩工具

《基于Python开发PPTX压缩工具》在日常办公中,PPT文件往往因为图片过大而导致文件体积过大,不便于传输和存储,所以本文将使用Python开发一个PPTX压缩工具,需要的可以了解下... 目录引言全部代码环境准备代码结构代码实现运行结果引言在日常办公中,PPT文件往往因为图片过大而导致文件体积过大,

MySQL中的MVCC底层原理解读

《MySQL中的MVCC底层原理解读》本文详细介绍了MySQL中的多版本并发控制(MVCC)机制,包括版本链、ReadView以及在不同事务隔离级别下MVCC的工作原理,通过一个具体的示例演示了在可重... 目录简介ReadView版本链演示过程总结简介MVCC(Multi-Version Concurr

nginx-rtmp-module构建流媒体直播服务器实战指南

《nginx-rtmp-module构建流媒体直播服务器实战指南》本文主要介绍了nginx-rtmp-module构建流媒体直播服务器实战指南,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有... 目录1. RTMP协议介绍与应用RTMP协议的原理RTMP协议的应用RTMP与现代流媒体技术的关系2

使用DeepSeek API 结合VSCode提升开发效率

《使用DeepSeekAPI结合VSCode提升开发效率》:本文主要介绍DeepSeekAPI与VisualStudioCode(VSCode)结合使用,以提升软件开发效率,具有一定的参考价值... 目录引言准备工作安装必要的 VSCode 扩展配置 DeepSeek API1. 创建 API 请求文件2.

C语言小项目实战之通讯录功能

《C语言小项目实战之通讯录功能》:本文主要介绍如何设计和实现一个简单的通讯录管理系统,包括联系人信息的存储、增加、删除、查找、修改和排序等功能,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录功能介绍:添加联系人模块显示联系人模块删除联系人模块查找联系人模块修改联系人模块排序联系人模块源代码如下