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

news2024/11/17 5:37:37

笔者在前 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> {
                // 获取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 个路径加载 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;
#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_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.鸿蒙南向开发方向

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1615260.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

重发布及路由策略实验

目录 一、实验拓扑 二、实验需求 1、按照图示配置 IP 地址&#xff0c;R1&#xff0c;R3&#xff0c;R4 上使用 loopback 口模拟业务网段 2、R2&#xff0c;R3 和R4运行 oSPF&#xff0c;各自协议内部互通 3、在 RIP 和 oSPF 间配置双向路由引入&#xff0c;要求除 R4 上的…

C++初阶学习第二弹——C++入门(下)

C入门&#xff08;上&#xff09;&#xff1a;C初阶学习第一弹——C入门&#xff08;上&#xff09;-CSDN博客 目录 一、引用 1.1 引用的实质 1.2 引用的用法 二、函数重载 三、内敛函数 四、auto关键字 五、总结 前言&#xff1a; 在上面一章我们已经讲解了C的一些基本…

【C语言】strstr函数刨析-----字符串查找

目录 一、strstr 函数介绍 ✨函数头文件&#xff1a; ✨函数原型&#xff1a; ✨函数解读 ✨功能演示 二、函数的原理以及模拟实现 ✨函数原理 ✨函数的模拟实现 三、strstr函数的注意事项 四、共勉 一、strstr 函数介绍 strstr函数是在一个字符串中查找另一个字符…

Leetcode 86. 分隔链表

题目链接&#xff1a; 86. 分隔链表 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/partition-list/description/ 题目&#xff1a; 给你一个链表的头节点 head 和一个特定值 x &#xff0c;请你对链表进行分隔&#xff0c;使得所有 小于 x 的节点都出…

CDN技术:全球化的数字内容快速分发系统

CDN技术&#xff1a;全球化的数字内容快速分发系统 在今天的互联网世界中&#xff0c;内容分发网络&#xff08;CDN&#xff09;技术起着至关重要的作用。它通过全球分布的服务器网络&#xff0c;快速、安全地将内容送达世界各地的用户&#xff0c;极大地提升了网页加载速度和…

SpringBoot+vue开发记录(二)

说明&#xff1a;本篇文章的主要内容为SpringBoot开发中后端的创建 项目创建: 1. 新建项目&#xff1a; 如下&#xff0c;这样简单创建就行了&#xff0c;JDK什么的就先17&#xff0c;当然1.8也是可以的&#xff0c;后面可以改。 这样就创建好了&#xff1a; 2. pom.xml…

【Android12】Bugreport实现原理

Bugreport实现原理 Bugreport Bugreport介绍 Android Bugreport是一个用于记录和收集 Android设备上系统信息、日志和调试信息的工具。 系统发生某些问题时&#xff0c;可以通过bugreport把系统当前时刻点&#xff08;运行BugRepot的时刻&#xff09;的系统相关的状态和信息…

Redis中BitMap在钉钉机器人中的应用

性能分析 数据库中有1000w用户&#xff0c;每个用户签到一次&#xff0c;对应两个字段 连续签到多少次 、签到时间。 签到时间字段占用10个字节&#xff0c;连续签到多少天 占用5个字节&#xff08;假设一个用户能活100年&#xff0c;每天都签到&#xff0c;一个用户最多签到3…

【爬取研招网指定学校专业信息】

前言 本文介绍了如何使用 Python 的 requests 库和 BeautifulSoup 库来爬取研究方向信息&#xff0c;并将其保存为 CSV 文件。爬取的网站为“中国研究生招生信息网”&#xff08;https://yz.chsi.com.cn/&#xff09;。代码从指定的专业目录页面爬取研究方向的相关信息&#x…

ROM修改进阶教程------services.jar文件过小 合并odex apk合并odex 几种方法步骤解析

在上期博文中有说明去卡密等相关操作。但在安卓低版本中有些services.jar文件过小。大小不足1K,这种是无法直接反编译的。我们简单使用压缩软件打开查看。其中文件小的里面没有dex文件。这种需要我们先合并odex使其成为一个可以直接反编译的文件再来操作。操作也可以用于其他a…

Day 31 贪心算法理论基础 455.分发饼干 376. 摆动序列 53. 最大子序和

贪心算法理论基础 ​ 贪心算法的本质&#xff1a;选择每一个阶段的局部最优&#xff0c;从而达到系统的整体最优&#xff1b; ​ 贪心的套路就是没有套路&#xff0c;最好的策略就是举反例&#xff0c;因为大多数时候并不要求严格证明&#xff0c;只需要得到普遍性结论即可&a…

Linux之ebpf(1)基础使用

Linux之ebpf(1)基础使用 Author: Once Day Date: 2024年4月20日 一位热衷于Linux学习和开发的菜鸟&#xff0c;试图谱写一场冒险之旅&#xff0c;也许终点只是一场白日梦… 漫漫长路&#xff0c;有人对你微笑过嘛… 全系列文章可以参考专栏&#xff1a;Linux基础知识_Once-D…

C语言:数据结构(单链表)

目录 1. 链表的概念及结构2. 实现单链表3. 链表的分类 1. 链表的概念及结构 概念&#xff1a;链表是一种物理存储结构上非连续、非顺序的存储结构&#xff0c;数据元素的逻辑顺序是通过链表的指针链接次序实现的。 链表的结构跟火车车厢相似&#xff0c;淡季时车次的车厢会相应…

Github账号注册

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

NC398 腐烂的苹果

腐烂的苹果 一个腐烂的苹果每分钟可以向上下左右四个方向扩展&#xff0c;扩展之后&#xff0c;又会有新的腐烂的苹果&#xff0c;一直去腐蚀好的苹果&#xff0c;求多少分钟后&#xff0c;网格中全是烂苹果。 第一次做这道题的时候&#xff0c;想到这道题考察的其实是多源BFS…

MATLAB——M文件

M文件 MATLAB允许编写两种程序文件- 脚本−脚本文件是扩展名为.m的程序文件。在这些文件中&#xff0c;您编写了一系列要一起执行的命令。脚本不接受输入&#xff0c;也不返回任何输出。它们对工作区中的数据进行操作。 函数−函数文件也是扩展名为.m的程序文件。函数可以接…

运营商三要素核验接口-手机实名验证API

运营商三要素核验接口是一种API&#xff08;Application Programming Interface&#xff0c;应用程序编程接口&#xff09;&#xff0c;主要用于通过互联网技术对接通信运营商的实名制数据库&#xff0c;以验证用户提供的手机号码、身份证号码、姓名这三项关键信息&#xff08;…

算法部署 | 使用TensorRT部署AlphaPose姿态估计算法

项目应用场景 面向 AlphaPose 姿态估计算法的推理加速场景&#xff0c;项目采用 TensorRT 进行 GPU 算法加速推理。 项目效果 项目细节 > 具体参见项目 README.md (1) 模型转换 python pytorch2onnx.py --cfg ./configs/coco/resnet/256x192_res50_lr1e-3_1x.yaml --chec…

【机器学习300问】74、如何理解深度学习中L2正则化技术?

深度学习过程中&#xff0c;若模型出现了过拟合问题体现为高方差。有两种解决方法&#xff1a; 增加训练样本的数量采用正则化技术 增加训练样本的数量是一种非常可靠的方法&#xff0c;但有时候你没办法获得足够多的训练数据或者获取数据的成本很高&#xff0c;这时候正则化技…

尚硅谷-JavaSE阶段考试与面试题库

一、基础题 1&#xff09;用最有效的的方法算出2称以8等于几 答案&#xff1a;2<<3 2&#xff09;两个对象a和b&#xff0c;请问ab和a.equals(b)有什么区别&#xff1f; ab&#xff1a;比较对象地址 a.equals(b)&#xff1a;如果a对象没有重写过equals方法&#xff0c…