为 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(): 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++” 了。

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

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

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().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 顶级定义了四个路径: 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

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

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

相关文章

如何一个例子玩明白GIT

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

轻量封装WebGPU渲染系统示例<45>- 材质组装流水线(MaterialPipeline)灯光、阴影、雾(源码)

当前示例源码github地址: https://github.com/vilyLei/voxwebgpu/blob/feature/material/src/voxgpu/sample/MaterialPipelineFog.ts 当前示例运行效果: 此示例基于此渲染系统实现&#xff0c;当前示例TypeScript源码如下&#xff1a; export class MaterialPipelineFog {pr…

数组创建方法

数组的创建 1.let a[] 2.let anew Array(5) 3.let anew Array(1,2,3) 4.let a[1,2,3] 创建数组是空还是有值是以上四种写法。但是如果没给值的变量是undefined&#xff0c;再a[0]找不到这种变量的。所以当找某一个数需要已经是数组内存。不想给值可以给空数组。只要是数组…

MEMS制造的基本工艺介绍——晶圆键合

晶圆键合是一种晶圆级封装技术&#xff0c;用于制造微机电系统 (MEMS)、纳米机电系统 (NEMS)、微电子学和光电子学&#xff0c;确保机械稳定和气密密封。用于 MEMS/NEMS 的晶圆直径范围为 100 毫米至 200 毫米&#xff08;4 英寸至 8 英寸&#xff09;&#xff0c;用于生产微电…

2023-12-05 Qt学习总结7

点击 <C 语言编程核心突破> 快速C语言入门 Qt学习总结 前言二十 QTcpSocket QTcpServer网络库服务端代码:客户端代码 二十一 QProcess进程类二十二 QThread线程总结 前言 要解决问题: 学习qt最核心知识, 多一个都不学. 二十 QTcpSocket QTcpServer网络库 QTcpSocket和…

持续集成交付CICD:Jenkins流水线实现Nexus制品晋级策略

目录 一、理论 1.开发测试运维环境 二、实验 1.Nexus制品晋级策略 一、理论 1.开发测试运维环境 &#xff08;1&#xff09;环境 1&#xff09;持续集成开发环境&#xff08;DEV: Development Environment&#xff09; 直接通过源代码编译打包&#xff0c;其会跑单元测试…

机器学习算法(9)——集成技术(Bagging——随机森林分类器和回归)

一、说明 在这篇文章&#xff0c;我将向您解释集成技术和著名的集成技术之一&#xff0c;它属于装袋技术&#xff0c;称为随机森林分类器和回归。 集成技术是机器学习技术&#xff0c;它结合多个基本模块和模型来创建最佳预测模型。为了更好地理解这个定义&#xff0c;我们需要…

WLAN配置实验

本文记录了WLAN配置实践的过程&#xff0c;该操作在华为HCIA中属于相对较复杂的实验&#xff0c;记录过程备忘。这里不就WLAN原理解释&#xff0c;仅进行配置实践&#xff0c;可以作为学习原理时候的参考。本文使用华为ENSP进行仿真。实验拓扑图如下&#xff1a; 1.WLAN工作流程…

UE5数据传递-纹理贴图

期待结果&#xff1a; 流程 1. 通过C写入数据到纹理贴图 2. 在材质中通过采样能正确读取写入的数值 踩坑&#xff1a; 1. UE5之后&#xff0c;需要设置采样类型&#xff0c;才能达到上图效果&#xff0c;默认采样类型做了插值计算 FColor中写入 PF_B8G8R8A8 UTexture2D* Conve…

AI 绘画 | Stable Diffusion 艺术二维码制作

前言 这篇文章教会你如果用Stable Diffusion WEB UI制作艺术二维码,什么是艺术二维码呢?就是普通二维码和艺术图片融合后的二维码图片,如下图所示。主要原理还是使用controlNet的control_v1p_sd15_qrcode_monster模型和光影模型control_v1p_sd15_brightness。 教程 准备…

【论文阅读笔记】NeRF+Mip-NeRF+Instant-NGP

目录 前言NeRF神经辐射场体渲染连续体渲染体渲染离散化 方法位置编码分层采样体渲染推导公式&#xff08;1&#xff09;到公式&#xff08;2&#xff09;部分代码解读相机变换&#xff08;重要&#xff01;&#xff09; Mip-NerfTo do Instant-NGPTo do 前言 NeRF是NeRF系列的…

DIP——边缘提取与分割

1.使用canny算法进行边缘提取 本实验比较简单&#xff0c;基本思路是对原图像进行一个高斯模糊处理&#xff0c;用于去噪&#xff0c;之后转换为灰度图&#xff0c;直接调用cv库中的canny记性边缘提取。若想直接得到彩色边缘&#xff0c;则通过按位与操作&#xff0c;将原始彩色…

ingress介绍和ingress通过LoadBalancer暴露服务配置

目录 一.ingress基本原理介绍 1.将原有用于暴露服务和负载均衡的服务的三四层负载均衡变为一个七层负载均衡 2.controller和ingress 3.通过下面这个图可能会有更直观的理解 二.为什么会出现ingress 1.NodePort存在缺点 2.LoadBalancer存在缺点 三.ingress三种暴露服务的…

基于JavaWeb+SSM+Vue微信小程序的科创微应用平台系统的设计和实现

基于JavaWebSSMVue微信小程序的科创微应用平台系统的设计和实现 源码获取入口Lun文目录前言主要技术系统设计功能截图订阅经典源码专栏Java项目精品实战案例《500套》 源码获取 源码获取入口 Lun文目录 1系统概述 1 1.1 研究背景 1 1.2研究目的 1 1.3系统设计思想 1 2相关技术…

Linux Component概述和高通component的使用

1 Linux为什么要引入Component框架&#xff1f; 为了让subsystem按照一定顺序初始化设备才提出来的。 subsystem中由很多设备模块&#xff0c;内核加载这些模块的时间不确定。子系统内有些模块是需要依赖其它模块先初始化才能进行自己初始化工作(例如v4l2 subdev和v4l2 video …

【C语言程序设计】编写简单的C程序

目录 前言 一、程序设计 二、程序改错 三、程序完善 总结 &#x1f308;嗨&#xff01;我是Filotimo__&#x1f308;。很高兴与大家相识&#xff0c;希望我的博客能对你有所帮助。 &#x1f4a1;本文由Filotimo__✍️原创&#xff0c;首发于CSDN&#x1f4da;。 &#x1f4e3;如…

unity 2d 入门 飞翔小鸟 死亡 显示GameOver(十四)

1、添加Img create->ui->img 把图片拖进去 2、和分数一样、调整位置 3、修改角色脚本 using System.Collections; using System.Collections.Generic; using UnityEngine;public class Fly : MonoBehaviour {//获取小鸟&#xff08;刚体&#xff09;private Rigidbod…

【Apipost】批量删除我的51CTO文章

文章目录 一、序二、API分析三、Apipost测试四、脚本五、Apipost中完成 一、序 去年开始再51CTO同步更新文章&#xff0c;一年多过去了&#xff0c;只涨了3个粉丝。看了下这个平台就是卖课、搞培训的&#xff0c;退出了。决定把文章也删除了&#xff08;有人私信我说专门注册了…

ToolkenGPT:用大量工具增强LLM

深度学习自然语言处理 原创作者&#xff1a;cola 用外部工具增强大型语言模型(LLM)已经成为解决复杂问题的一种方法。然而&#xff0c;用样例数据对LLM进行微调的传统方法&#xff0c;可能既昂贵又局限于一组预定义的工具。最近的上下文学习范式缓解了这一问题&#xff0c;但有…

Shell 常用命令详解-上

Shell 常用命令详解-上 1.目录查阅相关命令2.文件操作相关命令 1.目录查阅相关命令 ll 命令 命令描述&#xff1a;ll命令用于显示指定工作目录下的内容。 命令格式&#xff1a;ll [参数] [目录名]。 参数说明&#xff1a; 参数说明-a显示所有文件及目录&#xff08;包括隐藏文…