用 Addon 增强 Node.js 和 Electron 应用的原生能力

前言

Node.js Addon 是 Node.js 中为 JavaScript 环境提供 C/C++ 交互能力的机制。其形态十分类似 Java 的 JNI,都是通过提供一套 C/C++ SDK,用于在 C/C++ 中创建函数方法、进行数据转换,以便 JavaScript / Java 等语言进行调用。这样编写的代码通常叫做 Bindings。

此外还有基于 C ABI Calling Convention (例如 stdcall / System-V 等标准) 直接进行跨语言调用的方案,例如 Rust FFI、Python 的 ctypes、Node.js 的 ffi 包等。这两者的差别在于 Rust 等原生语言是直接针对平台来将函数调用编译为机器码,而 ctypes 和 ffi 包则是基于 libffi 动态生成机器码来完成函数调用的。和 Node.js Addon 的差别则在于调用和类型转换的开销上。

本文将围绕 Node.js Addon 进行介绍,即创建一个 Bindings 来增强 Node.js 或 Electron 应用的原生能力,使其可以和系统进行交互,或者使用一些基于 C/C++ 编写的第三方库。

Node.js Electron 的关系

Electron 在主进程和渲染进程中都包含了完整的 Node.js 环境,因此本文既适用于 Node.js 程序,也适用于 Electron 程序。

Node.js Addon 的类型

在 Node.js 的 Addon,有三种类型:

54dbdf1a2152fd46607fbefdad021e86.png

本文主要介绍 Node-API 的原理,以及以 node-addon-api 作为例子。

Node-API 基本原理

9ec850ac30e6f4e3b55173fc0446e6f7.png

Node.js 本质上是一个动态链接库(即 Windows 下的 .dll 文件、MacOS 下的 .dylib 文件、Linux 下的 .so 文件),只不过在分发时会将文件的扩展名改为 .node

加载

Node.js Addon 通常通过 CommonJS 的 require 函数进行导入和初始化。require 在被 .node 扩展名路径作为参数进行调用的情况下,最终会利用 dlopen(Windows 下是 LoadLibrary)方法来动态加载这个以 .node 扩展名的动态链接库:

a7df3f7402cd8a18fe174dba16a5d675.png

初始化

以 https://github.com/nodejs/node-addon-examples/blob/main/1_hello_world/napi/hello.c 作为参考:

static napi_value Init(napi_env env, napi_value exports) {
  napi_status status;
  napi_property_descriptor desc = DECLARE_NAPI_METHOD("hello", Method);
  status = napi_define_properties(env, exports, 1, &desc);
  assert(status == napi_ok);
  return exports;
}

NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

NAPI_MODULE 宏用来绑定一个 C 函数作为初始化函数。这个函数中可以用来给模块的 exports 对象添加所需要的功能。

例如上述的代码中,给 exports 添加了一个叫做 hello 的函数。这样一来,我们在 Node.js 中 require 这个模块之后,就能获得到一个包含 hello 函数的 exports 对象:

ea1c84af9017b88cae4f62b4e95bf250.png

调用

以 https://github.com/nodejs/node-addon-examples/blob/main/1_hello_world/napi/hello.c 作为参考:

static napi_value Method(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value world;
  status = napi_create_string_utf8(env, "world", 5, &world);
  assert(status == napi_ok);
  return world;
}

Method 本身是一个 C 函数,接受 napi_env 作为 JavaScript 的上下文信息。napi_callback_info 作为当前函数调用的信息,例如函数参数等。返回一个 napi_value 作为函数的返回结果。

从这个函数的例子中可以看到,在 C 中是可以获取到函数的调用参数,并且产生一个值作为函数的返回结果。稍后我们会以 node-addon-api 作为例子来具体介绍其编写方式。

c52925e33badd8e36c14c387e717c02d.png

模块编写指南

本节介绍使用 C++ 配合 node-addon-api 开发模块时常见的一些模式和样板代码,仅供参考。

更多用法详见官方文档:https://github.com/nodejs/node-addon-api/blob/main/doc/hierarchy.md

模块初始化

使用 NODE_API_MODULE 宏绑定一个 C++ 函数进行模块初始化:

Napi::Object Init(Napi::Env env, Napi::Object exports) {
  exports.Set(Napi::String::New(env, "hello"),
              Napi::Function::New(env, Method));
  return exports;
}

NODE_API_MODULE(hello, Init)
  • 其中 Napi::Env 是对 napi_env 的封装,代表一个 JavaScript 上下文,大部分和 JavaScript 交互的场景都需要这个上下文,可以保存起来以供下次使用(但是不要跨线程使用)。

  • Napi::Object exports 则是这个模块的 exports 对象,可以把想要给 JavaScript 暴露的值和函数都设置到这个上面。

创建 JavaScript 函数

首先需要创建一个如下函数签名的 C++ 函数:

Napi::Value Add(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();
  double arg0 = info[0].As<Napi::Number>().DoubleValue();
  double arg1 = info[1].As<Napi::Number>().DoubleValue();
  Napi::Number num = Napi::Number::New(env, arg0 + arg1);
  return num;
}

其中函数的返回值可以是任何派生自 Napi::Value 的类型,也可以是 Napi::Value 本身。

获取函数参数

通过 Napi::CallbackInfo& 来获取函数参数,例如 info[0] 代表第一个参数。

info[n] 会获取一个 Napi::Value 值,我们需要调用它的 As<T> 方法来转换为具体的值,我们才能将它继续转换为 C/C++ 可用的数据类型。例如,我们希望将函数的第一个参数转换为字符串,我们需要经过两个步骤:

  1. 将 Napi::Value 转换为 Napi::String:

Napi::String js_str = info[0].As<Napi::String>();
  1. 将 Napi::String 转换为 std::string

std::string cpp_str = js_str.Utf8Value();

其他数据类型例如 Napi::NumberNapi::Buffer<T> 均有类似的方法。

返回函数结果

我们可以直接创建一个 JavaScript 值并在 C++ 函数中返回。具体创建值的方法详见下一小节。

创建 JavaScript 值

我们可以利用各种实例化方法,来从 C/C++ 的数据类型中创建 JavaScript 的值,下面举几个常见的例子。

创建字符串
Napi::String::New(env, "字符串内容")
创建数字
Napi::Number::New(env, 123)
创建 Buffer

创建 Buffer 是一个有风险的操作。Node-API 提供了两种创建方式:

  • 提供一个指针和数据长度,创建一个数据的拷贝

    • ✅ 安全,首选这种方法

    • ✅ v8 会负责这个 Buffer 的垃圾回收

Napi::Buffer::Copy(napi_env env, const T* data, size_t length)
  • 直接基于指针和数据长度创建一个 External Buffer

    • ⚠️ 同一个指针(相同的内存地址)只能创建一个 Buffer,重复创建会引起错误

    • ⚠️ v8 / Node.js 不负责这个 Buffer 的内存管理

Napi::Buffer::New(napi_env env, const T* data, size_t length)

异步代码

异步函数

异步函数通常用于实现一些异步 IO 任务、事件,例如实现一个异步网络请求库的绑定。

异步函数通常有两种实现方式:回调 和 Promise。

同线程回调

同线程回调的使用场景比较少:

  • 使用了 libuv 来运行了一些异步任务,并且这个异步任务会在 libuv 主线程唤醒事件循环来返回结果,这时候可以比较安全地直接进行同线程回调。但是要求事先把 Napi::Env 保存在一个地方。

  • 实现一个函数的时候,在实现中直接同步调用一个 Napi::Function。

获取函数

通常我们会从函数调用的参数中获取到 Napi::Function,一般来说我们需要在当次调用就把这个函数给使用掉,避免后续被 v8 GC 回收。

持久化函数

如果我们确实需要在之后的其他时机去使用函数,我们需要将它通过 Napi::Persistent 持久化:

Napi::FunctionReference func_persist = Napi::Persistent(func);

使用时,可以作为一个正常的函数去使用。

调用函数

无论是 Napi::Function 还是 Napi::FunctionReference,我们都可以通过 Call 方法来调用:

Napi::Value ret_val = func_persist.Call({
  Napi::String::New(env, "Arg0")
});
跨线程回调

跨线程回调是比较常见使用场景,因为我们通常会想在另外一个线程调用 JavaScript 函数。

使用线程安全函数 (ThreadSafeFunction)

为了在其他线程中调用 JavaScript 函数,我们需要基于 Napi::Function 去创建一个 Napi::ThreadSafeFunction

Napi::ThreadSafeFunction tsfn = Napi::ThreadSafeFunction::New(
  env,                     // Napi::Env
  info[0].As<Function>(),  // JavaScript 函数
  "handler",               // 异步函数的名称,用于调试的识别
  0,                       // 队列最大大小,通常指定为 0 代表没有限制。如果队列已满则可能会导致调用时阻塞。
  1                        // 初始线程数量,通常指定为 1。实际上是作为内存管理使用。可参考这篇文档。
);

接着就可以把 tsfn 保存在任何位置,并且并不需要同时保存一份 Napi::Env

调用线程安全函数

调用线程函数有两种形式,一种是同步调用,另一种是异步调用。

同步调用

同步调用指的是如果我们限制了 ThreadSafeFunction 的队列大小,并对其进行了多次调用,从而创建了许多调用任务,则会导致队列已满,调用就会被阻塞,直到成功插入队列后返回结果。

这是进行一次同步调用的例子:

const char* value = "hello world";
napi_status status = tsfn.BlockingCall(value, [](Napi::Env env, Napi::Function callback, const char* value) {
  Napi::String arg0 = Napi::String::New(env, value);
  callback.Call({ arg0 });
});

这样一来就能顺利地在任意线程去调用 JavaScript 函数。

但是我们发现,实际上我们并不能同步地获取函数调用的返回结果。并且 Node-API 或者 node-addon-api 都没有提供这么一种机制。但是我们可以借助 libuv 的信号量来达到这个目的。

uv_sem_t sem;
uv_sem_init(&sem, 0);
const char* value = "hello world";
Napi::Value ret_val;
napi_status status = tsfn.BlockingCall(value, [&ret_val](Napi::Env env, Napi::Function callback, const char* value) {
  Napi::String arg0 = Napi::String::New(env, value);
  *ret_val = callback.Call({ arg0 });
  uv_sem_post(&sem);
});
uv_sem_wait(&sem);

// 直至 JavaScript 运行结束并返回结果,才会走到这里
// 这里就可以直接使用 ret_val 了

异步调用

异步调用则会在队列已满时直接返回错误状态而不进行函数调用。除此之外的使用方法同 “同步调用” 完全一致:

const char* value = "hello world";
napi_status status = tsfn.NonBlockingCall(value, [](Napi::Env env, Napi::Function callback, const char* value) {
  Napi::String arg0 = Napi::String::New(env, value);
  callback.Call({ arg0 });
});
Promise
C++ 中创建 Promise 给 JavaScript 使用

我们通常会需要在 C++ 中实现异步函数。除了直接用上面已经介绍的基于回调的方法之外,我们还可以直接在 C++ 中创建一个 Promise。

Promise 只支持同 线程 调用

由于 Promise 并未提供跨线程 Resolve 的方式,因此如果希望在其他线程对 Promise 进行 Resolve 操作,则需要结合 libuv 来实现。此方法比较繁琐,建议转而使用跨线程回调函数。如果读者感兴趣,后续本文可以补充相关内容。

我们可以直接创建一个 Promise,并在函数中返回:

Napi::Value YourFunction(const Napi::CallbackInfo& info) {
  Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(info.Env());

  // 我们可以把 env 和 Napi::Promise::Deferred 保存在任何地方。
  // deferred_ 会在 Resolve 或者 Reject 之后释放。
  env_ = info.Env();
  deferred_ = deferred;

  return deferred.Promise();
}

接着我们可以在其他地方调用 Napi::Promise::Deferred 来完成 Promise。注意,这里一定需要在主线程中调用:

// 返回成功结果
deferred_.Resolve(Napi::String::New(info.Env(), "OK"));
// 返回错误
deferred_.Reject(Napi::String::New(info.Env(), "Error"));
C++ 中使用来自 JavaScript 的 Promise

由于 Node-API 或者 node-addon-api 均没有提供使用 Promise 的封装,因此我们需要像在 JavaScript 中通过 .then 手动使用 Promise 的方式,在 C++ 中使用 Promise。

// 首先需要定义两个函数,用来接受 Promise 成功和失败
Napi::Value ThenCallback(const Napi::CallbackInfo &info) { 
  Napi::Value result = info[0];
  // result 是 Promise 的返回结果
  return info.Env().Undefined();
}
Napi::Value CatchCallback(const Napi::CallbackInfo &info) { 
  Napi::Value error = info[0];
  // error 是 Promise 的错误信息
  return info.Env().Undefined();
}

Napi::Promise promise = async_function.Call({}).As<Napi::Promise>()
Napi::Value then_func = promise.Get("then").As<Napi::Function>();
then_func.Call(promise, { Napi::Function::New(env, ThenCallback, "then_callback") });
Napi::Value catch_func = promise.Get("catch").As<Napi::Function>();
catch_func.Call(promise, { Napi::Function::New(env, CatchCallback, "catch_callback") });

显然这种使用方式是比较繁琐的,我们也可以通过一些办法使其可以将 C++ Lambda 作为回调函数来使用,但是本文暂时不涉及这部分内容。

异步任务

异步任务通常是利用 libuv 提供的线程池来运行一些 CPU 密集型的工作。而对于一些跨线程异步回调的 Bindings 实现则直接使用 ThreadSafeFunction 即可。

具体使用可以参考:https://github.com/nodejs/node-addon-api/blob/main/doc/async_worker.md

Node-API 的构建

基本构建配置

Node.js Addon 通常使用 node-gyp 构建,这是一个基于 Google 的 gyp 构建系统实现的构建工具。至于为何是 gyp,因为 Node.js 是基于 gyp 构建的。

我们来看一个 node-addon-api 项目的构建配置,以 bindings.gyp 命名:

{
  "targets": [
    {
      "target_name": "hello",
      "cflags!": [ "-fno-exceptions" ],
      "cflags_cc!": [ "-fno-exceptions" ],
      "sources": [ "hello.cc" ],
      "include_dirs": [
        "<!@(node -p "require('node-addon-api').include")"
      ],
      'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ],
    }
  ]
}

具体配置可以参考官方使用文档:https://gyp.gsrc.io/docs/UserDocumentation.md

一些常识:

  • "sources" 中需要包含所有 C/C++ 代码文件,不需要包含头文件

  • "<!@(node -p "require('node-addon-api').include")" 在使用 Node-API 还是 node-addon-api 的情况下是不同的。

  • "target_name" 通常需要修改为你希望使用的扩展名称,它会影响编译产物的名称。

常用构建命令

  • node-gyp rebuild 重新构建,会清理掉已有的构建缓存,推荐每次都使用这个命令来构建产物,避免出现奇怪的问题

    • 可以添加 --arch <ARCH> 参数来指定构建的目标架构,例如希望构建一个 32 位的产物,则可以使用 --arch ia32 来构建。

  • node-gyp clean 清理构建缓存。如果希望使用 node-gyp build 来进行构建的话,需要善用 clean 功能。

实用构建配置

添加头文件目录
'include_dirs': [
  'win32/x64/include'
]
在 Windows 下进行动态链接 / 静态链接
'libraries': [
  'some_library.lib'
]
  • 对于动态链接,需要指定 .dll 对应的 .lib 文件,并在分发的时候将 .dll 放在 .node 相同的目录下。

  • 对于静态链接,则直接指定 .lib 文件即可。但是在 Node.js Addon 中进行静态链接是一个比较费劲的事情,因为通常涉及到对其他静态依赖的管理,需要谨慎选择此方案。

在 Windows 下设置 C++ 版本
'msvs_settings': {
  'VCCLCompilerTool': {
    'AdditionalOptions': [
      '/std:c++20'
    ]
  }
}
在 Windows MSVC 下构建支持代码文件中的 UTF-8 字符(中文注释等)

本质上是给 MSVC 的编译器添加一个 /utf-8 参数

'msvs_settings': {
  'VCCLCompilerTool': {
    "AdditionalOptions": [
      '/utf-8'
    ]
  }
}
在 MacOS 下进行动态链接 / 静态链接
'link_settings': {
  'libraries': [
    '-L<动态库或静态库所在的文件夹>',
    '-l<动态库名称>'
  ]
}
在 MacOS 下引入系统 Framework 依赖
'libraries': [
  '-framework MediaPlayer',
  '-framework Cocoa',
]
在 MacOS 下设置 C++ 版本
"cflags_cc": [
  "-std=c++20"
]
在 MacOS Xcode 的 Release 构建下生成 .dSYM 调试文件
'xcode_settings': {
  'DEBUG_INFORMATION_FORMAT': 'dwarf-with-dsym'
}
使 MacOS 下的 addon 能够使用同目录下的动态库 / Framework
'link_settings': {
  'libraries': [
    '-Wl,-rpath,@loader_path',
    ## 此外,还可以设置到任何相对于 .node 文件的其他目录下
    '-Wl,-rpath,@loader_path/../../darwin/arm64',
  ]
},

但是这也要求 .dylib 文件支持该功能,可以通过 otool -D <你的动态链接库位置>.dylib 的返回结果来检查:

<你的动态链接库>.dylib:
@rpath/<链接库名称>.dylib

如果文件名往前的开头是 @rpath,则意味着支持该功能。如果不是,则可以使用 install_name_tool 来修改动态链接库使其支持:

install_name_tool -id "@rpath/<链接库名称>.dylib" <你的动态链接库位置>.dylib
在 MacOS 下支持 Objective-C 和 C++ 混编
'xcode_settings': {
  'OTHER_CFLAGS': [
    '-ObjC++'
  ]
}

开发&分发&使用

项目文件组织

通常来说,我们可以用下面的文件夹结构来扁平地组织我们的 addon 文件:

.
├── node_modules                   ## npm 依赖
├── build                          ## 构建路径
│   ├── Release                   ## Release 产物路径
│       ├── myaddon.node          ## addon 产物
│       ├── myaddon.node.dSYM     ## addon 的符号文件
├── binding.gyp                    ## 构建配置
├── addon.cc                       ## Addon 的 C++ 源码
├── index.js                       ## Addon 的 JavaScript 源码
├── index.d.ts                     ## Addon 的 TypeScript 类型(下方会介绍)
└── package.json                   ## Addon 的 package.json 文件

当然我们也可以把 JavaScript 源码和 C++ 源码分别放入不同的文件夹,只需要修改对应的构建配置和 package.json 即可。

编写 index.js - 使用 bindings 包

一般来说我们会直接在 C++ 中实现大部分逻辑,JavaScript 文件只用来引入 .node 文件。由于 Node.js Addon 存在各种不同的方案、构建配置,因此 .node 文件产物的位置可能也会因此不同,所以我们需要借助一个第三方 npm 包来自动为我们寻找 .node 文件的位置:

https://github.com/TooTallNate/node-bindings

通过 bindings,我们的 index.js 仅需一行代码就能自动获取并导出 .node 模块:

module.exports = require('bindings')('binding.node')

同时保证 package.json 的 main 配置为我们的 index.js:

{
  // ...
  "main": "index.js"
  // ...
}

为 Addon 添加 TypeScript 类型

添加 TypeScript 类型,最简单的方式只需要创建一个 index.d.ts 文件,并在其中声明在 C++ 代码中创建的函数们即可:

export interface FooOptions {
  bar: string
}

export function foo(options: FooOptions)

并在 package.json 添加一行参数用于指向类型文件:

{
  // ...
  "types": "index.d.ts"
  // ...
}

大部分情况下,这个方法就可以给你的 Node.js Addon 声明类型。

分发形式

安装时编译

一种方式是在使用者进行 npm install 时,使用用户设备进行 Addon 的编译。这时候我们可以使用 install 钩子来实现,我们仅需在 package.json 文件中添加如下内容:

{
  // ...
  "scripts": {
    // ...
    "install": "prebuild-install || node-gyp rebuild --release"
    // ...
  }
  // ...
}

保险起见,确保 node-gyp 在你的 devDependencies 之中,这样就能在用户通过 npm 安装你的 Addon 时,自动编译当前系统架构所对应的产物。

预编译

如果希望更近一步,节约用户安装 Addon 的时间,或者是为了让用户无需具备编译环境即可安装 Addon,可以使用预编译方案。即在集成环境中提前编译常见的操作系统、架构对应的 .node 文件,并随着 npm 包进行分发,再通过 bindings 或者其他一些库来自动匹配寻找系统所需要的对应 .node 文件。

由于预编译方案涉及到更多的细节,本文不再做介绍,大家可以参考该项目:

https://github.com/mapbox/node-pre-gyp

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

大模型推理加速框架vllm部署的实战方案

大家好,我是herosunly。985院校硕士毕业,现担任算法研究员一职,热衷于机器学习算法研究与应用。曾获得阿里云天池比赛第一名,CCF比赛第二名,科大讯飞比赛第三名。拥有多项发明专利。对机器学习和深度学习拥有自己独到的见解。曾经辅导过若干个非计算机专业的学生进入到算法…

【教学类-06-07】20231124 (55格版)X-X之间的加法、减法、加减混合题

背景需求 在大四班里&#xff0c;预测试55格“5以内、10以内、20以内的加法题、减法题、加减混合题”的“实用性”。 由于只打印一份20以内加法减法混合题。 “这套20以内的加减法最难”&#xff0c;我询问谁会做&#xff08;摸底幼儿的水平&#xff09; 有两位男孩举手想挑…

python——第十二天

内置模块或者其他模块学习方式&#xff1a; dir help os模块负责程序与操作系统的交互&#xff0c;提供了访问操作系统底层的接口&#xff1b;即os模块提供了非常丰富的方法用来处理文件和目录。 os&#xff1a; os.path 遍历C盘代码 import os from os import path def …

1|1111

1、指定在每天凌晨4&#xff1a;00将该时间点之前的系统日志信息&#xff08;/var/log/messages &#xff09;备份到目录下/backup&#xff0c;备份后日志文件名显示格式logfileYY-MM-DD-HH-MM 2、配置ssh免密登陆&#xff1a;客户端主机通过redhat用户基于秘钥验证方式进行远…

手写一个简单版的Spring

1. 创建一个工程及初始化 创建Java工程 创建对应的包 config&#xff1a;为配置类包 service&#xff1a;定义的将要被自己写的Spring容器管理的组件bean spring&#xff1a;里面定义自己写的Spring的类文件&#xff0c;包含子包anno注解包 test&#xff1a;定义测试类 2.…

springboot+vue基本微信小程序的外卖点餐平台系统

项目介绍 餐饮行业是一个传统的行业。根据当前发展现状&#xff0c;网络信息时代的全面普及&#xff0c;餐饮行业也在发生着变化&#xff0c;单就点餐这一方面&#xff0c;利用手机点单正在逐步进入人们的生活。传统的点餐方式&#xff0c;不仅会耗费大量的人力、时间&#xf…

C++入门第九篇---Stack和Queue模拟实现,优先级队列

前言&#xff1a; 我们已经掌握了string vector list三种最基本的数据容器模板&#xff0c;而对于数据结构的内容来说&#xff0c;其余的数据结构容器基本都是这三种容器的延申和扩展&#xff0c;在他们的基础上扩展出更多功能和用法&#xff0c;今天我们便来模拟实现一下C库中…

【OpenSTL】方便好用的时空预测开源库

OpenSTL&#xff1a;方便好用的时空预测开源库 时空预测学习是一种学习范式&#xff0c;它使得模型能够通过在无监督的情况下从给定的过去帧预测未来帧&#xff0c;从而学习空间和时间的模式。尽管近年来取得了显著的进展&#xff0c;但由于不同的设置、复杂的实现和难以复现性…

Go语言的学习笔记2——Go语言源文件的结构布局

用一个只有main函数的go文件来简单说一下Go语言的源文件结构布局&#xff0c;主要分为包名、引入的包和具体函数。下边是main.go示例代码&#xff1a; package mainimport "fmt"func main() { fmt.Println("hello, world") }package main就是表明这个文件…

AlDente Pro v1.22.2(mac电池最大充电限制工具)

AlDente Pro是一款适用于Mac操作系统的小工具&#xff0c;可以帮助您限制电池充电量以延长电池寿命。通常情况下&#xff0c;电池在充满的状态下会继续接受电源充电&#xff0c;这可能会导致电池寿命缩短。使用AlDente Pro&#xff0c;您可以设置电池只充到特定的充电水平&…

高清动态壁纸软件Live Wallpaper Themes 4K mac中文版功能

Live Wallpaper & Themes 4K mac是一款提供各种高清动态壁纸和主题的应用程序。该应用程序提供了大量的动态壁纸和主题&#xff0c;包括自然、动物、城市、抽象等各种类别&#xff0c;可以满足用户不同的需求。除了壁纸和主题之外&#xff0c;该应用程序还提供了许多其他功…

拦截器详解

一、概述 什么是拦截器&#xff1f; 是一种动态拦截方法调用的机制&#xff0c;类似于过滤器。Spring框架中提供的&#xff0c;用来动态拦截控制方法的执行。 到底是干啥用的&#xff1f; 拦截请求用的&#xff0c;在指定的方法调用前后&#xff0c;执行在拦截器中编写的程序 …

苹果App加急审核

苹果App加急审核 &#xff08;注意加急的次数&#xff0c;有的说一年能加急两次&#xff0c;有的说不止两次。遇到紧急问题了就用&#xff0c;非紧急 等一等也行&#xff09; 1.登录苹果账号 Sign In - Apple &#xff08; https://developer.apple.com/contact/app-store/?…

力扣每日一道系列 --- LeetCode 206. 反转链表

&#x1f4f7; 江池俊&#xff1a; 个人主页 &#x1f525;个人专栏&#xff1a; ✅数据结构探索 ✅LeetCode每日一道 &#x1f305; 有航道的人&#xff0c;再渺小也不会迷途。 LeetCode 206. 反转链表 思路一&#xff1a;头插 初始化两个指针&#xff0c;cur 和 newhead。…

单片机、ARM、嵌入式开发、Android 底层开发有什么关系?

单片机、ARM、嵌入式开发、Android 底层开发有什么关系&#xff1f; 从我目前的见识来看&#xff1a; 单片机是个系统&#xff08;比如&#xff1a;51、AVR、PLC...&#xff09;&#xff0c;其中包含了去除了输入输出之外的运算器、控制器、存储器&#xff0c;我们用程序可以非…

CANdelaStudio 使用教程3 新建Service

文章目录 简述Service 的相关配置项1、Protocol Services2、Diagnostic Class Templates3、Supported Diagnostic Classes 新建 Service1、新建 Service2、新建类并添加服务3、 选择支持的服务4、Diagnostic Class Templates&#xff1a;Identification 编辑 Service1、新增服务…

区块链技术将如何影响未来的数字营销?

你是否听腻了区块链和数字营销等流行语&#xff0c;却不明白它们对未来意味着什么&#xff1f;那么&#xff0c;准备好系好安全带吧&#xff0c;因为区块链技术将彻底改变我们对数字营销的看法。从建立消费者信任到提高透明度和效率&#xff0c;其可能性是无限的。 让我们来探…

有序表的详解

目录 有序表的介绍 树的左旋和右旋操作 AVL树的详解 SB树的详解 红黑树的介绍 SkipList的详解 有序表的介绍 有序表是除具备哈希表所具备的功能外&#xff0c;有序表中的内容都是按照key有序排列的&#xff0c;并且增删改查等操作的时间复杂度都是&#xff0c;红黑树&…

【超强笔记软件】Obsidian如何实现免费无限流量无套路云同步?

【超强笔记软件】Obsidian如何实现免费无限流量无套路云同步&#xff1f; 文章目录 【超强笔记软件】Obsidian如何实现免费无限流量无套路云同步&#xff1f;一、简介软件特色演示&#xff1a; 二、使用免费群晖虚拟机搭建群晖Synology Drive服务&#xff0c;实现局域网同步1 安…

Educational Codeforces Round 158 (Rated for Div. 2)(A~E)(贪心,树形DP)

A - Line Trip 题意&#xff1a;有一条路&#xff0c;可以用一条数线来表示。你位于数线上的点 0 &#xff0c;你想从点 0 到点 x &#xff0c;再回到点 0。你乘汽车旅行&#xff0c;每行驶 1个单位的距离要花费 1 升汽油。当您从点 0出发时&#xff0c;汽车已加满油(油箱中的…