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

相关文章

Golang操作DuckDB实战案例分享

《Golang操作DuckDB实战案例分享》DuckDB是一个嵌入式SQL数据库引擎,它与众所周知的SQLite非常相似,但它是为olap风格的工作负载设计的,DuckDB支持各种数据类型和SQL特性... 目录DuckDB的主要优点环境准备初始化表和数据查询单行或多行错误处理和事务完整代码最后总结Duck

基于Python开发电脑定时关机工具

《基于Python开发电脑定时关机工具》这篇文章主要为大家详细介绍了如何基于Python开发一个电脑定时关机工具,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1. 简介2. 运行效果3. 相关源码1. 简介这个程序就像一个“忠实的管家”,帮你按时关掉电脑,而且全程不需要你多做

Redis主从/哨兵机制原理分析

《Redis主从/哨兵机制原理分析》本文介绍了Redis的主从复制和哨兵机制,主从复制实现了数据的热备份和负载均衡,而哨兵机制可以监控Redis集群,实现自动故障转移,哨兵机制通过监控、下线、选举和故... 目录一、主从复制1.1 什么是主从复制1.2 主从复制的作用1.3 主从复制原理1.3.1 全量复制

Java中的Opencv简介与开发环境部署方法

《Java中的Opencv简介与开发环境部署方法》OpenCV是一个开源的计算机视觉和图像处理库,提供了丰富的图像处理算法和工具,它支持多种图像处理和计算机视觉算法,可以用于物体识别与跟踪、图像分割与... 目录1.Opencv简介Opencv的应用2.Java使用OpenCV进行图像操作opencv安装j

Python中的随机森林算法与实战

《Python中的随机森林算法与实战》本文详细介绍了随机森林算法,包括其原理、实现步骤、分类和回归案例,并讨论了其优点和缺点,通过面向对象编程实现了一个简单的随机森林模型,并应用于鸢尾花分类和波士顿房... 目录1、随机森林算法概述2、随机森林的原理3、实现步骤4、分类案例:使用随机森林预测鸢尾花品种4.1

SpringBoot项目启动后自动加载系统配置的多种实现方式

《SpringBoot项目启动后自动加载系统配置的多种实现方式》:本文主要介绍SpringBoot项目启动后自动加载系统配置的多种实现方式,并通过代码示例讲解的非常详细,对大家的学习或工作有一定的... 目录1. 使用 CommandLineRunner实现方式:2. 使用 ApplicationRunne

Redis主从复制的原理分析

《Redis主从复制的原理分析》Redis主从复制通过将数据镜像到多个从节点,实现高可用性和扩展性,主从复制包括初次全量同步和增量同步两个阶段,为优化复制性能,可以采用AOF持久化、调整复制超时时间、... 目录Redis主从复制的原理主从复制概述配置主从复制数据同步过程复制一致性与延迟故障转移机制监控与维

SpringBoot项目删除Bean或者不加载Bean的问题解决

《SpringBoot项目删除Bean或者不加载Bean的问题解决》文章介绍了在SpringBoot项目中如何使用@ComponentScan注解和自定义过滤器实现不加载某些Bean的方法,本文通过实... 使用@ComponentScan注解中的@ComponentScan.Filter标记不加载。@C

springboot 加载本地jar到maven的实现方法

《springboot加载本地jar到maven的实现方法》如何在SpringBoot项目中加载本地jar到Maven本地仓库,使用Maven的install-file目标来实现,本文结合实例代码给... 在Spring Boothttp://www.chinasem.cn项目中,如果你想要加载一个本地的ja

SpringCloud配置动态更新原理解析

《SpringCloud配置动态更新原理解析》在微服务架构的浩瀚星海中,服务配置的动态更新如同魔法一般,能够让应用在不重启的情况下,实时响应配置的变更,SpringCloud作为微服务架构中的佼佼者,... 目录一、SpringBoot、Cloud配置的读取二、SpringCloud配置动态刷新三、更新@R