分析:Minecraft客户端Lunar的启动机制

分析:Minecraft客户端Lunar的启动机制

 次点击
99 分钟阅读

作为一个广受欢迎的流行PVP客户端 Lunar 以其优异的优化、丰富的功能,以及几乎全Minecraft版本兼容、Forge、Fabric兼容获得了用户的广泛喜爱。

本文将通过一系列逆向分析手段来解析Lunar客户端的启动流程、包括其类修补、模块化加载流程,并以此初步窥探其背后的多版本开发流程。

第一步:分析模块组成

启动参数

首先,我们先启动客户端,并用ProcessHacker(或其他任意相同功能的工具)获取其启动命令行参数:

C:\Users\***\.lunarclient\jre\56e53accb20696f802d92bd011174126b5e3154e\zulu21.30.15-ca-jre21.0.1-win_x64\bin\javaw.exe

[1] --add-modules

[2] jdk.naming.dns

[3] --add-exports

[4] jdk.naming.dns/com.sun.jndi.dns=java.naming

[5] -Dlog4j2.formatMsgNoLookups=true

[6] --add-opens

[7] java.base/java.io=ALL-UNNAMED

[8] -XX:+UseStringDeduplication

[9] -Dichor.filteredGenesisSentries=.*lcqt.*|.*Some of your mods are incompatible with the game or each other.*

[10] -Dlunar.webosr.url=file:index.html

[11] -Xmx3072m

[12] -Dichor.fabric.localModPath=C:\Users\***\.lunarclient\profiles\lunar\1.21\mods

[13] -Djava.library.path=natives

[14] -Dlog4j.configurationFile=C:\Users\***\.lunarclient\profiles\lunar\1.21\logs\config.xml

[15] -Dichor.logsFile=C:\Users\***\.lunarclient\profiles\lunar\1.21\logs\ichor-boot.log

[16] -XX:+DisableAttachMechanism

[17] -XX:-CreateCoredumpOnCrash

[18] -XX:-CreateMinidumpOnCrash

[19] -cp

[20] common-0.1.0-SNAPSHOT-all.jar;lunar-platform-mappings-v1_21_11.jar;genesis-0.1.0-SNAPSHOT-all.jar;lunar-lang.jar;lunar-emote.jar;lunar.jar;modern-0.1.0-SNAPSHOT-all.jar

[21] com.moonsworth.lunar.genesis.Genesis

[22] --version

[23] 1.21.11

[24] --launcherVersion

[25] 3.5.16-ow

[26] --launcherFeatureFlags

[27] {"enabled":["ServersCTA","PlaywireRamp","SocialMessaging","LaunchCancelling","SelectModpack","ServerRecommendedModpack","EmbeddedBrowserOpens","MissionControl","ExternalLinkSSO","HomeAdOverwolf","MissionControlAdOverwolf","Radio","RadioPremium","AMDDriverFix","ExploreModsAdOverwolf","MissionControlTopAdOverwolf","OverwolfCRN"],"disabled":["CustomizableQuickPlay","CommunityServers","NotificationsInbox","ProfileModsExploreCTA","MissionControlChat","LoaderVersionSetting","InstallVCRedistributable","OverwolfOverlay","TopbarOutplayedPromotion"]}

[28] --installationId

[29] 14c6e792-51cf-484e-9bf0-49a160795245

[30] --overwolfMuid

[31] 5f17ca54-f600-481d-a6f9-44853dc84917

[32] --sentryTraceId

[33] 516c8f1a15498ff3f11edfa714ee706b

[34] --launchId

[35] c03dde90-b4d3-468f-b90b-e47f23bcf712

[36] --canaryToken

[37] version-inject

[38] --username

[39] SuperSkidder

[40] --uuid

[41] 0e49e539cbeb47a9acf8a04957ebb9f6

[42] --xuid

[43] 2535421184951631

[44] --accessToken

[45] eyJxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

[46] --userProperties

[47] {}

[48] --assetIndex

[49] 1.21

[50] --gameDir

[51] C:\Users\***\AppData\Roaming\.minecraft

[52] --texturesDir

[53] C:\Users\***\.lunarclient\textures

[54] --uiDir

[55] C:\Users\***\.lunarclient\ui

[56] --webosrDir

[57] C:\Users\***\.lunarclient\offline\multiver\natives

[58] --workingDirectory

[59] .

[60] --classpathDir

[61] .

[62] --width

[63] 854

[64] --height

[65] 480

[66] --ipcPort

[67] 28190

[68] --ichorClassPath

[69] common-0.1.0-SNAPSHOT-all.jar,lunar-platform-mappings-v1_21_11.jar,genesis-0.1.0-SNAPSHOT-all.jar,lunar-lang.jar,lunar-emote.jar,lunar.jar,modern-0.1.0-SNAPSHOT-all.jar

[70] --ichorExternalFiles

[71] user-message-patterns.json,kill-sound-chat-patterns.json,tier-tagger.json,vanilla_capes.json,waypoint-patterns.json,hypixel/quickplay.json,hypixel/bedwars.json,hypixel/teamview.json,hypixel/skyblock/kuudra-waypoints.json,hypixel/skyblock/max-levels.json,hypixel/skyblock/autocomplete-warps.json,hypixel/skyblock/item-abilities.json,hypixel/skyblock/metal-detector-locations.json,hypixel/skyblock/commands.json,hypixel/skyblock/hoppity-eggs.json,hypixel/skyblock/mineshaft-corpse-waypoints.json,hypixel/skyblock/chocolate-factory-prices.json,hypixel/skyblock/enchants.json,hypixel/skyblock/important-items.json,hypixel/skyblock/garden.json,hypixel/skyblock/splits.json,hypixel/skyblock/skill-xp.json,hypixel/skyblock/middle-click.json,hypixel/skyblock/glacite-tunnels.json,hypixel/skyblock/vendor-items.json,hypixel/skyblock/item-shop-prices.json,hypixel/skyblock/dungeon/trash-items.json,hypixel/skyblock/dungeon/rooms.json,hypixel/skyblock/dungeon/routes.json,hypixel/skyblock/dungeon/quiz-key.json,hypixel/skyblock/dungeon/waterboard-solutions.json

分析入口

阅读以上的启动参数,我们可以看到一些与类加载可能相关的内容,下面是分析过程:

第一步,看启动入口,并逐层分析

[19] -cp

[20] common-0.1.0-SNAPSHOT-all.jar;lunar-platform-mappings-v1_21_11.jar;genesis-0.1.0-SNAPSHOT-all.jar;lunar-lang.jar;lunar-emote.jar;lunar.jar;modern-0.1.0-SNAPSHOT-all.jar

[21] com.moonsworth.lunar.genesis.Genesis

(使用ProcessHacker查看到他的工作目录在:C:\Users\xxx\.lunarclient\offline\multiver)

这是java的classpath和启动入口类,我们来反编译看下

在此之前,我们先大致查看这几个jar的作用,然后找到它的类加载器,并深入分析(mappings,emote,lang这几个资源类jar就省略不看了)

  1. common-0.1.0-SNAPSHOT-all.jar
    这个应该是类加载的一部分

  2. genesis-0.1.0-SNAPSHOT-all.jar
    这个就是本文要研究的类加载器了

  3. modern-0.1.0-SNAPSHOT-all.jar
    这个初步来看是Lunar客户端的多版本bridge一类的东西

  4. lunar.jar
    这个就是Lunar客户端本体以及跨版本的wrapper的代码了

第二步:分析启动流程

重点看genesis-0.1.0-SNAPSHOT-all.jar的相关代码

参数解析

package com.moonsworth.lunar.genesis;

import com.moonsworth.lunar.genesis.COIIRHCCOOOCRCRIHHHRHCRCHROCCC;
import com.moonsworth.lunar.genesis.ClientGameBootstrap;
import com.moonsworth.lunar.genesis.PreLaunchLibraryBootstrap;
import com.moonsworth.lunar.genesis.RCRICCRHHHIOIRCORCCICROHOHIOHR;
import com.moonsworth.lunar.ichor.util.ICHIRCRIRCCIICHCCCIRRROCOOCIRR;
import com.moonsworth.lunar.ichor.util.IIIORROICHCIICOOHIHIHCIOHRIHRC;
import com.moonsworth.lunar.ichor.util.OIIRHCHCRROCOHRCOCRRICOORROIRH;
import io.sentry.Sentry;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileAttribute;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import joptsimple.ArgumentAcceptingOptionSpec;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import joptsimple.OptionSpecBuilder;

public class Genesis {
    public static final OIIRHCHCRROCOHRCOCRRICOORROIRH LOGGER = new OIIRHCHCRROCOHRCOCRRICOORROIRH("Genesis").HRIHOOIRHOOHRCICCRICOOHRHIIROH(Paths.get(".ichor/genesis.log", new String[0]));
    public static final Path LUNARCLIENT_DATA = Paths.get(System.getProperty("user.home"), ".lunarclient");
    public static final Path DUMP_CLASSES_WHITELIST = Paths.get("./dump-whitelist.txt", new String[0]);
    public static final Path PRELAUNCH_LIB_DIR = Paths.get(System.getProperty("ichor.prelaunchLibDir", "./.ichor/ichormodule-libs"), new String[0]);
    public static final String FILTERED_ERRORS_REGEX = System.getProperty("ichor.filteredGenesisSentries", ".*lcqt.*");
    private static final String logs = System.getProperty("ichor.logsFile", "logs/ichor-boot.log");

    public static void main(String[] stringArray) {
        Locale.setDefault(Locale.ROOT);
        System.setOut(new IIIORROICHCIICOOHIHIHCIOHRIHRC(System.out, logs, false));
        System.setErr(new IIIORROICHCIICOOHIHIHCIOHRIHRC(System.err, logs, true));
        RCRICCRHHHIOIRCORCCICROHOHIOHR.init();
        try {
            Genesis.run(stringArray);
        }
        catch (Throwable throwable) {
            Throwable throwable2;
            if (throwable instanceof InvocationTargetException) {
                InvocationTargetException invocationTargetException = (InvocationTargetException)throwable;
                throwable2 = invocationTargetException.getCause();
            }
            System.err.println("An error occurred while launching Lunar Client.");
            throwable2.printStackTrace();
            int n = Genesis.matchesAnyExceptionMessage(throwable2, string -> string.contains("lcqt")) ? 58 : 1;
            Pattern pattern = Pattern.compile(FILTERED_ERRORS_REGEX, 32);
            if (n == 1 && !Genesis.matchesAnyExceptionMessage(throwable2, string -> pattern.matcher((CharSequence)string).matches())) {
                System.err.println("Sending Sentry with this report...");
                Sentry.captureException(throwable2);
            } else {
                System.err.println("Skipping Sentry report due to filter regex.");
            }
            System.out.flush();
            System.err.flush();
            System.exit(n);
        }
    }

    private static boolean matchesAnyExceptionMessage(Throwable throwable, Predicate<String> predicate) {
        for (Throwable throwable2 = throwable; throwable2 != null; throwable2 = throwable2.getCause()) {
            if (throwable2.getMessage() == null || !predicate.test(throwable2.getMessage())) continue;
            return true;
        }
        return false;
    }

    public static void run(String[] stringArray) {
        OptionParser optionParser = new OptionParser();
        optionParser.allowsUnrecognizedOptions();
        ArgumentAcceptingOptionSpec<String> argumentAcceptingOptionSpec = optionParser.accepts("version").withRequiredArg().ofType(String.class);
        ArgumentAcceptingOptionSpec<String> argumentAcceptingOptionSpec2 = optionParser.accepts("classpathDir").withRequiredArg().ofType(String.class);
        ArgumentAcceptingOptionSpec<String> argumentAcceptingOptionSpec3 = optionParser.accepts("workingDirectory").withRequiredArg().ofType(String.class);
        OptionSpecBuilder optionSpecBuilder = optionParser.accepts("integrationTest");
        ArgumentAcceptingOptionSpec<String> argumentAcceptingOptionSpec4 = optionParser.accepts("ichorClassPath").withRequiredArg().ofType(String.class);
        ArgumentAcceptingOptionSpec<String> argumentAcceptingOptionSpec5 = optionParser.accepts("ichorExternalFiles").withRequiredArg().ofType(String.class);
        OptionSpecBuilder optionSpecBuilder2 = optionParser.accepts("dumpClasses");
        ArgumentAcceptingOptionSpec<String> argumentAcceptingOptionSpec6 = optionParser.accepts("partialJar").withRequiredArg().ofType(String.class);
        ArgumentAcceptingOptionSpec<String> argumentAcceptingOptionSpec7 = optionParser.accepts("installationId").withRequiredArg().defaultsTo("not supplied", (String[])new String[0]);
        OptionSet optionSet = optionParser.parse(stringArray);
        boolean bl = optionSet.has(optionSpecBuilder);
        Path path = Paths.get(optionSet.valueOf(argumentAcceptingOptionSpec2), new String[0]);
        String string = optionSet.valueOf(argumentAcceptingOptionSpec3);
        Path path2 = new File(string).toPath();
        Path path3 = path2.resolve("overrides");
        String string2 = optionSet.valueOf(argumentAcceptingOptionSpec7);
        RCRICCRHHHIOIRCORCCICROHOHIOHR.setUser(string2);
        try {
            Files.createDirectories(path3, new FileAttribute[0]);
        }
        catch (Exception exception) {
            LOGGER.IOICRRICOIIICOCORHRRCHROHRICRH(OIIRHCHCRROCOHRCOCRRICOORROIRH.IOICRRICOIIICOCORHRRCHROHRICRH.WARN, exception);
        }
        List<String> list = null;
        if (optionSet.has(optionSpecBuilder2) && Files.exists(DUMP_CLASSES_WHITELIST, new LinkOption[0])) {
            list = Files.readAllLines(DUMP_CLASSES_WHITELIST);
            LOGGER.info("Using whitelist of classes to dump: " + String.valueOf(list), new Object[0]);
        }
        Path path4 = null;
        if (optionSet.has(argumentAcceptingOptionSpec6)) {
            path4 = Paths.get(optionSet.valueOf(argumentAcceptingOptionSpec6), new String[0]);
        }
        String string3 = optionSet.valueOf(argumentAcceptingOptionSpec);
        Path path5 = LUNARCLIENT_DATA.resolve("mx-cache");
        String[] stringArray2 = null;
        String[] stringArray3 = null;
        if (optionSet.has(argumentAcceptingOptionSpec4)) {
            stringArray2 = optionSet.valueOf(argumentAcceptingOptionSpec4).split(",");
        }
        if (optionSet.has(argumentAcceptingOptionSpec5)) {
            stringArray3 = optionSet.valueOf(argumentAcceptingOptionSpec5).split(",");
            for (String object2 : stringArray3) {
                LOGGER.info("Found external file: " + object2, new Object[0]);
            }
        }
        String[] stringArray4 = Genesis.getClasspath();
        URL[] uRLArray = new URL[stringArray4.length];
        for (int i = 0; i < stringArray4.length; ++i) {
            Path path6 = Paths.get(stringArray4[i], new String[0]);
            if (!string2.equals("not supplied")) {
                LOGGER.info("Found classpath URL: " + String.valueOf(path6) + " with checksum " + com.moonsworth.lunar.IICCIHOIHHIOCOCHIORORORRORCIOO.RCRICCRHHHIOIRCORCCICROHOHIOHR.COIIRHCCOOOCRCRIHHHRHCRCHROCCC.COIOCROORIRHHOHIRCRIORHHHROIRH(path6), new Object[0]);
            }
            uRLArray[i] = path6.toUri().toURL();
        }
        if (uRLArray.length == 0) {
            throw new IllegalStateException("No classpath URLs found from " + System.getProperty("java.class.path"));
        }
        ClassLoader classLoader = Genesis.class.getClassLoader();
        try (COIIRHCCOOOCRCRIHHHRHCRCHROCCC cOIIRHCCOOOCRCRIHHHRHCRCHROCCC = new COIIRHCCOOOCRCRIHHHRHCRCHROCCC("Prelaunch", uRLArray, classLoader);){
            PreLaunchLibraryBootstrap.IOICRRICOIIICOCORHRRCHROHRICRH iOICRRICOIIICOCORHRRCHROHRICRH = new PreLaunchLibraryBootstrap.IOICRRICOIIICOCORHRRCHROHRICRH(string3, path5, path, path3, PRELAUNCH_LIB_DIR);
            List list2 = (List)cOIIRHCCOOOCRCRIHHHRHCRCHROCCC.COIIRHCCOOOCRCRIHHHRHCRCHROCCC(PreLaunchLibraryBootstrap.class, iOICRRICOIIICOCORHRRCHROHRICRH);
            ArrayList arrayList = Arrays.stream(uRLArray).collect(Collectors.toCollection(ArrayList::new));
            for (Object object : list2) {
                try {
                    LOGGER.info("Adding pre-launch library to game classpath: " + String.valueOf(object), new Object[0]);
                    if (!ICHIRCRIRCCIICHCCCIRRROCOOCIRR.CORHOOIICIHHRRRIHIHCIRRIRHOCIC((Path)object)) {
                        throw new IllegalStateException("Library is not a file: " + String.valueOf(object));
                    }
                    arrayList.add(object.toUri().toURL());
                }
                catch (MalformedURLException malformedURLException) {
                    throw new RuntimeException(malformedURLException);
                }
            }
            try (COIIRHCCOOOCRCRIHHHRHCRCHROCCC cOIIRHCCOOOCRCRIHHHRHCRCHROCCC2 = new COIIRHCCOOOCRCRIHHHRHCRCHROCCC("Game", arrayList.toArray(new URL[0]), classLoader);){
                Object object;
                Thread.currentThread().setContextClassLoader(cOIIRHCCOOOCRCRIHHHRHCRCHROCCC2);
                object = new ClientGameBootstrap.IOICRRICOIIICOCORHRRCHROHRICRH(stringArray, path, path5, path3, string3, stringArray2, stringArray3, bl, path2, PRELAUNCH_LIB_DIR, path4, list);
                cOIIRHCCOOOCRCRIHHHRHCRCHROCCC2.COIIRHCCOOOCRCRIHHHRHCRCHROCCC(ClientGameBootstrap.class, object);
            }
        }
    }

    public static String[] getClasspath() {
        return System.getProperty("java.class.path").split(File.pathSeparator);
    }
}

加载类

除去一些日志、错误报告分析等内容,需要我们关注的核心逻辑如下(我们切换到IntelliJIDEA进行分析,因为可以借助IDE的分析和推断能力):

这里的


是他的自定义ClassLoader,也就是真正的类加载核心,而前面的其他代码,主要包括了Sentry的遥测集成、读取参数和一些参数的拼接处理等。

我们来看下这个ClassLoader的核心逻辑:
它先是创建了一个var46,传入了所需参数(主要是一个Uri列表和一个ClassLoader)
这其中的一个逻辑就是在findClass处做了一个排除,排除掉它自身,防止回环

下载资源

随后呢,它创建了PreLaunchLibraryBootstrap.IOICRRICOIIICOCORHRRCHROHRICRH var28
通过一些分析,可以看出,这个PreLaunchLibraryBootstrap实际上是一个下载器,用于从网络下载启动所需的所有library,并返回一个Uri列表


这一行的作用其实就是通过反射调用这个apply方法

既然明确了它的功能,那么里面的具体逻辑我们就粗略过一下,只需要知道最终返回的是一个jar的path列表即可

apply方法主要是获取了三个参数的内容ichor.fabricLoaderVersion、ichor.launch.libraries、ichor.prelaunchLibDir
然后传递到download方法中,download方法代码如下:

    public static List<Path> download(IRIOIOIRRRCOORHCOOOHHRIHOCOCOO var0, Map<String, String> var1, Path var2) {
        try {
            Files.createDirectories(var2);
            com.moonsworth.lunar.genesis.COIIRHCCOOOCRCRIHHHRHCRCHROCCC var3 = (com.moonsworth.lunar.genesis.COIIRHCCOOOCRCRIHHHRHCRCHROCCC)com.moonsworth.lunar.CRHORCHCOOCCRHORIHOOOIHCIOOHHR.IRIOIOIRRRCOORHCOOOHHRIHOCOCOO.class.getClassLoader();
            HashSet var4 = new HashSet();

            try {
                ServiceLoader.load(com.moonsworth.lunar.CRHORCHCOOCCRHORIHOOOIHCIOOHHR.RCOIOOHHRRCCRHRIIRORHCIRICCIII.class, var3).iterator().forEachRemaining((var1x) -> var4.addAll(Arrays.asList(var1x.RCOIOOHHRRCCRHRIIRORHCIRICCIII())));
            } catch (ServiceConfigurationError var6) {
                throw new ServiceConfigurationError("Failed to find PreLaunchLibraryRequests! PreLaunchLibrary is loaded in " + var3.getName() + " and PreLaunchLibraryRequest is loaded in " + com.moonsworth.lunar.CRHORCHCOOCCRHORIHOOOIHCIOOHHR.RCOIOOHHRRCCRHRIIRORHCIRICCIII.class.getClassLoader().getName() + " (they should be the same).", var6);
            }

            return var4.isEmpty() ? List.of() : (List)var4.stream().flatMap((var4x) -> {
                Path[] var5 = download(var4x, var0, (String)var1.get(var4x.name()), var2, var3);
                return Stream.of(var5);
            }).collect(Collectors.toList());
        } catch (Throwable var7) {
            throw var7;
        }
    }

这里用到了ServiceLoader,这是Java的一个SPI接口,用来实现运行时动态加载接口实现。

换言之,在这里的作用就是:可能有多套download实现,通过ServiceLoader这个spi获取所有的download接口实现,并且在下面用stream逐个执行download后,将返回的Path[] 列表collect在一起然后返回。


这就是上文的依次执行的download()方法,会从maven仓库中下载对应的jar

Transform

在全部下载完jar后,会对jar进行一次transform处理,也就是这段代码:


(这里有做一个缓存,如果不存在才会transform)

这里的var16 OCORRORHIOCOHHICOIHCIHCORRIIOR这个类的定义比较复杂,实现了多种transform逻辑,下面是AI分析给出的总结:

## 1) 类整体职责与核心字段

| 字段 | 推断含义 |

| --------------------------------------------------------------------------------------- | ------------------------------------------------------ |

| ICHIOROIOOCRRCICCICCHRRCOCHCHR : List<CRHORCHCOOCCRHORIHOOOIHCIOOHHR> | 注入阶段(InjectStage)列表,决定 class 变换顺序 |

| OHOOHIIHRHHHCCOHCIRRCHIIIIIHHH : Map<String, ICIIOOOCCIIOCOHIOIIOOHOCROICRI> | 已加载的 IchorModule(按 moduleId 索引) |

| RCCHOCRRHHIIIIHRICIOCCRIHRORCR : List<IICCIHOIHHIOCOCHIORORORRORCIOO> | IchorLoader 列表(模块提供的各种加载器/扩展点) |

| CORORCOOOIIHCHCHOCROHIRHCOHIIC : RCRICCRHHHIOIRCORCCICROHOHIOHR | “管线上下文/执行器”,真正负责对 ClassNode/bytes 执行 stage 的 transform |

| OHHICHRIHICIOCRCRHOIOHHRCRCRHH : OIOHHIIHHRCOCIRIIRIORROIOHICII<String,String,String> | 近似三维表:moduleId × fileKey → pathString(外部文件索引) |

| HIRHRIRHROIOCHHCHOCRHRRCIRIOCH : OIOHHIIHHRCOCIRIIRIORROIOHICII<String,String,String> | 近似三维表:namespace × from → to(字符串映射表,来自 json “mappings”) |

| CHOCCOHIORHIHHHIHHIHHHCOHIHROH : Map<String, COIIRHCCOOOCRCRIHHHRHCRCHROCCC> | entrypoint/启动点注册表(按 key 获取 entrypoint) |

| IIHRIIHIOIHIHRHOIIICOIHRHICIIC : Map<Object, RCOIOOHHRRCCRHRIIRORHCIRICCIII> | injection 实例 → injection 元数据 的映射(用于校验/查找注入对象) |

| IIIIOHRIOHHRIIHRCHHOOHOIHHHCOO : IRIOIOIRRRCOORHCOOOHHRIHOCOCOO | 环境/资源管理:classpathDir、overridesDir、缓存、查找已缓存 class 等 |

| IHCOIOOCORRRROIRCIOCOHHROHCRCH : Debug timings holder | 仅在 debug 开启时统计各种耗时 |

| OOHHCRROOOOIORHOHCHRHCRROIOOIC | 系统属性开关 ichor.debugPipelineTimings |

---

## 2) 方法作用表格(按“功能模块”分组)

### A. 构造/生命周期

| 方法签名(原名) | 推断作用 |

| ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

| OCORRORHIOCOHHICOIHCIHCORRIIOR(stages, env, classLoader) | 构建 pipeline:校验 stages;创建各种表;通过 ServiceLoader 扫描并加载 IchorModule;收集模块提供的 loaders;收集模块外部文件索引;加载 loaders;注册默认 entrypointfrex_flawless_frames);解析 loader 提供的 json mappings;输出 debug 统计 |

| close() | 关闭上下文/释放引用COROR...close();清理 map/list/set;debug 时清 timings |

### B. 动态加载/注册扩展

| 方法 | 推断作用 |

|---|---|

| IRIOIOIRRRCOORHCOOOHHRIHOCOCOO(loader) | 动态添加一个 loader:登记类名到 set;加入列表;调用 loader.loadIchor(context) |

| RCHRHRCHIRCRORIIHRCOHHROOCCIIR(key) | 获取/创建 entrypoint 注册器COIIRHCC...)并缓存 |

| IOICRRICOIIICOCORHRRCHROHRICRH(entryKey, cl, type, consumer) | 运行某个 entrypoint:找不到就 log;找到则 entrypoint.invoke(cl, type, consumer) |

### C. Class 变换(最核心)

| 方法 | 推断作用 |

| --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

| IOICRRICOIIICOCORHRRCHROHRICRH(className, bytes?, classLoader)CHRHICIRHCCCOIIRIRHHCHCORCRIOI | 按需获取并变换一个类:处理 stage 重映射导致的名称集合;尝试从缓存/资源/loader(ClassProvider)拿字节码;解析成 ASM ClassNode;从某个 stage 索引开始继续执行 transform;最后输出 bytes;找不到则抛 ClassNotFoundException;debug 记录耗时 |

| COIIRHCCOOOCRCRIHHHRHCRCHROCCC(classPathOrName, bytes?, cl)StageResult | 仅执行“阶段链式处理”,返回中间结构(类似“每个 stage 的 result”),用于批处理时先走一遍 stage remap/预处理 |

| IOICRRICOIIICOCORHRRCHROHRICRH(inputPath, outputPath, cl) | 批处理 jar/目录:遍历 class 文件;对每个 class 调用上面的 stage 处理 + transform;失败则降级写原始/中间结果;将最终 class 名可能做 “header/classname 修正” 后写出 |

### D. Mixins / 合成类 / 额外类定义

| 方法 | 推断作用 |

|---|---|

| RROOCCRHHOHHIOOIRIHOCIHRHIROCO(cl) | 让 context 以该 CL 初始化/扫描,然后收集所有启用 mixin runtime 的 mixin/target classes 列表(返回 Set<String>) |

| getSyntheticClasses() | 汇总所有运行时 mixin 产生的 synthetic classes |

| getExtraClassDefinitions() | 汇总所有运行时 mixin 产生的额外 class bytes(Map<className, bytes>) |

| RIIRRIHOHCHRHOIIHOROROHORCICCI() | 进入某种“mixin runtime 激活/冻结”状态:标记 context;把具备 mixin runtime 的 stage 加入一个列表;并把每个 pipeline 子单元标记为 ready/activeIRHRO...=true) |

| IOICRRICOIIICOCORHRRCHROHRICRH(cl, outPath) | 组合动作:获取 mixin 相关 class 集合;写入 outPath;并把所有外部文件(模块提供的 jars/资源)也注入到这个集合/输出(看起来像生成“需要加载/重定向”的集合) |

### E. 外部文件索引 / 路径解析 / 模块查询

| 方法 | 推断作用 |

|---|---|

| HCRRCCRHIHRCRIHICOOCOCHHRIOCHC(fileKey) | 从外部文件表中:按 fileKey 找任意一个路径字符串,解析为 Path |

| OIRRIOCIRHCICCIHOCOCRIRRRRICCH(pathString) | 把模块登记的字符串路径解析成实际 Path:支持 absolute:: 前缀;支持 .jar;支持在 overridesDir 中覆盖;否则在 classpathDir/overridesDir 下模糊匹配“以 var1 开头的文件名” |

| CHCOHCOHOHHOOIRCIOHCRROORHIIRH() | 返回所有外部文件(path)列表(排序后) |

| CCOOOCIIRHIIOCCOOCIRHHICOOCCCH(fileKey) | 根据 fileKey 反查“提供该文件的 module”(取 column 的任意 key) |

| hasModule(id) / IICHOOHCHROHOHHCCCHOORCHHHCHOR(id) | 查询模块是否存在 / 获取模块 Optional |

### F. 映射(json mappings / lorenz MappingSet)

| 方法 | 推断作用 |

|---|---|

| CRRCRCCIROCHIRIIRHCCRIIIHROROH() | 扫描某类 loader...IRIOIOIR...)返回的 JsonObject 列表,读取其中 "mappings" 字段,填充 HIRHRIR...namespace, from -> to |

| IOCIRHRCHRRRIHOHIHOROHCHRHOHRC(ns, key) | 查映射:如果 (ns,key) 存在映射返回 mapped,否则返回原 key |

| IOICRRICOIIICOCORHRRCHROHRICRH(mappingRef) | 从 env 的 mappings 仓库里取某个 mapping 文件(返回 Optional<...bytes holder>) |

| IOICRRICOIIICOCORHRRCHROHRICRH(mappingRef, readerFactory) | 取 mapping bytes,用给定 readerFactory 构造 MappingsReader,解析为 MappingSet |

| COIIRHCCOOOCRCRIHHHRHCRCHROCCC(mappingRef) | 同上,但使用固定 readerCHIIIO...createReader)解析为 MappingSet |

| IOICRRICOIIICOCORHRRCHROHRICRH(loaderType, mappingSet, classProvider) | 若存在某 loader 并实现特定接口,则把 mappings 与 classProvider 注入进去(用于 remap/resolve) |

### G. Debug/统计/辅助

| 方法 | 推断作用 |

|---|---|

| HCOHRICHRCCOHHCOHRHICHHCIOOCCC() | 打印 pipeline debug:总耗时、每个 class transform 耗时 Top、包聚合耗时、remapping 耗时、nectar timings/matches 等 |

| OICHHHIHRIHIIHRRHCOHRIRHHCHOOI(className) | debug 模式下取某个 class 的更详细统计对象 |

| IIIORROICHCIICOOHIHIHCIOHRIHRC(cl) | 切换“当前运行 classloader”;如果 cl 实现 ichor api 接口则把已加载的模块/loader类名集合传给它(类似 allowlist/共享信息) |

| OHHIHHRCCOORRRORRCHCROHCCOCCCC(obj) | obj → injection 元数据;若无则抛异常(校验:这不是一个 pipeline 注册过的 injection) |

---

## 3) 复杂方法拆解 + 关键代码片段逐步注释

### 3.1 核心IOICRRICOIIICOCORHRRCHROHRICRH(String, byte[], ClassLoader)(单类读取+变换)

```java

public CHRHICIRHCCCOIIRIRHHCHCORCRIOI getTransformedClass(

String requestedName, @Nullable byte[] providedBytes, ClassLoader runningCl

) {

// 0) module-info 特判:不做 transform,直接包装返回

if (requestedName.endsWith("module-info")) {

return new CHRHICIRHCCCOIIRIRHHCHCORCRIOI(requestedName, providedBytes);

}

// 1) 记录/切换当前运行的 ClassLoader(并可能通知 ichor api 的 classloader)

this.ensureRunningClassLoader(runningCl);

long start = now();

// 2) 计算“可能的类名集合 var7”

// 目的:InjectStage 可能会重映射/改名,所以要把各种可能名字都纳入候选,以便:

// - 从缓存命中

// - 从资源中找到真正的 .class

// - 或从 ClassProvider 拿到

String currentName = requestedName;

Set<String> candidates = new HashSet<>();

candidates.add(requestedName);

// 2.1) 从后往前跑一遍 stage 的“名称映射”,得到最终名字 currentName

for (int i = stages.size() - 1; i >= 0; --i) {

InjectStage st = stages.get(i);

currentName = context.remapName(st, currentName);

candidates.add(currentName);

}

// 2.2) 再从前往后跑一遍:把 candidates 经过每个 stage 的 remap 扩张

// 形成闭包:避免漏掉某些 stage 组合下产生的名字

for (InjectStage st : stages) {

Set<String> extra = new HashSet<>();

for (String c : candidates) {

extra.add(context.remapName(st, c));

}

candidates.addAll(extra);

}

// 3) 看 env/cache 是否已经给出了某个候选类的 bytes 以及“已经跑到哪个 stage”

int startStageIndex = 0;

String effectiveName = currentName;

byte[] bytes = providedBytes;

Optional<CachedHit> hit = env.lookupCachedClassBytes(candidates);

if (hit.isPresent()) {

// 命中缓存:说明该类可能已被处理到某个 stage 之后

startStageIndex = stages.indexOf(hit.stage()) + 1;

effectiveName = hit.className();

bytes = hit.bytes();

}

// 4) 若还没 bytes,则从 runningCl.getResourceAsStream / 自定义 CL / ClassProvider 取

if (bytes == null) {

InputStream in = null;

// 4.1) 优先用自定义 ClassLoader 的“特殊取资源方式”

Function<String, InputStream> resource = (runningCl instanceof SpecialCL)

? ((SpecialCL) runningCl)::getResourceStream

: runningCl::getResourceAsStream;

// 4.2) 尝试 effectiveName -> requestedName -> candidates 逐个找 .class

in = resource.apply(effectiveName.replace('.', '/') + ".class");

if (in == null) in = resource.apply(requestedName.replace('.', '/') + ".class");

if (in == null) {

for (String c : candidates) {

in = resource.apply(c.replace('.', '/') + ".class");

if (in != null) break;

}

}

// 4.3) 如果依然没找到 stream,尝试从 loader 中的 ClassProvider 直接取 bytes

if (in == null) {

for (Loader l : loaders) {

if (l instanceof ClassProvider) {

bytes = ((ClassProvider) l).get(effectiveName.replace('.', '/'));

if (bytes != null) break;

}

}

}

// 4.4) 如果找到 stream,则 readAllBytes

if (bytes == null && in != null) {

bytes = in.readAllBytes();

in.close();

}

}

// 5) 有 bytes 则 parse 成 ASM ClassNode(允许后续 transform)

ClassNode node = null;

if (bytes != null) {

ClassReader cr = new ClassReader(bytes);

node = new ClassNode();

cr.accept(node, 0);

}

// 6) 准备“要执行的 stage 子数组”:从 startStageIndex 开始到结尾

InjectStage[] remainingStages = stages.subList(startStageIndex, stages.size()).toArray(...);

// 7) 特判:如果是 mixin 本身的类(org/spongepowered/asm/mixin/),绕开某部分 transform

if (node == null || !node.name.startsWith("org/spongepowered/asm/mixin/")) {

try {

// 对 ClassNode 做一系列 stage 处理(可能包含重映射、mixin 织入等)

node = context.transformNode(remainingStages, runningCl, true, effectiveName, node);

} catch (Throwable t) {

throw new FatalIchorError("Failed to transform " + requestedName, t);

}

}

// 8) 最后一步:把最终 stage 输出为 bytes(写回/生成)

bytes = context.toBytes(remainingStages[remainingStages.length - 1], runningCl, node, true);

// 9) 如果输出 bytes 还是 null,认为找不到该类

if (bytes == null) {

throw new ClassNotFoundException("IchorPipeline can't find class in " + runningCl.getName()

+ ": " + effectiveName + "(" + requestedName + ")");

}

// 10) 输出 className 以 ClassNode 的 name 为准

String finalName = node.name.replace('/', '.');

// 11) debug 记录耗时

if (debug) timings.classTimes.put(finalName, now()-start);

return new CHRHICIRHCCCOIIRIRHHCHCORCRIOI(finalName, bytes);

}

```

这段方法的核心目的

- 解决“类名经过多 stage 可能发生变换”的问题(候选集合闭包)

- 多来源获取原始 bytes(cache / ClassLoader resource / ClassProvider)

- 把 class 逐 stage 变换为最终 bytes,并带 debug 计时

---

### 3.2 外部文件路径解析OIRRIOCIRHCICCIHOCOCRIRRRRICCH(String)

```java

public Optional<Path> resolveExternalPath(String spec) {

Path classpath = env.classpathDir();

Path overrides = env.overridesDir();

boolean hasOverrides = overrides != null;

Path result;

// 1) absolute::/xxx/yyy 直接当绝对路径

if (spec.startsWith("absolute::")) {

result = Paths.get(spec.substring("absolute::".length()));

// 2) 以 .jar 结尾:当作相对文件,优先 overridesDir,其次 classpathDir

} else if (spec.endsWith(".jar")) {

if (hasOverrides) {

result = overrides.resolve(spec);

if (Files.notExists(result)) result = classpath.resolve(spec);

} else {

result = classpath.resolve(spec);

}

// 3) 否则当作“前缀匹配文件名”:在目录下 list,找 fileName startsWith(spec)

} else {

result = findFirstStartsWith(spec, classpath);

// overrides 若存在则覆盖

if (hasOverrides) {

Path overrideHit = findFirstStartsWith(spec, overrides);

if (overrideHit != null) result = overrideHit;

}

}

return Optional.ofNullable(result);

}

```

它体现了一个典型的资源覆盖逻辑:**overridesDir > classpathDir**,并支持“前缀匹配”以适应带版本号的文件名(例如 xxx-1.2.3.jar)。

---

### 3.3 解析 JSON mappingsCRRCRCCIROCHIRIIRHCCRIIIHROROH()

该方法扫描 loader 给的 json,读取结构大致如下:

```json

{

"mappings": {

"namespaceA": { "from1": "to1", "from2": "to2" },

"namespaceB": { "x": "y" }

}

}

```

然后写入表 HIRHRIRHROIOCHHCHOCRHRRCIRIOCH.put(namespace, from, to),用于 IOCIRHRCHRRRIHOHIHOROHCHRHOHRC(ns, key) 查询。

---

## 4) 工作流程(从初始化到变换/输出)

### 4.1 初始化阶段(构造函数)

1. 校验必须至少有一个 InjectStage

2. 初始化各种容器(模块表、loader 列表、外部文件表、entrypoint 表、debug timings 等)。

3. 用 ServiceLoader.load(IchorModule, pipelineClassLoader) 扫描模块:

- 记录模块类名到 COOCIIICO...(后续可能用于 classloader allowlist/共享信息)

- modulesById.put(moduleId, module)

- 从模块取 loaders:加入 RCCHO...,并记录 loader 类名

- 从模块取外部文件映射:写入 OHHICHR...(moduleId × fileKey → pathString)

4. 对所有 loader 调用 loadIchor(context) 完成注册/初始化。

5. 注册内置 entrypointfrex_flawless_framesFlawlessFrames::register

6. 扫描 loader 的 json 并提取 "mappings",填充字符串映射表 HIRHRIR...

7. debug 开启时输出加载耗时、外部文件数、loader 数等。

### 4.2 运行时“按需变换 class”

当外部(通常是自定义 ClassLoader)请求某个类时,会调用:

- getTransformedClass(name, maybeBytes, runningCL)

其内部会:

1. 确认当前运行 CL 并在切换时通知 ichor api classloader(把已加载模块/loader类名集合发给它)。

2. 计算候选类名集合(解决多 stage remap)。

3. 走 cache 命中则跳过已完成的 stage。

4. 否则从 resource / ClassProvider 获取 bytes。

5. 用 ASM 解析为 ClassNode 并执行剩余 stages 的 transform。

6. 输出最终 bytes,返回 {finalClassName, bytes}

### 4.3 批处理输出(transform jar/目录)

- transformPath(in, out, cl) 会遍历 class 文件:

- 先跑一次 stage 预处理(得到可能的新类名/中间结果)

- 再调用单类 transform

- 失败则降级写未完全变换结果(避免整个批处理失败)

### 4.4 mixin runtime 相关能力

- 可以收集 mixin/target class 集合、synthetic classes、extra class definitions

- RIIRRIHO...() 类似“切到运行时织入模式/冻结阶段列表”,为 mixin runtime 做准备。

经过我的阅读,它主要实现的功能是:

将多个jar合并,transform其中需要修改的class后,合并到一个jar里

这其中tranformer的逻辑有这几个:



这个方法比较长,主要的作用是remap以及执行pipeline的多道injection逻辑,其中包括一些类的特殊处理,比如mixin等等




会对classnode进行变换,变换完成后又通过这个方法转换回bytes然后写入

具体的transform逻辑比较复杂,我这里就简单概括一下,它做的事有下面几个:
1. 使用mapping进行remap
2. 初始化mixin环境
3. inject fabric的trigger触发代码
4. 其他的一些兼容性处理

总结

类加载

lunar客户端的启动主要依赖包名为com.moonsworth.lunar.ichor的类加载器

启动游戏后只加载了少数几个启动jar包,随后genesis包会从启动参数中获取需要加载的所有jar,然后执行到PreLaunchLibraryBootstrap中时,通过maven仓库下载完整的游戏启动jars

随后进入到类加载逻辑:

genesis会通过多个classloader,执行多道工序(包括初始化mixin,使其生效),最终实现的目的是:对游戏的类进行一次”烘焙“

它会将所有的类在通过RCRICCRHHHIOIRCORCCICROHOHIOHR这个类进行pipeline处理后的bytes,通过OCORRORHIOCOHHICOIHCIHCORRIIOR这个类合并成一个jar,以便加速后续启动(烘焙后的结果保存在.lunarclient\offline\multiver\cache)

跨版本

跨版本的实现主要依靠 各版本单独的mixin实现 + wrapper(lunar实现的跨版本api,也就是包名为com.lunarclient的部分) + 模块化加载 实现。

有关Lunar的跨版本wrapper设计,我会在另外的文章中解析。

© 本文著作权归作者所有,未经许可不得转载使用。