为 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 模板 项目作为示例:

1.jpg

创建完成后需要注意,Android studio 会自动修改项目 settings.gradle.kts 在其中添加一个插件 org.jetbrains.kotlin.android ,这会导致编译错误 java.lang.IllegalArgumentException: Cannot provide multiple default versions for the same plugin.,所以需要我们删掉新添加的这个插件:

2.jpg

然后在 shared 模块中的 build.gradle.kts 文件的 Android 依赖部分引入 nativelib 模块:

kotlin {

	// ……

    sourceSets {

    	// ……

        val androidMain by getting {
            dependencies {
                // ……

                api(project(":nativelib"))
            }
        }

        // ……

    }
}

接着,需要注意 nativelib 模块的两个文件 native.cppNativeLib.kt

3.jpg

我们看一下 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(): String

    companion 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++” 了。

运行代码,可以看到完美符合预期:

4.gif

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++ 文件所在目录后执行上述命令编译:

5.jpg

此时我们可以看到在 “./nativelib/src/main/cpp” 目录下已经生成了 nativelib.bin 文件。

注意:在 macOS 上系统自带了 g++ 命令,但是一般来说 Windows 系统没有自带 g++ 命令,所以需要先自己安装 g++

然后,我们在 sahred 模块下的 desktopMain 包中新建一个文件 NativeLib.kt ,注意该文件的包名需要和 C++ 定义的一致:

6.jpg

然后编写该文件内容为:

package com.equationl.nativelib

class NativeLib {
    external fun stringFromJNI(): String

    companion 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 程序:

7.gif

运行结果完美符合预期。

为 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 添加成它的依赖,以保证在它之前执行。

另外,这里必须明确保证 compileJniprepareAppResources 之前执行,否则由于我们的 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().javaHome
        val systemPrefix: String

        val 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 顶级定义了四个路径: resourcePathbinFilePathcppFileDirectorycppFilePath,分别表示需要存放二进制文件的资源目录、二进制文件输出路径、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 路径 includePath1includePath2 ,其中的 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.

简单说就是 prepareAppResourcescompileJni 都声明了同一个输出路径,除非明确指定它们两个之间的依赖关系,否则编译会出现问题。

其实也很好理解,他们的输出路径都是一个,如果不明确依赖关系的话增量编译就永远不会触发了,永远都将是全量编译。

而在这里我们的需求是首先使用 compileJni 生成二进制文件后,由 prepareAppResources 将其打包,所以自然应该是写成 prepareAppResources 依赖于 compileJni

最后,还是需要强调一点,Desktop 编译 C++ 是不支持交叉编译的,也就是说在 Windows 只能编译 Windows 的程序,在 macOS 只能 编译 macOS 的程序。

其实即使 C++ 可以交叉编译也没用,因为 Compose Desktop 并不支持交叉编译,哈哈哈。

参考资料

  1. Native dependency in Kotlin/Multiplatform — part 2: JNI for JVM & Android
  2. Kotlin JNI for Native Code

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

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

相关文章

京东数据运营(京东API接口):10月投影仪店铺数据分析

鲸参谋监测的京东平台10月份投影仪市场销售数据已出炉&#xff01; 10月份&#xff0c;环同比来看&#xff0c;投影仪市场销售均上涨。鲸参谋数据显示&#xff0c;今年10月&#xff0c;京东平台投影仪的销量为16万&#xff0c;环比增长约22%&#xff0c;同比增长约8%&#xff1…

免费分享一套Springboot+Vue前后端分离的在线商城系统,挺实用的

大家好&#xff0c;我是java1234_小锋老师&#xff0c;看到一个不错的SpringbootVue前后端分离的在线商城系统&#xff0c;分享下哈。 项目视频演示 【免费】SpringbootVue在线商城系统 毕业设计 Java毕业设计_哔哩哔哩_bilibili【免费】springbootvue在线商城系统 毕业设计 …

六何分析法分析uniApp

一、什么是 uniApp&#xff08;What&#xff09; uni-app 是一个使用 Vue.js 开发所有前端应用的框架&#xff0c;开发者编写一套代码&#xff0c;可发布iOS、Android、H5、以及各种小程序( 微信/支付宝/百度/头条/00/钉钉/淘宝)、快应用等多个平台。uni-app 在手&#xff0c;…

【蜗牛到家】获南明电子信息产业引导基金战略投资

智慧社区生活服务平台「蜗牛到家」已于近期获得贵阳南明电子信息产业引导基金、华科明德战略投资。 贵阳南明电子信息产业引导基金属于政府旗下产业引导基金&#xff0c;贵州华科明德基金管理有限公司擅长电子信息产业、高科技产业、城市建设及民生保障领域的投资&#xff0c;双…

TCP的滑动窗口机制

网络的错误检测和补偿机制非常复杂。 一、等待超时时间&#xff08;返回ACK号的等待时间&#xff09; 当网络繁忙时会发生拥塞&#xff0c;ACK号的返回变慢&#xff0c;较短的等待时间会导致频繁的数据重传&#xff0c;导致本就拥塞的网络雪上加霜。如果等待时间过长&#xf…

如何一个例子玩明白GIT

一个例子玩明白GIT GIT的介绍和教程五花八门&#xff0c;但实际需要用的就是建仓、推送、拉取等操作&#xff0c;这儿咱可以通过一个例子熟悉这些操作&#xff0c;一次性搞定GIT的使用方法学习。下面这个例子的内容是内容是建立初始版本库&#xff0c;然后将数据复制到 "远…

【Linux】第二十七站:内存管理与文件页缓冲区

文章目录 一、物理内存和磁盘交换数据的最小单位二、操作系统如何管理内存三、文件的页缓冲区四、基数树or基数&#xff08;字典树&#xff09;五、总结 一、物理内存和磁盘交换数据的最小单位 我们知道系统当中除了进程管理、文件管理以外&#xff0c;还有内存管理 内存的本质…

【数据结构】面试OJ题———栈|队列|互相实现|循环队列|括号匹配

目录 1. 有效的括号 思路&#xff1a; 2.用队列实现栈 思路&#xff1a; 3.用栈实现队列 思路&#xff1a; 4.设计循环队列 思路&#xff1a; 1. 有效的括号 20. 有效的括号 - 力扣&#xff08;LeetCode&#xff09; 给定一个只包括 (&#xff0c;)&#xff0c;{&…

Gazebo 跟踪8字形和U形轨迹(1) — 错误处理

Gazebo 跟踪8字形和U形轨迹(1) — 错误处理 整个过程还是比较曲折的&#xff0c;主要都是一些细小的问题&#xff0c;跑了很多遍模型才发现 参考轨迹生成问题不大&#xff0c;主要是参考横摆角和参考曲率部分有问题 atan和atan2 首先看下两者的区别 atan 函数&#xff1a;…

Electron[4] Electron最简单的打包实践

1 背景 前面三篇已经完成通过Electron搭建的最简单的HelloWorld应用了&#xff0c;虽然这个应用还没添加任何实质的功能&#xff0c;但是用来作为打包的案例&#xff0c;足矣。下面再分享下通过Electron-forge来将应用打包成安装包。 2 依赖 在Electron[2] Electron使用准备…

CF1898C Colorful Grid(构造)

题目链接 题目大意 n 行 m 列 的一个矩阵&#xff0c;每行有m - 1条边&#xff0c;每列有 n - 1 条边。 问一共走 k 条边&#xff0c;能不能从 &#xff08;1&#xff0c; 1&#xff09;&#xff0c;走到&#xff08;n&#xff0c; m&#xff09;&#xff0c;要求该路径上&am…

pandas数据处理闯关

pandas数据处理 第1关数据准备:将txt文件转成Excel文件第1关:将超市销售Excel文件根据商品的类别筛选存储任务描述相关知识:1. pd.read_excel()读取Excel文件2. DataFrame.to_excel() 向excel文件写入数据3. unique()唯一值函数4. 使用 loc 和 iloc 选择数据 本关代码 第2关数据…

基于YOLOv8深度学习的舰船目标分类检测系统【python源码+Pyqt5界面+数据集+训练代码】目标检测、深度学习实战

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

如何优雅地使用Mybatis逆向工程生成类

文/朱季谦 1.环境&#xff1a;SpringBoot 2.在pom.xml文件里引入相关依赖&#xff1a; 1 <plugin>2 <groupId>org.mybatis.generator</groupId>3 <artifactId>mybatis-generator-maven-plugin</artifactId>4 <version>1.3.6<…

【小沐学Python】Python实现WebUI网页图表(gradio)

文章目录 1、简介2、安装3、基本测试3.1 入门代码3.2 组件属性3.3 多个输入和输出组件3.4 图像示例3.5 聊天机器人3.6 模块&#xff1a;更灵活、更可控3.7 进度条 结语 1、简介 https://www.gradio.app/ Gradio是用友好的网络界面演示机器学习模型的最快方法&#xff0c;因此…

自然语言处理第2天:自然语言处理词语编码

​ ☁️主页 Nowl &#x1f525;专栏 《自然语言处理》 &#x1f4d1;君子坐而论道&#xff0c;少年起而行之 ​​ 文章目录 一、自然语言处理介绍二、常见的词编码方式1.one-hot介绍缺点 2.词嵌入介绍说明 三、代码演示四、结语 一、自然语言处理介绍 自然语言处理&#xf…

04-详解Eureka注册中心的作用,具体配置,服务注册和服务发现

Eureka注册中心的作用 Eureka架构 远程调用的两个问题 服务的ip地址和端口号写死: 生产环境中服务的地址可能会随时发生变化,如果写死每次都需要重新修改代码多实例问题: 在高并发的情况下一个服务可以有多个实例形成一个集群,此时如果采用硬编码的方式只能访问服务的一个实…

【代码随想录】算法训练计划41

dp 1、343. 整数拆分 题目&#xff1a; 给定一个正整数 n &#xff0c;将其拆分为 k 个 正整数 的和&#xff08; k > 2 &#xff09;&#xff0c;并使这些整数的乘积最大化。 返回 你可以获得的最大乘积 。 输入: n 10 输出: 36 解释: 10 3 3 4, 3 3 4 36。 思路…

揭秘字符串的奥秘:探索String类的深层含义与源码解读

文章目录 一、导论1.1 引言&#xff1a;字符串在编程中的重要性1.2 目的&#xff1a;深入了解String类的内部机制 二、String类的设计哲学2.1 设计原则&#xff1a;为什么String类如此重要&#xff1f;2.2 字符串池的概念与作用 三、String类源码解析3.1 成员变量3.2 构造函数3…

【小聆送书第二期】人工智能时代之AIGC重塑教育

&#x1f308;个人主页&#xff1a;聆风吟 &#x1f525;系列专栏&#xff1a;网络奇遇记、数据结构 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 &#x1f4cb;正文&#x1f4dd;活动参与规则 参与活动方式文末详见。 &#x1f4cb;正文 AI正迅猛地…