# 让 APK 轻一点 Source: https://yumeyuka.moe/blog/apk-shrink/index ***过去对 Android 内部的一些机制了解得并不深入,所以这些内容之前也没有怎么接触过*** # 问题引入 之前在 Android 端 App 开发中集成 [Sub-Store](https://github.com/sub-store-org/Sub-Store) 学习了 SubCase 这样的一个做法,后端是由 [Javet](https://github.com/caoccao/Javet)驱动的这么一个方法 Release 打包之后的 Apk 大小居然有 80M,这还是不算其他的,仅仅是 Sub-Store 这么一个 feature 引入大小体积就变化这么大,解包之后发现绝大多数体积都来自 Javet 这么一个库,而且发现并未启用旧版打包方式来压缩体积,衡量一下对比发现,在省流量和省体积之间选择前者收益应该是高一点的,然后就发了一个 PR [https://github.com/sionnx/SubCase/pull/4](https://github.com/sionnx/SubCase/pull/4) ```kotlin theme={null} // https://developer.android.com/reference/tools/gradle-api/8.0/com/android/build/api/dsl/JniLibsPackagingOptions#useLegacyPackaging() // var useLegacyPackaging: Boolean? useLegacyPackaging = true ``` 启用之后看起来打包体积优化收益还是挺高的,**82MB → 22 MB** ,但是对在 YumeBox 中已经打包 Mihomo 作为内核情况下体积还是有一些大。 作为扩展性 feature 可选性比较重要,并不需要强制打包在内,需要时扩展下载即可,例如 Mt 管理器的模拟终端扩展包 ## Extension APK 方案 那么如果要实现集成 [Sub-Store](https://github.com/sub-store-org/Sub-Store) 这个 feature ,那么 Javet 肯定是绕不开的, 如果做成扩展插件属性会不会可行? 答案是肯定的,Mt 管理器就是这么做的,将 二进制文件封装到扩展包中 `Lib` * 扩展 App/ * assets * lib/ * arm64-v8a/ * adb * bash * More Binary * META-INF/ * res/ * .../ 那么做法就是在打包时候将 `Javet` 这个库排除不进行打包 ```kotlin build.gradle.kts icon=code focus={3} theme={null} packaging { jniLibs { excludes += listOf("lib/**/libjavet*.so") useLegacyPackaging = true } } ``` 现在是将 `Javet` 不包含的打包,那么真正需要用到的时候需要去下载扩展包 [Extension Apk Demo](https://github.com/YumeYucca/YumeBox/tree/Moe/extension), 在 `AndroidManifest.xml` 声明权限,Android 的 应用可见性(**Package Visibility** )机制. ```xml theme={null} ``` 当然扩展 app 仅仅是一个容器作用,不需要时间的代码和依赖加入,将代码和依赖默认附加的内容排除,做到最简化 详见 [YumeBox/blob/Moe/extension/build.gradle.kt](shttps://github.com/YumeYucca/YumeBox/blob/Moe/extension/build.gradle.kts) ```kotlin build.gradle.kts expandable icon=code theme={null} plugins { id("com.android.application") } dependencies { implementation(libs.javet.node.android) } android { sourceSets { getByName("main") { jniLibs.directories.apply { clear() add("jniLibs") } } } tasks.withType().configureEach { doFirst { appMetadata.asFile.orNull?.writeText("") } } packaging { jniLibs { useLegacyPackaging = true } resources { excludes += listOf("META-INF/**") } } dependenciesInfo { includeInApk = false includeInBundle = false } buildTypes { debug { isMinifyEnabled = false } release { isMinifyEnabled = true isShrinkResources = true vcsInfo.include = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", ) } } splits { abi { isEnable = true reset() val abiList = (gropify.abi.extension.list ?: "arm64-v8a") .split(',') .map { it.trim() } .filter { it.isNotEmpty() } // Qualify receiver so Kotlin does not resolve include() against Iterable from the // chain. @Suppress("SpreadOperator") this@abi.include(*abiList.toTypedArray()) isUniversalApk = false } } } tasks.register("collectJavetNative") { group = "distribution" description = "Extracts the arm64-v8a Javet native library for the Expand release" from(provider { configurations.getByName("releaseRuntimeClasspath").files.map(::zipTree) }) { include("jni/arm64-v8a/libjavet-node-android.v.5.0.9.so") rename { "libjavet.so" } eachFile { relativePath = RelativePath(true, name) } includeEmptyDirs = false } into(rootProject.layout.projectDirectory.dir("output_apk/Expand")) } tasks.register("compressJavetNative") { group = "distribution" description = "Compresses the Javet native library as an XZ release asset" dependsOn("collectJavetNative") val inputFile = rootProject.layout.projectDirectory.file("output_apk/Expand/libjavet.so") val outputFile = rootProject.layout.projectDirectory.file("output_apk/Expand/libjavet.so.xz") inputs.file(inputFile) outputs.file(outputFile) commandLine("7z", "a", "-y", "-txz", "-mx=9", outputFile.asFile, inputFile.asFile) } ``` 那么做壳完成之后具体将 `Javet` 库打包到扩展 App 之后应该怎么做呢?直接时候 load 其实就可以。其实这里将库打包到扩展包中还有另一个用途, 由于之前的误判,认为 `libxxx.so` 需要可执行权限才可以正常 load。打包扩展包另一个用途便是从扩展包的 lib 解压保留可执行权限, ~~好像是这样,还是复制一份到私有目录然后 load 来着,忘记了~~ 相关的 issue * [https://github.com/LuckyPray/DexKit/issues/24](https://github.com/LuckyPray/DexKit/issues/24) * [https://github.com/android/ndk/issues/979](https://github.com/android/ndk/issues/979) **那么结论就是并不需要可执行权限,放到宿主私有目录即可,即所有组为 App** ```mermaid placement="top-right" actions={true} theme={null} %%{init: {'flowchart': {'defaultRenderer': 'elk'}}}%% flowchart LR A[App 初始化] --> B[检测目标 SO] B --> C{SO 是否存在} C -->|是| D[加载 SO] C -->|否| E[跳过加载] D --> F{加载成功?} F -->|是| G[初始化 Native 功能] F -->|否| H[禁用相关功能] E --> H H --> I[等待 SO 可用条件] I --> J{SO 是否可用} J -->|否| I J -->|是| D G --> K[功能正常运行] ``` # 资源压缩 上面有提到对于 `Libxxx.so` Load **不需要可执行权限,放到宿主私有目录即可**, 那么从网络下载 download 也属于这一类,放到宿主私有目录然后 Load,进一步的省去了安装扩展包的复杂性 那么这样 Javet 体积问题就解决了,那么能不能对 YumeBox 进一步瘦身呢? 有的有的,还可以吧 Geo 等文件压缩一下,(ps:Geo 等文件并不包含 buildMRS.7z)CMFA 并未压缩这部分, 理论可行。那么最终选择是压缩率最高但解压慢的 `xz` 算法,那么最终的收益是多少呢 15MB -> 8M,收益看起来挺可观的 回到 Geo 文件本身,打包进 apk 这部分属于配置兼容性,如果不存在,内核也会自动 download,亦或者内核自动更新这部分,观察下面这部分代码,CMFA 贴的,比较安装日期然后更新 Geo 文件, 所以这部分也可以做成可选的,并不需要强制打包在 apk 内部,可通过 app 手动或者自动更新这部分,对于首次安装保持兼容性显然需要打包进去,后面如果不清除数据覆盖安装未打包的也可行 为了保持兼容性,一到两切就比较重要了,即使不需要 Geo 文件,首次安装也需要强制 builtin 版本 ```kotlin MainApplication.kt expandable icon=code theme={null} https://github.com/MetaCubeX/ClashMetaForAndroid/blob/app/src/main/java/com/github/kr328/clash/MainApplication.kt private fun extractGeoFiles() { clashDir.mkdirs() val updateDate = packageManager.getPackageInfo(packageName, 0).lastUpdateTime val geoipFile = File(clashDir, "geoip.metadb") if (geoipFile.exists() && geoipFile.lastModified() < updateDate) { geoipFile.delete() } if (!geoipFile.exists()) { FileOutputStream(geoipFile).use { assets.open("geoip.metadb").copyTo(it) } } val geositeFile = File(clashDir, "geosite.dat") if (geositeFile.exists() && geositeFile.lastModified() < updateDate) { geositeFile.delete() } if (!geositeFile.exists()) { FileOutputStream(geositeFile).use { assets.open("geosite.dat").copyTo(it) } } val asnFile = File(clashDir, "ASN.mmdb") if (asnFile.exists() && asnFile.lastModified() < updateDate) { asnFile.delete() } if (!asnFile.exists()) { FileOutputStream(asnFile).use { assets.open("ASN.mmdb").copyTo(it) } } val bundleMRSFile = File(clashDir, "BundleMRS.7z") if (bundleMRSFile.exists() && bundleMRSFile.lastModified() < updateDate) { bundleMRSFile.delete() } if (!bundleMRSFile.exists()) { FileOutputStream(bundleMRSFile).use { assets.open("BundleMRS.7z").copyTo(it) } } } ``` ## Native 压缩尝试 如果 `Geo` 文件都 `xz` 压缩打包了,那么能不能让 `so` 也压缩一下呢?上面已经证实过,宿主目录内 load 不需要可执行权限。 那么看下来,理论似乎是可行的,大体实例就是在 Java 层做一个可以, init 初始化时 load 所有的 lib 这部分在实现时遇到了阻碍,具体原因是 lib 等由 App 自己完成解压 load,安装时并未解压设置可执行权限,导致 ELF 可执行权限丢失, 一般正常 Load share 没什么问题 ```mermaid placement="top-right" theme={null} %%{init: {'flowchart': {'defaultRenderer': 'elk'}}}%% flowchart LR A[App 初始化] --> B[读取压缩资源] B --> C{资源类型} C -->|Geo 文件| D[XZ 解压] C -->|SO 文件| E[XZ 解压] D --> F[写入宿主目录] E --> F F --> G{SO 是否存在} G -->|是| H[System.load 加载 SO] G -->|否| I[禁用相关功能] H --> J[初始化 Native 功能] ``` ## Native 解压 Loader `xz` 是一种压缩率高,但是解压慢的算法,Java 层的 load解压速度还是太慢了,选择 Native C 语言重写,仅实现需要的部分,在实际打包时候这部分 loader并不需要 xz 压缩,需要排除 ~~总不能解压工具还压缩吧~~ ```C theme={null} // https://github.com/YumeYucca/YumeBox/tree/Moe/pack/native FetchContent_Declare( xz URL https://github.com/tukaani-project/xz/releases/download/v5.8.1/xz-5.8.1.tar.gz URL_HASH SHA256=507825b599356c10dca1cd720c9d0d0c9d5400b9de300af00e4d1ea150795543 ) ``` ## Java 层压缩 嗯,既然 lib native 都 xz 压缩了,那能不能 Java 层也 压缩下呢? 可以的 !! * App/ * assets/ * lib/ * META-INF/ * res/ * src/ * classes.dex * resources.arsc * AndroidManifest.xml * ... Java 层经过编译打包后也就是 `classes.dex` 文件,如果这部分也压缩的话,那么应该由谁来负责解压自己然后 load 呢, 嗯,方法就是做壳,自己解压自己然后 load 自己,那么原本的负责 load Native 的方法也放到壳里面, 这样就可以实现 Java 层也压缩的效果,而且不需要额外的依赖,复用之前的 Native 解压方法即可 ```mermaid placement="top-right" theme={null} %%{init: {'flowchart': {'defaultRenderer': 'elk'}}}%% flowchart LR A[App 初始化] --> B[读取压缩资源] B --> C{资源类型} C -->|Java 文件| D[XZ 解压] C -->|SO 文件| E[XZ 解压] ``` 那么应该怎么实现呢,详见源码 [ApplicationBridge.java](https://github.com/YumeYucca/YumeBox/blob/Moe/pack/src/dev/yume/loader/ApplicationBridge.java) ```java loader expandable icon=code theme={null} // https://github.com/YumeYucca/YumeBox/blob/Moe/pack/src/dev/yume/loader/ final class ApplicationBridge { private ApplicationBridge() { } static Application create(Context base, ClassLoader loader, String className) { try { Class type = Class.forName(className, true, loader); Application application = (Application) type.getDeclaredConstructor().newInstance(); Method attach = ReflectionAccess.findMethod(Application.class, "attach", Context.class); attach.invoke(application, base); return application; } catch (ReflectiveOperationException error) { throw new IllegalStateException("Unable to create the original Application: " + className, error); } } @SuppressWarnings("unchecked") static void replace(Application shell, Application original) { try { Object base = ReflectionAccess.get(shell, "mBase"); Object loadedApk = ReflectionAccess.get(base, "mPackageInfo"); Object activityThread = ReflectionAccess.get(loadedApk, "mActivityThread"); ReflectionAccess.set(loadedApk, "mApplication", original); ReflectionAccess.set(activityThread, "mInitialApplication", original); List applications = (List) ReflectionAccess.get(activityThread, "mAllApplications"); int index = applications.indexOf(shell); if (index >= 0) { applications.set(index, original); } ReflectionAccess.set(base, "mOuterContext", original); } catch (ReflectiveOperationException error) { throw new IllegalStateException("Unable to replace the loader Application", error); } } } ``` 需要注意的是,xz Native 压缩后的资源不需要放到 Lib 目录,放到 assets 目录避免二次解压 ## 总结 至此,完成的从 Native Assets 到 Java 层的压缩,收益非常明显,如果在不打包 Geo 等文件,仅作为更新包安装时,体积大小仅 18-20 M ,(会随着内核大小而波动)即使打包 Geo 文件,那么依然是目前最小的 mihomo 客户端 仅仅是 Mihome 官方 Release 发布大小也差不多接近 YumeBox 更新时安装包大小,(相当于仅更新内核了说是) 以下均统计打包 Geo 文件的时比对 | Android 客户端 | 体积大小 | | ------------------- | ------- | | YumeBox | 35 MB | | ClashMetaForAndroid | 42.8 MB | | FlClash | 52.4 MB | | ClashMi | 43.9 MB | # 给代码塞点新玩具 Source: https://yumeyuka.moe/blog/load-so-kernel-switch/index ***每一次探索的起点,往往都是一个「为什么」*** ## 前情提要 接下来的内容与上一篇文章有关联,部分内容上一篇已经提到,这里不再赘述 # 为什么要换一种方式 在 YumeBox 开发初期,Core 部分代码并未独立实现,而是直接复用了 ClashMetaForAndroid 的 [Core JNI 实现](https://github.com/MetaCubeX/ClashMetaForAndroid/tree/main/core/src)。毕竟本着“***少造轮子***”的原则,直接复用成熟代码是开发初期较为平稳的做法 从实际运行效果来看,这套 JNI Bridge 表现确实相当稳定;加之上游内核并未提供 Android Go mobile 绑定,CMFA Core 的 `FFI `方案可以说是最佳实践 但在桌面端和 Web 面板间的通信,则是选择另一种方式 [REST API](https://wiki.metacubex.one/api/),由内核提供的 API ## 换一种实现方式 ClashMetaForAndroid 的这套 JNI Bridge 的 FFI 方案,虽然确实稳定可用,但在不断的迭代中,手写 JNI 的**维护成本与链路复杂度**逐渐显现出来 ```mermaid actions={true} theme={null} flowchart LR A["Kotlin UI / Service"] --> B["JNI Bridge"] B --> C["C / CGO 胶水层"] C --> D["Go Core"] D --> C --> B --> A B -."配置、线程、异常、对象生命周期".-> E["跨语言状态耦合"] ``` Core 部分的代码占比并不小,同时存在 `CGO` 和 C 以及 JNI,但是耦合太重,如果要增加一些新的 feature 时,就要同时改动 `CGO` `C/JNI` 和 `kotlin` 部分,太过繁琐 ## Root 权限下 Tun 的运行 在 Android 设备上,Mihomo 运行由 App 建立 `VPN service`路由流量,受限于 Root 权限的缺失,并不是 **Native Tun** 的实现 成熟的 Root 方案例如 KernelSU Magisk 等,且用户群体也不少,但是大多数都在使用 Magisk 模块的透明代理。于是开箱即用的 Tun 实现成为了目标的 feature 之一 但问题出现各个层面,起初的解决方案是,是由 [Libsu](https://github.com/topjohnwu/libsu) 直接创建 Root service,补齐 JNI 侧的传参即可,但新的问题接踵而至,在设计中,由 `Root `权限创建的进程或者 service 应该不跟随 App 的生命周期,换句话说就是,当 APP 被系统或者主动 kill 掉时,仍在代理流量 由 `su` 拉起的 `Root `核心,本质上应该是一个独立的守护进程:App 被系统回收以后,核心不应该立刻停止,代理流量也不应该突然中断。可如果核心完全依赖 `JNI `和绑定 `Service`,那么 `App `重启以后,之前的进程状态、控制连接和配置都很难恢复 ```mermaid actions={true} theme={null} flowchart LR A["App 启动 Root Service"] --> B["JNI 直接拉起核心"] B --> C["核心创建 Root Tun"] D["系统回收 App"] --> E["Service / JNI 状态丢失"] E --> F["核心仍可能存活,但通信状态断开"] F --> G["App 重启后难以恢复"] ``` ## 核心与壳 经过研究发现,**将 Share 换成 PIE 执行**,可以解决 Core 的复用问题,将 JNI Bridge 重写实现,做一个的壳,只需要将核心部分拉起运行即可,顺带解决了 JNI 通信状态回连,由 `Unix Socket` 通信,当然,这是标准做法 当然,这里会出现另一个问题:如果直接将内核打包成可执行文件,就会像上一篇文章中提到的那样——在将 lib 打包并压缩,但这里已经是 PIE 可执行文件,并不是 share 共享库。这就会导致可执行权限丢失,而且另一个问题是,在安卓上,安卓 SDK 29 限制了可执行文件的运行位置:在宿主私有目录是不能直接运行可执行文件的,能运行的位置只有 APP 解压之后的 lib 目录 所以这就在打包时出现了一个问题:真正收益大小带来的主要是核心的打包,但如果核心坚持作为 PIE 可执行文件,那么后面就没有办法继续作为 load 共享库。最后确定的方向是:核心仍然编译成共享库,但不再让 Kotlin 直接加载并调用大量 Go 导出函数。新增一个很小的 PIE 启动壳,由它负责 `dlopen` 真正的 `libmihomocore.so`,再调用唯一的 `MihomoMain` 入口 ```c theme={null} // shell.c:启动壳只负责加载核心并转交入口 void *library = dlopen("libmihomocore.so", RTLD_NOW | RTLD_LOCAL); int (*main_entry)(int, char **, char *) = dlsym(library, "MihomoMain"); return main_entry(argc, argv, channel); ``` ```go theme={null} // entry.go:核心业务从这里开始,但不再暴露几十个 JNI 方法 //export MihomoMain func MihomoMain(argc C.int, argv **C.char, channel *C.char) C.int { return C.int(runMain(readArgs(argc, argv), C.GoString(channel))) } ``` ## 进程间通信 进程独立以后,新的问题就变成了:配置怎么给核心?VPN 建立出来的 TUN FD 怎么给核心?核心建立的出站 socket 又如何请求 Android 的 `protect()`? 最后的方案没有再设计一套复杂的 JNI 协议,而是使用 Unix 原生的 `socketpair`。启动阶段创建一对 `AF_UNIX + SOCK_SEQPACKET`,一端留在 Kotlin,另一端跟随子进程进入 Go 核心 ```c theme={null} int channel[2]; if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, channel) < 0) { /* 通信通道创建失败,核心不能继续启动 */ } ``` 之所以选择 `SOCK_SEQPACKET`,是因为它保留了消息边界。配置可以分成多条消息发送,核心每次读取到的就是一条完整消息,不需要额外设计长度头,也不会出现 TCP 常见的粘包和半包问题 ```kotlin theme={null} // Channel.kt:一次调用对应一条消息。 channel.writeMessage(configBytes, offset, size) ``` ```mermaid theme={null} flowchart LR App["Kotlin / App"] Socket["Unix socketpair"] Core["Go Core"] App -->|"配置块(多次)"| Socket Socket -->|"配置数据"| Core Core -.->|"owner / protect 请求"| Socket Socket -.->|"Android 处理结果"| App ``` 这条 Socket 不对外监听端口,只存在于父子进程之间。核心启动完成以后,它还会继续保留,用来处理两类 Android 才能完成的请求:查询连接属于哪个 App,以及保护核心自己的出站 socket ## Socket 保护 VPN 模式下,TUN 接收设备流量并交给核心。核心随后要连接远端代理服务器。如果这个连接也被 VPN 捕获,它又会回到 TUN,核心再次读取,最后形成一个非常典型的回环 这里不能简单地把整个 App UID 排除出 VPN,因为 App 自己的普通网络请求仍然需要经过代理。当前实现采用的是“按 socket 保护”:核心每次准备建立出站连接时,把这个 socket 的 FD 通过 `SCM_RIGHTS` 发回 App,请求 `VpnService.protect(fd)` ```go theme={null} // hooks.go:在 connect 前把核心 socket 交给 Android 处理 dialer.DefaultSocketHook = func(_, _ string, conn syscall.RawConn) error { var protectErr error conn.Control(func(fd uintptr) { protectErr = rpc.protect(int(fd)) }) return protectErr } ``` ```kotlin theme={null} // CoreProcess.kt:只保护核心传来的 socket,处理后立即关闭副本 private fun protectCoreSocket(fd: Int): String = try { if ((context as? VpnService)?.protect(fd) == true) "protected" else "denied" } finally { ParcelFileDescriptor.adoptFd(fd).close() } ``` ## 节点预览 当然,在桌面端或者 Web 端,可以通过核心实时运行时的 API 获取当前节点列表。但如果是在 Android 端,没有一个持续运行的 Service 去把内核拉起来运行,那么节点要怎么预览呢? 如果不启动真正的核心进程,又想拿到节点列表,其实这个问题也很简单。简单来讲,就是再做一个壳。 真正启动的时候,再把这个负责启动核心的壳替换掉;而当前用于预览的壳,只负责读取配置,然后提供一个 API 用来预览节点。它并不会真正启动核心,更不会接管实际的代理流量。 ## 内核切换 这里和上一篇文章有一些关联。在上一篇文章中介绍过,共享库的 `load` 加载和可执行权限并没有直接关系。也就是说,只要共享库存放在宿主目录中,就可以正常通过 `load` 加载 结合上面提到的方案,核心依然作为共享库打包,而不是直接作为 PIE 可执行文件。这样一来,只需要构建不同版本的内核源码,就可以在运行时切换内核 而壳层并不会影响核心的加载,因为不同内核的入口点都是一样的 至于内核的切换方案,基本上可以直接从**云端获取**。但是 Android 端受限于共享库,而不是官方提供的可执行文件,所以我们需要单独创建一个工作流,实时同步上游源码,然后构建出对应的内核 这也是后来 smart 分支不再单独构建,而是直接合并到主项目中,通过切换的方式使用的原因 ## 总结 这一套方案从根本上来讲,并没有对内核源码打多少补丁,仅仅修改了构建时所需要的 tag 限制,再通过一些简单的壳把真正的内核拉起来 这样也就不需要像 ClashMetaForAndroid 那样手写大量 JNI 代码和 CGO 导出 从可持续维护的角度来看,这种方案的维护成本也比较低。因为真正需要维护的只是比较简单的壳,而不是直接通过 FFI 去修改内核源码 例如 FlClash 的内核并不是直接实时同步上游源码进行构建,而是在 fork 源码之后,再 Patch 到源码中创建 FFI 接口 **当然,在大多数情况下,我们并不希望对上游源码进行过多修改** 所以这套方案总结下来,真正修改的部分其实只有构建限制 tag。换句话说,在尽可能不修改上游源码的情况下,实现了完整的 Root 内核进程、TUN 以及内核切换 ## 自定义图标扩展 这里用到的方法也比较简单: 1. 在文档页面创建一个组件,实时裁切用户上传的图标 2. 将裁切后的图标中转上传到 Cloudflare S3,作为临时存储,并返回一个链接 3. 前端指向构建仓库中的一个 Issue,并创建对应的 Issue 模板 4. 用户原样填写这个模板,然后触发 GitHub Action 方案的优点是:不需要更换签名,也不需要修改其他内容 # 好朋友 Source: https://yumeyuka.moe/friends # 梦璃酱的小作坊 Source: https://yumeyuka.moe/index **Ciallo~(∠・ω\< )⌒★** 初次见面,我是 **桜川夢璃**,也可以叫我 **梦璃酱** 喜欢写代码,也希望把日常生活点缀得稍微甜一点的工程师 喜欢安静地做点事情,也喜欢追番。 日子大多在屏幕前度过,一会儿敲点东西,一会儿看点喜欢的内容,在两种世界之间慢慢切换(´-\) 不太追求效率,更在意过程。 比起「做了多少」,更希望留下些什么是自己真正喜欢的。 还在成长中,也在摸索自己的方向,希望这里能慢慢堆起一些温柔的小成果( ´ ▽ \ ) 真正的成长并非抹杀过去的自己,而是在接纳伤 痛后,依然有选择善良的勇气 ## 最近的小成果 在成长和学习中不断前进 ## 投喂一下 投喂 # Cmake 笔记 Source: https://yumeyuka.moe/notes/cmake/index ***こういう絵、目開いてる方が激重感情抱いてがち*** ## CMake Generators **Cmake** 是一个配置程序,有时会被称为 **“元”** 构建系统, **Cmake** 并不负责编译构建整个项目, 相反,**Cmake** 会根据环境配置、以及工具链,生成用于解决不同平台构建系统 在 **Linux** 平台上常用的编译器是 `GCC`,在 **Windows** 上,常用的编译器是 `MSVC`,在 **MacOS** 上,常用的编译器是 `Clang`,对于不同的编译平台构建差异,Cmake 正是为了解决这些问题而存在的 **“元”** 构建系统 ## 安装 CMake 在使用之前先安装 Cmake 环境 下载链接 [https://cmake.org/download](https://cmake.org/download) 在官网下载对应系统的安装包,Windows 是 `msi` 安装版或者 `zip` 解压后 path 添加环境变量 对于 Linux 推荐是使用管理器,例如 **ArchLinux** , 推荐 `yay` 或 `paru` 进行安装 ```shell Install icon="code" theme={null} yay -S cmake ``` 然后在终端输入 `cmake --version` 查看是否安装成功 ```shell Version icon="code" theme={null} $ cmake --version cmake version 4.4.3 CMake suite maintained and supported by Kitware (kitware.com/cmake). ``` ### 使用基础知识 ```shell Commad icon="code" theme={null} # 指定项目根目录,目录下必须包含 CMakeLists.txt 文件 cmake -S # 指定构建目录,Cmake 会将生成的构建文件及产物输出到指定目录 cmake -B # 构建项目,在指定的目录中运行构建 cmake -- build ``` ## 创建一个 Cmake 项目 CMakeLists.txt 是 Cmake 的配置文件,有时候会被称为 **列表文件** 或者 `CML`, 在项目中,不仅是项目顶层目录,子模块也可以包含该文件 ```cmake CMakeLists.txt icon="code" theme={null} cmake_minimum_required(VERSION 3.23) # 指定 Cmake 版本 project(MyProject) # 指定项目名称 add_executable(MyProject main.cpp) # 添加可执行文件 ``` ## 配置文件介绍 未完待续... # 配置 LLVM 工具链 Source: https://yumeyuka.moe/notes/llvm/index ***配置工具链也是一门学问*** # 开发工具选择 工欲善其事,必先利其器 * [Visual Studio Code](https://www.bing.com/ck/a?!&\&p=ba3366210734759ca6c2ff0d8e4d6f20c38c25a26de1a59524eef47831e1732aJmltdHM9MTc4NzcwMjQwMA\&ptn=3\&ver=2\&hsh=4\&fclid=3a0b8182-881b-6bf4-0357-963f89946ab0\&u=a1aHR0cHM6Ly9jb2RlLnZpc3VhbHN0dWRpby5jb20v\&ntb=1) * [CLion](https://www.jetbrains.com/clion/) * [Visual Studio IDE](https://visualstudio.microsoft.com/zh-hans/vs/) 我比较喜欢 Jetbrains 家的 IDE,选择 CLion 作为开发工具,可能是 IDEA 的原因,Jetbrains 家的 IDE 上手都比较快 # 工具链选择 当然是选择 LLVM MinGW 啦 一般选择 [llvm-mingw-20260616-ucrt-x86\_](https://github.com/mstorsjo/llvm-mingw/releases/download/20260616/llvm-mingw-20260616-ucrt-x86_64.zip)[64.zip](http://64.zip) ,下载 release 然后解压工具链存放问题,然后添加 Path 到环境变量 然后 CLion 配置工具链这样,直接添加即可,然后在 Cmake 工具链处添加选择的工具链,例如 LLVM 然后随便创建一个新的项目,写点代码看看 ```cpp theme={null} #include auto main() -> int { printf("Hello, World!"); return 0; } ``` # 关于格式化 `.clang-format` 的一些自用配置 ```yaml .clang-format expandable theme={null} --- BasedOnStyle: LLVM Language: Cpp Standard: Latest ColumnLimit: 106 IndentWidth: 4 TabWidth: 4 ConstructorInitializerIndentWidth: 4 ContinuationIndentWidth: 4 LineEnding: LF UseCRLF: false UseTab: Never InsertNewlineAtEOF: false AccessModifierOffset: -4 AlignAfterOpenBracket: Align AlignArrayOfStructures: Right AlignConsecutiveAssignments: Consecutive AlignConsecutiveBitFields: Consecutive AlignConsecutiveDeclarations: Consecutive AlignConsecutiveMacros: Consecutive AlignConsecutiveShortCaseStatements: Enabled: true AcrossEmptyLines: false AcrossComments: false AlignCaseArrows: true AlignCaseColons: true AlignEscapedNewlines: Left AlignOperands: AlignAfterOperator AlignTrailingComments: true AllowAllArgumentsOnNextLine: true AllowAllParametersOfDeclarationOnNextLine: true AllowBreakBeforeNoexceptSpecifier: Always AllowShortBlocksOnASingleLine: Always AllowShortCaseExpressionOnASingleLine: true AllowShortCaseLabelsOnASingleLine: true AllowShortCompoundRequirementOnASingleLine: true AllowShortEnumsOnASingleLine: true AllowShortFunctionsOnASingleLine: All AllowShortIfStatementsOnASingleLine: AllIfsAndElse AllowShortLambdasOnASingleLine: All AllowShortLoopsOnASingleLine: true AlwaysBreakAfterDefinitionReturnType: None AlwaysBreakAfterReturnType: None AlwaysBreakBeforeMultilineStrings: false AlwaysBreakTemplateDeclarations: Yes BinPackArguments: true BinPackParameters: true BitFieldColonSpacing: After BraceWrapping: AfterCaseLabel: false AfterClass: false AfterControlStatement: Never AfterEnum: false AfterExternBlock: false AfterFunction: false AfterNamespace: false AfterStruct: false AfterUnion: false BeforeCatch: false BeforeElse: false BeforeLambdaBody: false BeforeWhile: false IndentBraces: false SplitEmptyFunction: true SplitEmptyRecord: true SplitEmptyNamespace: true BreakAdjacentStringLiterals: false BreakAfterAttributes: Leave BreakAfterReturnType: None BreakArrays: true BreakBeforeBinaryOperators: NonAssignment BreakBeforeBraces: Attach BreakBeforeConceptDeclarations: false BreakBeforeInlineASMColon: OnlyMultiline BreakBeforeTernaryOperators: true BreakBinaryOperations: Never BreakConstructorInitializers: AfterColon BreakFunctionDefinitionParameters: false BreakInheritanceList: AfterColon BreakStringLiterals: false BreakTemplateDeclarations: true CompactNamespaces: false Cpp11BracedListStyle: true DerivePointerAlignment: false DisableFormat: false EmptyLineAfterAccessModifier: Never EmptyLineBeforeAccessModifier: LogicalBlock FixNamespaceComments: false IncludeBlocks: Regroup IncludeCategories: - Regex: '^<.*/.*\.h>$' Priority: 4 SortPriority: 0 CaseSensitive: false - Regex: '^<.*\.h>' Priority: 3 SortPriority: 0 CaseSensitive: false - Regex: '^<.*/.*' Priority: 2 SortPriority: 0 CaseSensitive: false - Regex: '^<.*' Priority: 1 SortPriority: 0 CaseSensitive: false - Regex: '.*' Priority: 5 SortPriority: 0 CaseSensitive: false IndentAccessModifiers: false IndentCaseBlocks: false IndentCaseLabels: true IndentExternBlock: NoIndent IndentGotoLabels: true IndentPPDirectives: BeforeHash IndentRequiresClause: false IndentWrappedFunctionNames: true InsertBraces: false InsertTrailingCommas: None IntegerLiteralSeparator: Binary: 0 BinaryMinDigits: 0 Decimal: 0 DecimalMinDigits: 0 Hex: 0 HexMinDigits: 0 KeepEmptyLines: AtEndOfFile: false AtStartOfBlock: false AtStartOfFile: false LambdaBodyIndentation: OuterScope MaxEmptyLinesToKeep: 1 NamespaceIndentation: All PackConstructorInitializers: NextLine PointerAlignment: Left PPIndentWidth: -1 QualifierAlignment: Custom QualifierOrder: - type - static - friend - constexpr - const - volatile - inline - restrict ReferenceAlignment: Pointer ReflowComments: true RemoveBracesLLVM: true RemoveParentheses: ReturnStatement RemoveSemicolon: true RequiresClausePosition: SingleLine RequiresExpressionIndentation: OuterScope SeparateDefinitionBlocks: Leave ShortNamespaceLines: 0 SkipMacroDefinitionBody: false SortIncludes: CaseSensitive SortUsingDeclarations: true SpaceAfterCStyleCast: false SpaceAfterLogicalNot: false SpaceAfterTemplateKeyword: false SpaceAroundPointerQualifiers: Default SpaceBeforeAssignmentOperators: true SpaceBeforeCaseColon: false SpaceBeforeCpp11BracedList: false SpaceBeforeCtorInitializerColon: false SpaceBeforeInheritanceColon: true SpaceBeforeJsonColon: false SpaceBeforeParens: ControlStatements SpaceBeforeRangeBasedForLoopColon: true SpaceBeforeSquareBrackets: false SpaceInEmptyBlock: false SpaceInEmptyParentheses: false SpacesBeforeTrailingComments: 1 SpacesInAngles: Never SpacesInCStyleCastParentheses: false SpacesInConditionalStatement: false SpacesInContainerLiterals: false SpacesInLineCommentPrefix: Minimum: 1 Maximum: -1 SpacesInParentheses: false SpacesInSquareBrackets: false VerilogBreakBetweenInstancePorts: true ```