为 Compose MultiPlatform 添加 C/C++ 支持(2):在 jvm 平台使用 jni 实现桌面端与 C/C++ 互操作
前言
在上篇文章中我们已经介绍了实现 Compose MultiPlatform 对 C/C++ 互操作的基本思路。
并且先介绍了在 kotlin native 平台使用 cinterop 实现与 C/C++ 的互操作。
今天这篇文章将补充在 jvm 平台使用 jni。
在 Compose MultiPlatform 中,使用 jvm 平台的是 Android 端和 Desktop 端,而安卓端可以直接使用安卓官方的 NDK 实现交叉编译,但是 Desktop 不仅不支持交叉编译,甚至连使用 Gradle 自动编译都没有。
所以本文重点主要在于实现 Desktop 的 jni 编译以及调用编译出来的二进制库。
Android 使用 jni
在介绍 Desktop 使用 jni 之前,我们先回顾一下在 Android 中使用 jni,并复用 Android 端的 C++ 代码给 Desktop 使用。
感谢谷歌的工作,在安卓中使用 jni 非常简单,我们只需要在 Android Studio 随便打开一个已有的项目,然后依次选择菜单 File - New - New Module - Android Native Library,保持默认参数,点击 Finish 即可完成创建安卓端的 jni 模块。
这里我们以 jetBrains 的官方 Compose MultiPlatform 模板 项目作为示例:
创建完成后需要注意,Android studio 会自动修改项目 settings.gradle.kts 在其中添加一个插件 org.jetbrains.kotlin.android
,这会导致编译错误 java.lang.IllegalArgumentException: Cannot provide multiple default versions for the same plugin.
,所以需要我们删掉新添加的这个插件:
然后在 shared
模块中的 build.gradle.kts 文件的 Android 依赖部分引入 nativelib
模块:
kotlin {// ……sourceSets {// ……val androidMain by getting {dependencies {// ……api(project(":nativelib"))}}// ……}
}
接着,需要注意 nativelib
模块的两个文件 native.cpp 和 NativeLib.kt:
我们看一下 nativelib
模块中的 nativelib.cpp 文件的默认内容:
#include <jni.h>
#include <string>extern "C" JNIEXPORT jstring JNICALL
Java_com_equationl_nativelib_NativeLib_stringFromJNI(JNIEnv* env,jobject /* this */) {std::string hello = "C++";return env->NewStringUTF(hello.c_str());
}
代码很简单,就是返回一个字符串 “Hello from C++”,我们改成返回 “C++”。
这里需要注意这个函数的名称: Java_com_equationl_nativelib_NativeLib_stringFromJNI
开头的 “Java” 是固定字符,后面的 “com_equationl_nativelib_NativeLib” 表示从 java 调用时的类的包名+类名,最后的 “stringFromJNI” 才是这个函数的名称。
通过 jni 从 java(kt)中调用这个函数时必须确保其包名和类名与其一致才能成功调用。
然后查看 NativeLib.kt 文件:
class NativeLib {external fun stringFromJNI(): Stringcompanion object {init {System.loadLibrary("nativelib")}}
}
其中 external fun stringFromJNI(): String
表示需要调用的 c++ 函数名。
System.loadLibrary("nativelib")
表示加载 C++ 编译生成的二进制库,这里我们无需关心具体的编译过程和编译产物,只需要直接加载 nativelib
即可,剩下的工作 NDK 已经替我们完成了。
最后,我们来调用一下这个 C++ 函数。
不过在此之前先简单介绍一下我们用作示例的这个 Compose MultiPlatform 的内容,它的 UI 就是一个按钮,按钮默认显示 “Hello, World!”,当点击按钮后会通过一个 expect
函数获取当前平台的名称然后显示到按钮上:
@OptIn(ExperimentalResourceApi::class)
@Composable
fun App() {MaterialTheme {var greetingText by remember { mutableStateOf("Hello, World!") }var showImage by remember { mutableStateOf(false) }Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {Button(onClick = {greetingText = "Hello, ${getPlatformName()}"showImage = !showImage}) {Text(greetingText)}AnimatedVisibility(showImage) {Image(painterResource("compose-multiplatform.xml"),contentDescription = "Compose Multiplatform icon")}}}
}expect fun getPlatformName(): String
所以接下来我们修改安卓平台的 getPlatformName
函数的 actual
实现,由:
actual fun getPlatformName(): String = "Android"
修改为:
actual fun getPlatformName(): String = NativeLib().stringFromJNI()
这样,它获取的名称就是来自 C++ 代码的 “C++” 了。
运行代码,可以看到完美符合预期:
Desktop 使用 jni
上一节我们已经完成了在 Android 中使用 jni,本节我们将在 Desktop 中也实现使用 jni,并且复用上节中的 nativelib.cpp 文件。
因为直接使用 Gradle 编译 C++ 代码不是很方便,而且还不支持交叉编译,所以这里我们首先手动编译,验证可行后再自己编写 gradle 脚本实现自动编译。
有关编写 gradle 脚本的基础知识可以阅读我之前的文章 Compose Desktop 使用中的几个问题(分平台加载资源、编写Gradle 任务下载平台资源、桌面特有组件、鼠标&键盘事件) 了解。
首先,我们可以使用命令 g++ nativelib.cpp -o nativelib.bin -shared -fPIC -I C:\Users\equationl\.jdks\corretto-19.0.2\include -I C:\Users\equationl\.jdks\corretto-19.0.2\include\win32
编译我们的 C++ 文件为当前平台可用的二进制文件。
上述命令中 nativelib.cpp
即需要编译的文件,nativelib.bin
为输出的二进制文件,C:\Users\equationl\.jdks\corretto-19.0.2\
为你电脑上安装的任意的 jdk 目录。
输入 “ j d k P a t h / i n c l u d e " 和 " jdkPath/include" 和 " jdkPath/include"和"jdkPath/include/win32” 是因为这两个目录下有我们的 C++ 文件导入所需的头文件,如 “jni.h” 。
切换到我们的 C++ 文件所在目录后执行上述命令编译:
此时我们可以看到在 “./nativelib/src/main/cpp” 目录下已经生成了 nativelib.bin 文件。
注意:在 macOS 上系统自带了 g++ 命令,但是一般来说 Windows 系统没有自带 g++ 命令,所以需要先自己安装 g++
然后,我们在 sahred 模块下的 desktopMain 包中新建一个文件 NativeLib.kt ,注意该文件的包名需要和 C++ 定义的一致:
然后编写该文件内容为:
package com.equationl.nativelibclass NativeLib {external fun stringFromJNI(): Stringcompanion object {init {System.load("D:\\project\\ideaProject\\compose-multiplatform-c-test\\nativelib\\src\\main\\cpp\\nativelib.bin")}}
}
可以看到在 Desktop 中加载二进制库和 Android 中略有不同,它使用的是 System.load()
而不是 System.loadLibrary()
,并且加载二进制文件时使用的是绝对路径。
这是因为我们无法在 Desktop 中像 Android 一样直接把二进制文件打包到指定的路径下并且直接使用库名通过 System.loadLibrary()
加载,所以只能使用绝对路径加载外部二进制文件。
这里我们把加载的文件路径写为了先前生成的 nativelib.bin
的路径。
接着,依旧是修改 dektop 的 getPlatformName
函数的实现为:
actual fun getPlatformName(): String = NativeLib().stringFromJNI()
然后运行 Desktop 程序:
运行结果完美符合预期。
为 Desktop 实现自动编译 C++
在上一节中我们已经实现了 Desktop 使用 jni 并验证了可行性,但是目前还是手动编译代码,这显然是不现实的,所以我们本节将讲解如何自己编写脚本实现自动编译。
另外,上一节中我们说过, Dektop 加载二进制文件使用的是绝对路径,所以我们需要将编译生成的二进制文件放到指定位置并打包进 Desktop 程序安装包中,Desktop 在安装时会自动将这个文件解压到指定路径,关于这个的基础知识还是可以看我的文章 Compose Desktop 使用中的几个问题(分平台加载资源、编写Gradle 任务下载平台资源、桌面特有组件、鼠标&键盘事件) 了解。
首先,需要指定一下资源文件目录,在 desktopApp
模块的 buiuld.gradle.kts 文件中添加以下内容:
compose.desktop {application {// ……nativeDistributions {// ……appResourcesRootDir.set(project.layout.projectDirectory.dir("resources"))}}
}
指定资源目录为 resources
。
然后依旧是在这个文件中,添加一个函数 runCommand
,用于执行 shell 命令:
fun runCommand(command: String, timeout: Long = 120): Pair<Boolean, String> {val process = ProcessBuilder().command(command.split(" ")).directory(rootProject.projectDir).redirectOutput(ProcessBuilder.Redirect.INHERIT).redirectError(ProcessBuilder.Redirect.INHERIT).start()process.waitFor(timeout, TimeUnit.SECONDS)val result = process.inputStream.bufferedReader().readText()val error = process.errorStream.bufferedReader().readText()return if (error.isBlank()) {Pair(true, result)}else {Pair(false, error)}
}
代码很简单,接收一个字符串表示的 shell 命令,返回一个 Pair
,第一个 booean 数据表示是否执行成功;第二个 String 是输出内容。
接着注册一个 task:
tasks.register("compileJni") { }
修改原有的 prepareAppResources
task,添加上我们刚注册的 compileJni
为它的依赖:
gradle.projectsEvaluated {tasks.named("prepareAppResources") {dependsOn("compileJni")}
}
这里的修改依赖需要加在 gradle.projectsEvaluated
语句中,因为 prepareAppResources
这个 task 推迟了注册,如果不在项目配置完成后再修改依赖的话会报 prepareAppResources
不存在。
注:这里的 prepareAppResources
是 task 模块中用于执行复制和打包资源文件的 task,所以我们把自定义的 compileJni
添加成它的依赖,以保证在它之前执行。
另外,这里必须明确保证 compileJni
在 prepareAppResources
之前执行,否则由于我们的 compileJni
任务的输出路径和 prepareAppResources
任务的输出路径冲突,会导致编译失败,具体后面详细解释。
接着,在 compileJni
task 中编写我们的编译逻辑,我们先看一下完整的代码,然后再逐一解释:
tasks.register("compileJni") {description = "compile jni binary file for desktop"val resourcePath = File(rootProject.projectDir, "desktopApp/resources/common/lib/")val binFilePath = File(resourcePath, "nativelib.bin")val cppFileDirectory = File(rootProject.projectDir, "nativelib/src/main/cpp")val cppFilePath = File(cppFileDirectory, "nativelib.cpp")// 指定输入、输出文件,用于增量编译inputs.dir(cppFileDirectory)outputs.file(binFilePath)doLast {project.logger.info("compile jni for desktop running……")val jdkFile = org.gradle.internal.jvm.Jvm.current().javaHomeval systemPrefix: Stringval os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()if (os.isWindows) {systemPrefix = "win32"}else if (os.isMacOsX) {systemPrefix = "darwin"}else if (os.isLinux) {systemPrefix = "linux"}else {project.logger.error("UnSupport System for compiler cpp, please compiler manual")return@doLast}val includePath1 = jdkFile.resolve("include")val includePath2 = includePath1.resolve(systemPrefix)if (!includePath1.exists() || !includePath2.exists()) {val msg = "ERROR: $includePath2 not found!\nMaybe it's because you are using JetBrain Runtime (Jbr)\nTry change Gradle JDK to another jdk which provide jni support"throw GradleException(msg)}project.logger.info("Check Desktop Resources Path……")if (!resourcePath.exists()) {project.logger.info("${resourcePath.absolutePath} not exists, create……")mkdir(resourcePath)}val runTestResult = runCommand("g++ --version")if (!runTestResult.first) {throw GradleException("Error: Not find command g++, Please install it and add to your system environment path\n${runTestResult.second}")}val command = "g++ ${cppFilePath.absolutePath} -o ${binFilePath.absolutePath} -shared -fPIC -I ${includePath1.absolutePath} -I ${includePath2.absolutePath}"project.logger.info("running command $command……")val compilerResult = runCommand(command)if (!compilerResult.first) {throw GradleException("Command run fail: ${compilerResult.second}")}project.logger.info(compilerResult.second)project.logger.lifecycle("compile jni for desktop all done")}
}
首先,在 task 顶级定义了四个路径: resourcePath
、 binFilePath
、cppFileDirectory
和 cppFilePath
,分别表示需要存放二进制文件的资源目录、二进制文件输出路径、C++文件存放目录和需要编译的具体 C++ 文件路径。
rootProject.projectDir
返回的是当前项目的根目录。
接着,我们通过 inputs.dir()
方法添加了该 task 的输入路径。
outputs.file
方法添加了该 task 的输出文件。
定义输入路径和输出文件与我们这里需要执行的编译没有直接关联,这里定义这个两个路径是为了让 Gradle 实现增量编译,即只有在上次编译完成后输入路径的中的文件内容发生了变化或输出文件发生了变化才会继续执行这个 task,否则会认为这个 task 没有变化,不会执行,表现在编译输出日志则为:
> Task :desktopApp:compileJni UP-TO-DATE
接下来,我们的代码写在了 doLast { }
语句中,则表示里面的代码只有在编译阶段才会执行,在配置阶段不会执行。
在其中的 org.gradle.internal.jvm.Jvm.current().javaHome
返回的是当前项目 Gradle 使用的 jdk 根目录。
然后,我们需要拼接出编译时需要导入的两个 jdk 路径 includePath1
和 includePath2
,其中的 includePath2
不同的系统名称不一样,所以需要判断一下当前编译使用的系统并更改该值。 可以通过 DefaultNativePlatform.getCurrentOperatingSystem().isXXX
判断当前是否是某个系统。
接着,检查存放二进制文件的目录是否存在,不存在则创建。
下一步是使用 g++ --version
测试是否安装了 g++ 。
最后,拼接出编译命令后执行编译:
g++ ${cppFilePath.absolutePath} -o ${binFilePath.absolutePath} -shared -fPIC -I ${includePath1.absolutePath} -I ${includePath2.absolutePath}
此时如果编译成功,那么二进制文件会输出到我们指定的 dektop 资源目录下。
我们现在只需要修改 dektop 加载二进制文件的代码为:
val libFile = File(System.getProperty("compose.application.resources.dir")).resolve("lib").resolve("nativelib.bin")
System.load(libFile.absolutePath)
上述代码中 System.getProperty("compose.application.resources.dir")
返回的是我们最开始在 Gradle 中定义的资源打包安装解压后在系统上的绝对路径。
至此,我们的自动编译已经完成!
最后来说一下我们前面提到的为什么我们的 compileJni
task 必须在 prepareAppResources
之前执行,我们现在直接把原本的修改 prepareAppResources
依赖于 compileJni
改成 Desktop 模块执行的第一个 task compileKotlinJvm
依赖 compileJni
:
tasks.named("compileKotlinJvm") {dependsOn("compileJni")
}
运行后会看到报错:
A problem was found with the configuration of task ':desktopApp:prepareAppResources' (type 'Sync').- Gradle detected a problem with the following location: '/Users/equationl/AndroidStudioProjects/life-game-compose/desktopApp/resources/common'.Reason: Task ':desktopApp:prepareAppResources' uses this output of task ':desktopApp:compileJni' without declaring an explicit or implicit dependency. This can lead to incorrect results being produced, depending on what order the tasks are executed.Possible solutions:1. Declare task ':desktopApp:compileJni' as an input of ':desktopApp:prepareAppResources'.2. Declare an explicit dependency on ':desktopApp:compileJni' from ':desktopApp:prepareAppResources' using Task#dependsOn.3. Declare an explicit dependency on ':desktopApp:compileJni' from ':desktopApp:prepareAppResources' using Task#mustRunAfter.
简单说就是 prepareAppResources
和 compileJni
都声明了同一个输出路径,除非明确指定它们两个之间的依赖关系,否则编译会出现问题。
其实也很好理解,他们的输出路径都是一个,如果不明确依赖关系的话增量编译就永远不会触发了,永远都将是全量编译。
而在这里我们的需求是首先使用 compileJni
生成二进制文件后,由 prepareAppResources
将其打包,所以自然应该是写成 prepareAppResources
依赖于 compileJni
。
最后,还是需要强调一点,Desktop 编译 C++ 是不支持交叉编译的,也就是说在 Windows 只能编译 Windows 的程序,在 macOS 只能 编译 macOS 的程序。
其实即使 C++ 可以交叉编译也没用,因为 Compose Desktop 并不支持交叉编译,哈哈哈。
参考资料
- Native dependency in Kotlin/Multiplatform — part 2: JNI for JVM & Android
- Kotlin JNI for Native Code
相关文章:

为 Compose MultiPlatform 添加 C/C++ 支持(2):在 jvm 平台使用 jni 实现桌面端与 C/C++ 互操作
前言 在上篇文章中我们已经介绍了实现 Compose MultiPlatform 对 C/C 互操作的基本思路。 并且先介绍了在 kotlin native 平台使用 cinterop 实现与 C/C 的互操作。 今天这篇文章将补充在 jvm 平台使用 jni。 在 Compose MultiPlatform 中,使用 jvm 平台的是 An…...

【PyTorch】卷积神经网络
文章目录 1. 理论介绍1.1. 从全连接层到卷积层1.1.1. 背景1.1.2. 从全连接层推导出卷积层 1.2. 卷积层1.2.1. 图像卷积1.2.2. 填充和步幅1.2.3. 多通道 1.3. 池化层(又称汇聚层)1.3.1. 背景1.3.2. 池化运算1.3.3. 填充和步幅1.3.4. 多通道 1.4. 卷积神经…...

qt可以详细写的项目或技术
1.QT 图形视图框架 2.QT 模型视图结构 3.QT列表显示大量信息 4.QT播放器 5.QT 编解码 6.QT opencv...

操作系统笔记——储存系统、文件系统(王道408)
文章目录 前言储存系统地址转换内存扩展覆盖交换 储存器分配——连续分配固定大小分区动态分区分配动态分区分配算法 储存器分配——非连续分配页式管理基本思想地址变换硬件快表(TLB)多级页表 段式管理段页式管理 虚拟储存器——基于交换的内存扩充技术…...

基于Html+腾讯云播SDK开发的m3u8播放器
周末业余时间在家无事,学习了一下腾讯的云播放sdk,并制作了一个小demo(m3u8播放器),该在线工具是基于腾讯的云播sdk开发的,云播sdk非常牛,可以支持多种播放格式。 预览地址 m3u8player.org 源码…...

uniapp小程序分享为灰色
引用:https://www.cnblogs.com/panwudi/p/17074172.html uniapp开发的微信小程序,没有转发,分享: 创建一个mixin:common/share.js export default {onShareAppMessage(res) { //发送给朋友return {}},onShareTimeline(res) {//…...

python:五种算法(OOA、WOA、GWO、PSO、GA)求解23个测试函数(python代码)
一、五种算法简介 1、鱼鹰优化算法OOA 2、鲸鱼优化算法WOA 3、灰狼优化算法GWO 4、粒子群优化算法PSO 5、遗传算法GA 二、5种算法求解23个函数 (1)23个函数简介 参考文献: [1] Yao X, Liu Y, Lin G M. Evolutionary programming made…...

DIP——添加运动模糊与滤波
1.运动模糊 为了模拟图像退化的过程,在这里创建了一个用于模拟运动模糊的点扩散函数,具体模糊的方向取决于输入的motion_angle。如果运动方向接近水平,则模糊效果近似水平,如果运动方向接近垂直,则模糊效果近似垂直。具…...

SQL Server查询计划(Query Plan)——SQL处理过程
6. 查询计划(Query Plan) 6.1. SQL处理过程 就SQL语句的处理过程而言,各关系库间大同小异,尤其是商业库之间实现机制和细节差别更小些,其功能及性能支持方面也更加强大和完善。SQL Server作为商业库中的后起之秀,作为SQL语句处理过程的主要支撑和保障,其优化器及相关机…...

【动手学深度学习】(十二)现代卷积神经网络
文章目录 一、深度卷积神经网络AlexNet1.理论知识 一、深度卷积神经网络AlexNet 1.理论知识 ImageNet(2010) 图片自然物体的彩色图片手写数字的黑色图片大小468 * 38728*28样本数1.2M60K类数100010 AlexNet AlexNet赢了2012ImageNet竞赛更深更大的LeNet主要改进ÿ…...

【小沐学Python】Python实现TTS文本转语音(speech、pyttsx3、百度AI)
文章目录 1、简介2、Windows语音2.1 简介2.2 安装2.3 代码 3、pyttsx33.1 简介3.2 安装3.3 代码 4、ggts4.1 简介4.2 安装4.3 代码 5、SAPI6、SpeechLib7、百度AI8、百度飞桨结语 1、简介 TTS(Text To Speech) 译为从文本到语音,TTS是人工智能AI的一个模组…...

TCP通信
第二十一章 网络通信 本章节主要讲解的是TCP和UDP两种通信方式它们都有着自己的优点和缺点 这两种通讯方式不通的地方就是TCP是一对一通信 UDP是一对多的通信方式 接下来会一一讲解 TCP通信 TCP通信方式呢 主要的通讯方式是一对一的通讯方式,也有着优点和缺点…...
2023济南大学acm新生赛题解
通过答题情况的难度系数: 签到:ACI 铜牌题:BG 银牌题:EF 金牌题:DHJKO 赛中暂未有人通过:LMNP A - AB Problem 直接根据公式计算就行。 #include<stdio.h> int main(){int a,b;scanf("%…...

docker-compose安装教程
1.确认docker-compose是否安装 docker-compose -v如上图所示表示未安装,需要安装。 如上图所示表示已经安装,不需要再安装,如果觉得版本低想升级,也可以继续安装。 2.离线安装 下载docker-compose安装包,上传到服务…...

【rabbitMQ】rabbitMQ用户,虚拟机地址(添加,修改,删除操作)
rabbitMQ的下载,安装和配置 https://blog.csdn.net/m0_67930426/article/details/134892759?spm1001.2014.3001.5502 rabbitMQ控制台模拟收发消息 https://blog.csdn.net/m0_67930426/article/details/134904365?spm1001.2014.3001.5502 目录 用户 添加用户…...

Python高级算法——动态规划
Python中的动态规划:高级算法解析 动态规划是一种解决多阶段决策问题的数学方法,常用于优化问题。它通过将问题分解为子问题,并在解决这些子问题的基础上构建全局最优解。在本文中,我们将深入讲解Python中的动态规划,…...

MySQL在Centos7环境安装
说明: • 安装与卸载中,⽤⼾全部切换成为root,⼀旦 安装,普通⽤⼾能使⽤的 1. 卸载不要的环境 [roothcss-ecs-1036 ~]# ps ajx |grep mariadb # 先检查是否有mariadb存在 13134 14844 14843 13134 pts/0 14843 S 1005 0:00 gr…...

halcon视觉缺陷检测常用的6种方法
一、缺陷检测综述 缺陷检测是视觉需求中难度最大一类需求,主要是其稳定性和精度的保证。首先常见缺陷:凹凸、污点瑕疵、划痕、裂缝、探伤等。常用的手法有六大金刚(在halcon中的ocv和印刷检测是针对印刷行业的检测,有对应算子封装): 1.blob+特征 2.blob+差分+特征 3.光度…...

openGauss学习笔记-151 openGauss 数据库运维-备份与恢复-物理备份与恢复之gs_basebackup
文章目录 openGauss学习笔记-151 openGauss 数据库运维-备份与恢复-物理备份与恢复之gs_basebackup151.1 背景信息151.2 前提条件151.3 语法151.4 示例151.5 从备份文件恢复数据 openGauss学习笔记-151 openGauss 数据库运维-备份与恢复-物理备份与恢复之gs_basebackup 151.1 …...

报错:Uncaught ReferenceError: Cannot access ‘l‘ before initialization
在文件 .babelrc 或 babel.config.js ,webpack.config.js 下配置 .babel 或 babel.config.js "plugins": ["babel/plugin-transform-runtime" ] webpack.config.js,详见 Webpack target module.exports {target: [web, es5], }...

计算机视觉-机器学习-人工智能顶会 会议地址
计算机视觉-机器学习-人工智能顶会 会议地址 最近应该要整理中文资料的参考文献,很多会议文献都需要补全会议地点(新国标要求)。四处百度感觉也挺麻烦的,而且没有比较齐全的网站可以搜索。因此自己整理了一下计算机视觉-机器学习…...

784. 字母大小写全排列
字母大小写全排列 描述 : 给定一个字符串 s ,通过将字符串 s 中的每个字母转变大小写,我们可以获得一个新的字符串。 返回 所有可能得到的字符串集合 。以 任意顺序 返回输出。 题目 : LeetCode 784.字母大小写全排列 : 784. 字母大小写全排列 分析…...

HarmonyOS鸿蒙应用开发——HTTP网络访问与封装
文章目录 基本使用封装参考 基本使用 鸿蒙应用发起HTTP请求的基本使用,如下: 导入http模块创建httpRequest对象发起http请求,并处理响应结果 第一、导入http模块: import http from ohos.net.http第二、创建httpRequest对象&a…...

vscode 编写爬虫爬取王者荣耀壁纸
网上关于爬虫大部分教程和编辑器用的都不是vscode ,此教程用到了vscode、Python、bs4、requests。 vscode配置Python安装环境可以看看这个大佬的教程 03-vscode安装和配置_哔哩哔哩_bilibili vscode配置爬虫环境可以参考这个大佬的教程【用Vscode实现简单的python…...

spring boot + uniapp 微信公众号 jsapi 支付
后端支付类 package com.ruoyi.coupon.payment;import com.google.gson.Gson; import com.ruoyi.coupon.payment.dto.PayParamJsapiDto; import com.ruoyi.coupon.payment.dto.RefundParam; import com.ruoyi.coupon.service.ICouponConfigService; import com.wechat.pay.jav…...

【数学建模】《实战数学建模:例题与讲解》第九讲-时间序列分析(含Matlab代码)
【数学建模】《实战数学建模:例题与讲解》第九讲-时间序列分析(含Matlab代码) 基本概念确定性时间序列分析方法平稳时间序列模型ARIMA模型季节性序列 习题8.11. 题目要求2.解题过程3.程序4.结果 习题8.21. 题目要求2.解题过程3.程序4.结果 习…...

大话数据结构-查找-有序表查找
注:本文同步发布于稀土掘金。 3 有序表查找 3.1 折半查找 折半查找(Binary Search)技术,又称为二分查找,它的前提是线性表中的记录必须是关键码有序(通常从小到大有序),线性表必须…...

Qt实现二维码生成和识别
一、简介 QZxing开源库: 生成和识别条码和二维码 下载地址:https://gitcode.com/mirrors/ftylitak/qzxing/tree/master 二、编译与使用 1.下载并解压,解压之后如图所示 2.编译 打开src目录下的QZXing.pro,选择合适的编译器进行编译 最后生…...

MyBatisX插件
MyBatisX插件 MyBatis-Plus为我们提供了强大的mapper和service模板,能够大大的提高开发效率。 但是在真正开发过程中,MyBatis-Plus并不能为我们解决所有问题,例如一些复杂的SQL,多表联查,我们就需要自己去编写代码和SQ…...

《C++20设计模式》学习笔记---原型模式
C20设计模式 第 4 章 原型模式4.1 对象构建4.2 普通拷贝4.3 通过拷贝构造函数进行拷贝4.4 “虚”构造函数4.5 序列化4.6 原型工厂4.7 总结4.8 代码 第 4 章 原型模式 考虑一下我们日常使用的东西,比如汽车或手机。它们并不是从零开始设计的,相反&#x…...