# 让 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
```